INSERT ... ON CONFLICT UPDATE and RLS
The patch that implements INSERT ... ON CONFLICT UPDATE has support
and tests for per-column privileges (which are not relevant to the
IGNORE variant, AFAICT). However, RLS support is another thing
entirely. It has not been properly thought out, and unlike per-column
privileges requires careful consideration, as the correct behavior
isn't obvious.
I've documented the current problems with RLS here:
https://wiki.postgresql.org/wiki/UPSERT#RLS
It's not clear whether or not the auxiliary UPDATE within an INSERT...
ON CONFLICT UPDATE statement should have security quals appended.
Stephen seemed to think that that might not be the best solution [1]/messages/by-id/20141121205926.GK28859@tamriel.snowman.net -- Peter Geoghegan.
I am not sure. I'd like to learn what other people think.
What is the best way of integrating RLS with ON CONFLICT UPDATE? What
behavior is most consistent with the guarantees of RLS? In particular,
should the implementation append security quals to the auxiliary
UPDATE, or fail sooner?
[1]: /messages/by-id/20141121205926.GK28859@tamriel.snowman.net -- Peter Geoghegan
--
Peter Geoghegan
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Mon, Jan 5, 2015 at 12:54 PM, Peter Geoghegan <pg@heroku.com> wrote:
The patch that implements INSERT ... ON CONFLICT UPDATE has support
and tests for per-column privileges (which are not relevant to the
IGNORE variant, AFAICT). However, RLS support is another thing
entirely. It has not been properly thought out, and unlike per-column
privileges requires careful consideration, as the correct behavior
isn't obvious.I've documented the current problems with RLS here:
https://wiki.postgresql.org/wiki/UPSERT#RLS
It's not clear whether or not the auxiliary UPDATE within an INSERT...
ON CONFLICT UPDATE statement should have security quals appended.
Stephen seemed to think that that might not be the best solution [1].
I am not sure. I'd like to learn what other people think.What is the best way of integrating RLS with ON CONFLICT UPDATE? What
behavior is most consistent with the guarantees of RLS? In particular,
should the implementation append security quals to the auxiliary
UPDATE, or fail sooner?
I think the INSERT .. ON CONFLICT UPDATE shouldn't be able to attempt
an update unless the UPDATE policies of the table are such that a
regular UPDATE would find the affected row. The post-image of the row
needs to satisfy any UPDATE CHECK OPTION. If the INSERT fails due to
a conflict with an unseen row, and the UPDATE can't find that row
either due to RLS, then it should probably error out; the alternative
is to silently do nothing, but that feels wrong.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Tue, Jan 6, 2015 at 9:39 AM, Robert Haas <robertmhaas@gmail.com> wrote:
I think the INSERT .. ON CONFLICT UPDATE shouldn't be able to attempt
an update unless the UPDATE policies of the table are such that a
regular UPDATE would find the affected row. The post-image of the row
needs to satisfy any UPDATE CHECK OPTION. If the INSERT fails due to
a conflict with an unseen row, and the UPDATE can't find that row
either due to RLS, then it should probably error out; the alternative
is to silently do nothing, but that feels wrong.
I can certainly see the argument for that behavior. I'm inclined to
agree that this is better.
With th existing implementation, UPDATE check options are effective at
preventing updates. However, the implementation is not effective at
preventing row locking from finding a row that the user would not
otherwise be able to find (with a conventional UPDATE). So I guess
what I have to do now is figure out a way of also having the "... FOR
UPDATE .... USING ( )" qual be enforced after row locking in respect
of the row locked (locked, but not yet used to generate a post-image)
in the target table. If it isn't satisfied, throw an error that
doesn't leak anything about the target row, rather than silently not
affecting the row. So, as you say, a divergence from what regular RLS
UPDATEs do that happens to make more sense here.
--
Peter Geoghegan
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Another issue that I see is that there is only one resultRelation
between the INSERT and UPDATE. That means that without some additional
special measure, both UPDATE and INSERT "WITH CHECK ( ... ) " options
are applied regardless of whether the INSERT path was taken, or the
UPDATE path. Maybe that's actually sensible (note that even this
doesn't happen right now, since the auxiliary UPDATE is currently
minimally processed by the rewriter). What do people think about that?
It seems like it might be less surprising to have that fail earlier
when there are separate policies for INSERT and UPDATE -- for UPSERT,
all policies are applied regardless of which path is taken.
The task of making the security qual enforced like a check option
before we go to update a locked row (at the start of the UPDATE path,
for any UPDATE policy with a USING security qual) looks complicated.
I'd appreciate a little help on the implementation details.
Apparently Oracle does not offer "fine-grained access control" with
MERGE, which I believe means they just don't support this kind of
thing at all. Obviously I'd rather avoid that here, but the correct
semantics are not obvious. ON CONFLICT UPDATE could almost justify
making CREATE POLICY FOR INSERT accept a USING expression, since
that's really where the row to UPDATE comes from.
--
Peter Geoghegan
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 6 January 2015 at 20:44, Peter Geoghegan <pg@heroku.com> wrote:
Another issue that I see is that there is only one resultRelation
between the INSERT and UPDATE. That means that without some additional
special measure, both UPDATE and INSERT "WITH CHECK ( ... ) " options
are applied regardless of whether the INSERT path was taken, or the
UPDATE path. Maybe that's actually sensible (note that even this
doesn't happen right now, since the auxiliary UPDATE is currently
minimally processed by the rewriter). What do people think about that?
It seems like it might be less surprising to have that fail earlier
when there are separate policies for INSERT and UPDATE -- for UPSERT,
all policies are applied regardless of which path is taken.
I think the policies applied should depend on the path taken, so if it
does an INSERT, then only the INSERT CHECK policy should be applied
(after the insert), but if it ends up doing an UPDATE, I would expect
the UPDATE USING policy to be applied (before the update) and the
UPDATE CHECK policy to be applied (after the update). I would not
expect the INSERT CHECK policy to be applied on the UPDATE path.
As to whether the UPDATE USING policy should cause an error to be
thrown if it is not satisfied, my inclination would be to not error,
and use the command tag to report that no rows were updated, since
that is what would happen with a regular UPDATE.
So overall INSERT .. ON CONFLICT UPDATE would be consistent with
either an INSERT or an UPDATE, depending on whether the row existed
beforehand, which is easier to explain than having some special UPSERT
semantics.
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Jan 7, 2015 at 4:04 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
I think the policies applied should depend on the path taken, so if it
does an INSERT, then only the INSERT CHECK policy should be applied
(after the insert), but if it ends up doing an UPDATE, I would expect
the UPDATE USING policy to be applied (before the update) and the
UPDATE CHECK policy to be applied (after the update). I would not
expect the INSERT CHECK policy to be applied on the UPDATE path.
I agree.
As to whether the UPDATE USING policy should cause an error to be
thrown if it is not satisfied, my inclination would be to not error,
and use the command tag to report that no rows were updated, since
that is what would happen with a regular UPDATE.
My inclination would be to error, but I'd be OK with your proposal.
So overall INSERT .. ON CONFLICT UPDATE would be consistent with
either an INSERT or an UPDATE, depending on whether the row existed
beforehand, which is easier to explain than having some special UPSERT
semantics.
Yeah. We won't escape the question so easily where triggers are
concerned, but at least for RLS it seems like it should be possible to
avoid confusing, one-off semantics.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:
On 6 January 2015 at 20:44, Peter Geoghegan <pg@heroku.com> wrote:
Another issue that I see is that there is only one resultRelation
between the INSERT and UPDATE. That means that without some additional
special measure, both UPDATE and INSERT "WITH CHECK ( ... ) " options
are applied regardless of whether the INSERT path was taken, or the
UPDATE path. Maybe that's actually sensible (note that even this
doesn't happen right now, since the auxiliary UPDATE is currently
minimally processed by the rewriter). What do people think about that?
It seems like it might be less surprising to have that fail earlier
when there are separate policies for INSERT and UPDATE -- for UPSERT,
all policies are applied regardless of which path is taken.I think the policies applied should depend on the path taken, so if it
does an INSERT, then only the INSERT CHECK policy should be applied
(after the insert), but if it ends up doing an UPDATE, I would expect
the UPDATE USING policy to be applied (before the update) and the
UPDATE CHECK policy to be applied (after the update). I would not
expect the INSERT CHECK policy to be applied on the UPDATE path.
You're making a distinction where I'm not convinced there should be one.
While I understand that, internally, there are two paths (INSERT vs.
UPDATE), it's still only one command for the user and their goal is
simply "update if this exists, insert if it doesn't." I also don't like
the idea that the policy to be applied depends on if it ends up being an
INSERT or an UPDATE. I liked Peter's suggestion that the row must pass
both WITH CHECK conditions- at least that's consistent and
understandable.
As to whether the UPDATE USING policy should cause an error to be
thrown if it is not satisfied, my inclination would be to not error,
and use the command tag to report that no rows were updated, since
that is what would happen with a regular UPDATE.
Yes, for a regular UPDATE that's what would happen- but this isn't a
regular UPDATE, it's an UPSERT. Consider how users handle this now-
they have routines which continually try to do one or the other until
either the INSERT or the UPDATE is successful. I've never seen such a
function, properly written, that says "try to INSERT, if that fails, try
to UPDATE and if that doesn't update anything, then simply exit." What
is the use-case for that?
So overall INSERT .. ON CONFLICT UPDATE would be consistent with
either an INSERT or an UPDATE, depending on whether the row existed
beforehand, which is easier to explain than having some special UPSERT
semantics.
Any semantics which result in a no-op would be surprising for users of
UPSERT. I like the idea of failing earlier- if you try to INSERT .. ON
CONFLICT UPDATE a row which you're not allowed to INSERT, erroring
strikes me as the right result. If you try to INSERT .. ON CONFLICT
UPDATE a row which you're not allowed to UPDATE, I'd also think an error
would be the right answer, even if you don't end up doing an actual
UPDATE- you ran a command asking for an UPDATE to happen under a certain
condition (the row already exists) and the policy states that you're not
allowed to do such an UPDATE.
As for the UPDATE's USING clause, if you're not allowed to view the
records to be updated (which evidently exists because the INSERT
failed), I'd handle that the same way we handle the case that a SELECT
policy might not allow a user to see rows that they can attempt to
INSERT- they'll get an error back in that case too.
In the end, these are edge cases as policies are generally self
consistent and so I think we have some leeway, but I don't think we have
the right to turn an INSERT .. ON CONFLICT UPDATE into a no-op. That
would be like allowing an INSERT to be a no-op.
Thanks,
Stephen
* Robert Haas (robertmhaas@gmail.com) wrote:
On Wed, Jan 7, 2015 at 4:04 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
I think the policies applied should depend on the path taken, so if it
does an INSERT, then only the INSERT CHECK policy should be applied
(after the insert), but if it ends up doing an UPDATE, I would expect
the UPDATE USING policy to be applied (before the update) and the
UPDATE CHECK policy to be applied (after the update). I would not
expect the INSERT CHECK policy to be applied on the UPDATE path.I agree.
I can certainly understand the appeal of this approach, but I don't
think it makes sense. Consider what happens later on down the road,
after the code has been written and deployed using INSERT .. ON CONFLICT
UPDATE where 99% of the time only one path or the other is taken. Then
the other path is taken and suddenly the exact same command and row ends
up returning errors. Additional testing should have been done to check
if that happens, of course, but I really don't like the idea that the
exact same command, with the exact same policies, would succeed or fail,
due to policies, based on the data in the database.
That's not to say that, generally, speaking, the data in the database
shouldn't impact policy failures, but in this case, the policies might
not even reference any other tables in the database.
As to whether the UPDATE USING policy should cause an error to be
thrown if it is not satisfied, my inclination would be to not error,
and use the command tag to report that no rows were updated, since
that is what would happen with a regular UPDATE.My inclination would be to error, but I'd be OK with your proposal.
I'm pretty strongly in favor of erroring. :)
So overall INSERT .. ON CONFLICT UPDATE would be consistent with
either an INSERT or an UPDATE, depending on whether the row existed
beforehand, which is easier to explain than having some special UPSERT
semantics.Yeah. We won't escape the question so easily where triggers are
concerned, but at least for RLS it seems like it should be possible to
avoid confusing, one-off semantics.
Triggers are another question..
One approach that I don't recall seeing (and I'm not convinced that it's
a good idea either, but thought it deserved mention) is the idea of
having an UPSERT-specific set of policies (and perhaps an
UPSERT-specific trigger...?). That gets pretty grotty when you consider
existing systems which have INSERT and UPDATE triggers already, but
UPSERT is a pretty big deal and a big change and for users who are just
starting to use it, requiring that they write triggers and policies
specifically to address that case might be acceptable. That doesn't
address the cases where users have direct SQL access and might be able
to then bypass existing triggers, but we might be able to work out an
answer to that case..
Other databases have this capability and have triggers and at least one
ends up firing both INSERT and UPDATE triggers, with many complaints
from users about how that ends up making the performance suck. Perhaps
we could use that as a fallback but support the explicit single trigger
option too.. Just some thoughts, apologies if it's already been
convered in depth previously.
Thanks!
Stephen
On Wed, Jan 7, 2015 at 3:01 PM, Stephen Frost <sfrost@snowman.net> wrote:
* Robert Haas (robertmhaas@gmail.com) wrote:
On Wed, Jan 7, 2015 at 4:04 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
I think the policies applied should depend on the path taken, so if it
does an INSERT, then only the INSERT CHECK policy should be applied
(after the insert), but if it ends up doing an UPDATE, I would expect
the UPDATE USING policy to be applied (before the update) and the
UPDATE CHECK policy to be applied (after the update). I would not
expect the INSERT CHECK policy to be applied on the UPDATE path.I agree.
I can certainly understand the appeal of this approach, but I don't
think it makes sense. Consider what happens later on down the road,
after the code has been written and deployed using INSERT .. ON CONFLICT
UPDATE where 99% of the time only one path or the other is taken. Then
the other path is taken and suddenly the exact same command and row ends
up returning errors.
I'd say: that's life. If you don't test your policies, they might not work.
--
Robert Haas
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
* Robert Haas (robertmhaas@gmail.com) wrote:
On Wed, Jan 7, 2015 at 3:01 PM, Stephen Frost <sfrost@snowman.net> wrote:
* Robert Haas (robertmhaas@gmail.com) wrote:
On Wed, Jan 7, 2015 at 4:04 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
I think the policies applied should depend on the path taken, so if it
does an INSERT, then only the INSERT CHECK policy should be applied
(after the insert), but if it ends up doing an UPDATE, I would expect
the UPDATE USING policy to be applied (before the update) and the
UPDATE CHECK policy to be applied (after the update). I would not
expect the INSERT CHECK policy to be applied on the UPDATE path.I agree.
I can certainly understand the appeal of this approach, but I don't
think it makes sense. Consider what happens later on down the road,
after the code has been written and deployed using INSERT .. ON CONFLICT
UPDATE where 99% of the time only one path or the other is taken. Then
the other path is taken and suddenly the exact same command and row ends
up returning errors.I'd say: that's life. If you don't test your policies, they might not work.
I agree with that up to a point- this case feels more like a POLA
violation though rather than a run-of-the-mill "you need to test all
that." I'm a bit worried this might lead to cases where users can't use
UPSERT because they don't know which set of policies will end up getting
applied, although the above approach is strictly a subset of the
approach I'm advocating, but at least with the 'throw it all together'
case, you know exactly what you're getting with UPSERT.
Thanks,
Stephen
On Wed, Jan 7, 2015 at 12:01 PM, Stephen Frost <sfrost@snowman.net> wrote:
Other databases have this capability and have triggers and at least one
ends up firing both INSERT and UPDATE triggers, with many complaints
from users about how that ends up making the performance suck. Perhaps
we could use that as a fallback but support the explicit single trigger
option too.. Just some thoughts, apologies if it's already been
convered in depth previously.
I would like to expose whether or not statement-level triggers are
being called in the context of an INSERT ... ON CONFLICT UPDATE, FWIW.
--
Peter Geoghegan
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Jan 7, 2015 at 12:26 PM, Stephen Frost <sfrost@snowman.net> wrote:
I agree with that up to a point- this case feels more like a POLA
violation though rather than a run-of-the-mill "you need to test all
that." I'm a bit worried this might lead to cases where users can't use
UPSERT because they don't know which set of policies will end up getting
applied, although the above approach is strictly a subset of the
approach I'm advocating, but at least with the 'throw it all together'
case, you know exactly what you're getting with UPSERT.
I think that it makes sense to bunch the policies together, because
INSERT ... ON CONFLICT UPDATE is mostly an INSERT - informally
speaking, the "UPDATE part" is just enough to resolve a conflict -
this is ultimately just a new clause of INSERT. Besides, I think that
making the permissions as restrictive as possible is the least
surprising behavior. It's clearly the most secure behavior.
I think that we collectively lean towards actively throwing an error
as the preferred behavior - I think that's good, because the intent of
the user to UPDATE some particular tuple that they're not allowed to
UPDATE is clear and unambiguous. As long as we're doing that, clearly
writing a query that will fail based on the "wrong" path being taken
is wrong-headed. Now, you can construct an UPSERT case that a "bunch
together policies" approach doesn't serve well. I could be mistaken,
but I think that case is likely to be more than a little contrived.
We're now going to be testing the TARGET.* tuple before going to
update, having failed to insert (and after row locking the TARGET.*
tuple) - the target tuple is really how the update finds the existing
row to update, so it should be tested, without necessarily being
blamed directly (certainly, we cannot leak the TARGET.* tuple values,
so maybe we should try blaming the EXCLUDED.* tuple first for error
messaging reasons). If it fails because of the insert check after row
locking but before update, well, it was liable to anyway, when the
insert path was taken. Conversely, if it failed after actually
inserting where that would not happen with a vanilla INSERT (due to a
bunched-together update "USING()" security barrier qual or explicit
user-supplied CHECK OPTION), well, you'd have gotten the same thing if
you took the UPDATE path anyway.
That just leaves the regular ExecUpdate() call of
ExecWithCheckOptions(). When that is called with "bunched together"
policies, you're now testing the final tuple. So it might be a bit
confusing that the policy of final tuples originating from INSERTs is
also enforced here, for the final tuple of an "UPDATE". But in order
for even that to be the case, you'd have to have a final tuple that
was substantially different from the one originally proposed for
insertion that made the difference between a qual passing and not
passing. How likely is that in realistic use cases? And besides, isn't
the policy for INSERTs still quite relevant even here? We're talking
about a final tuple that originated from an INSERT statement, after
all.
--
Peter Geoghegan
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Jan 07, 2015 at 03:01:20PM -0500, Stephen Frost wrote:
* Robert Haas (robertmhaas@gmail.com) wrote:
On Wed, Jan 7, 2015 at 4:04 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
I think the policies applied should depend on the path taken, so if it
does an INSERT, then only the INSERT CHECK policy should be applied
(after the insert), but if it ends up doing an UPDATE, I would expect
the UPDATE USING policy to be applied (before the update) and the
UPDATE CHECK policy to be applied (after the update). I would not
expect the INSERT CHECK policy to be applied on the UPDATE path.I agree.
I can certainly understand the appeal of this approach, but I don't
think it makes sense. Consider what happens later on down the road,
after the code has been written and deployed using INSERT .. ON CONFLICT
UPDATE where 99% of the time only one path or the other is taken. Then
the other path is taken and suddenly the exact same command and row ends
up returning errors. Additional testing should have been done to check
if that happens, of course, but I really don't like the idea that the
exact same command, with the exact same policies, would succeed or fail,
due to policies, based on the data in the database.
There's precedent. Unique constraints, for example.
Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Jan 7, 2015 at 5:16 PM, David Fetter <david@fetter.org> wrote:
There's precedent. Unique constraints, for example.
I don't see that as any kind of precedent.
--
Peter Geoghegan
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Jan 07, 2015 at 05:18:58PM -0800, Peter Geoghegan wrote:
On Wed, Jan 7, 2015 at 5:16 PM, David Fetter <david@fetter.org> wrote:
There's precedent. Unique constraints, for example.
I don't see that as any kind of precedent.
In the part you sliced off, Stephen described a situation where the
contents of a database either do or don't cause a query to violate a
constraint. Uniqueness constraints are one example of this.
CREATE TABLE foo(id INTEGER PRIMARY KEY);
INSERT INTO foo(id) VALUES(1); /* Works the first time */
INSERT INTO foo(id) VALUES(1); /* Fails the next time */
Same database, same constraints, different outcome.
Cheers,
David.
--
David Fetter <david@fetter.org> http://fetter.org/
Phone: +1 415 235 3778 AIM: dfetter666 Yahoo!: dfetter
Skype: davidfetter XMPP: david.fetter@gmail.com
Remember to vote!
Consider donating to Postgres: http://www.postgresql.org/about/donate
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Wed, Jan 7, 2015 at 5:33 PM, David Fetter <david@fetter.org> wrote:
Same database, same constraints, different outcome.
You're missing the point, I think. UPSERT means that the user doesn't
care about whether or not a given tuple proposed for insertion was
handled with an insert or an update. If you have distinct INSERT and
UPDATE policies (not that I think that many people will), and if they
differ in such a way as to make the difference between some actual
INSERT ... ON CONFLICT UPDATE in the app throwing an error or not
throwing an error depending only on whether the UPDATE (or INSERT)
path was taken, then that combination (the combination of that UPSERT
command, that UPDATE policy and that INSERT policy) is wrong-headed.
It's not based on some particular set of data in the database, but any
and all possible sets. Stephen's objection isn't that the update
operation throws an error; it's that the insert doesn't. I'm with
Stephen; ISTM that selectively enforcing RLS like that is a foot gun.
For column level privileges, you wouldn't expect to only get an error
about not having the relevant update permissions at runtime, when the
update path happens to be taken. And so it is for RLS.
--
Peter Geoghegan
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Peter,
* Peter Geoghegan (pg@heroku.com) wrote:
For column level privileges, you wouldn't expect to only get an error
about not having the relevant update permissions at runtime, when the
update path happens to be taken. And so it is for RLS.
Right, that's the precedent we should be considering. Column-level
privileges is a great example- you need both insert and update
privileges for the columns involved for the command to succeed. It
shouldn't depend on which path actually ends up being taken.
Thanks!
Stephen
On 9 January 2015 at 00:49, Stephen Frost <sfrost@snowman.net> wrote:
Peter,
* Peter Geoghegan (pg@heroku.com) wrote:
For column level privileges, you wouldn't expect to only get an error
about not having the relevant update permissions at runtime, when the
update path happens to be taken. And so it is for RLS.Right, that's the precedent we should be considering. Column-level
privileges is a great example- you need both insert and update
privileges for the columns involved for the command to succeed. It
shouldn't depend on which path actually ends up being taken.
It's not the same as column-level privileges though, because RLS
policies depend on the new/existing data, not just the table metadata.
So for example an "UPDATE ... WHERE false" can fail column-level
privilege checks even though it isn't updating anything, but it cannot
fail a RLS policy check.
I was trying to think up an example where you might actually have
different INSERT and UPDATE policies, and the best I can think of is
some sort of mod_count column where you have an INSERT CHECK
(mod_count = 0) and an UPDATE CHECK (mod_count > 0). In that case,
checking both policies would make an UPSERT impossible, whereas if you
think of it as doing either an INSERT or an UPDATE, as the syntax
suggests, it becomes possible.
(I'm assuming here from your description that you intend for the
policy expressions to be AND'ed together, so it might end up being an
AND of 2 OR expressions.)
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Fri, Jan 9, 2015 at 12:19 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
I was trying to think up an example where you might actually have
different INSERT and UPDATE policies, and the best I can think of is
some sort of mod_count column where you have an INSERT CHECK
(mod_count = 0) and an UPDATE CHECK (mod_count > 0). In that case,
checking both policies would make an UPSERT impossible, whereas if you
think of it as doing either an INSERT or an UPDATE, as the syntax
suggests, it becomes possible.
Why does this user want to do this upsert? If they're upserting, then
the inserted row could only reasonably have a value of (mod_count =
0). If updating, then they must have a constant value for the update
path (a value that's greater than 0, naturally - say 2), which doesn't
make any sense in the context of an upsert's auxiliary update - what
happened to the 0 value? Sorry, but I don't think your example makes
sense - I can't see what would motivate anyone to write a query like
that with those RLS policies in place. It sounds like you're talking
about an insert and a separate update that may or may not affect the
same row, and not an upsert. Then those policies make sense, but in
practice they render the upsert you describe contradictory.
FWIW, I'm not suggesting that there couldn't possibly be a use case
that doesn't do well with this convention where we enforce RLS
deepening on the path taken. The cases are just very marginal, as I
think your difficulty in coming up with a convincing counter-argument
shows. I happen to think what Stephen and I favor ("bunching together"
USING() barrier quals and check options from INSERT and UPDATE
policies) is likely to be the best alternative available on balance.
More generally, you could point out that I'm actually testing
different tuples at different points in query processing under that
regime (e.g. the post-insert tuple, or the before-update conflicting,
existing tuple from the target, or the post update tuple) and so
things could fail when the update path is taken despite the fact that
they didn't fail when the insert path was taken. That's technically
true, of course, but with idiomatic usage it isn't true, and that's
what I care about.
Does anyone have another counter-example of a practical upsert
statement that cannot be used with certain RLS policies due to the
fact that we chose to "bunch together" INSERT and UPDATE RLS policies?
--
Peter Geoghegan
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 9 January 2015 at 08:49, Peter Geoghegan <pg@heroku.com> wrote:
On Fri, Jan 9, 2015 at 12:19 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
I was trying to think up an example where you might actually have
different INSERT and UPDATE policies, and the best I can think of is
some sort of mod_count column where you have an INSERT CHECK
(mod_count = 0) and an UPDATE CHECK (mod_count > 0). In that case,
checking both policies would make an UPSERT impossible, whereas if you
think of it as doing either an INSERT or an UPDATE, as the syntax
suggests, it becomes possible.Why does this user want to do this upsert? If they're upserting, then
the inserted row could only reasonably have a value of (mod_count =
0). If updating, then they must have a constant value for the update
path (a value that's greater than 0, naturally - say 2), which doesn't
make any sense in the context of an upsert's auxiliary update - what
happened to the 0 value? Sorry, but I don't think your example makes
sense - I can't see what would motivate anyone to write a query like
that with those RLS policies in place. It sounds like you're talking
about an insert and a separate update that may or may not affect the
same row, and not an upsert. Then those policies make sense, but in
practice they render the upsert you describe contradictory.
Whoa, hang on. I think you're being a bit quick to dismiss that
example. Why shouldn't I want an upsert where the majority of the
table columns follow the usual "make it so" pattern of an upsert, but
there is also this kind of audit column to be maintained? Then I would
write something like
INSERT INTO tbl (<some values>, 0)
ON CONFLICT UPDATE SET <same values>, mod_count=mod_count+1;
The root of the problem is the way that you're proposing to combine
the RLS policies (using AND), which runs contrary to the way RLS
policies are usually combined (using OR), which is why this kind of
example fails -- RLS policies in general aren't intended to all be
true simultaneously.
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Dean, Peter,
* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:
On 9 January 2015 at 08:49, Peter Geoghegan <pg@heroku.com> wrote:
On Fri, Jan 9, 2015 at 12:19 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
I was trying to think up an example where you might actually have
different INSERT and UPDATE policies, and the best I can think of is
some sort of mod_count column where you have an INSERT CHECK
(mod_count = 0) and an UPDATE CHECK (mod_count > 0). In that case,
checking both policies would make an UPSERT impossible, whereas if you
think of it as doing either an INSERT or an UPDATE, as the syntax
suggests, it becomes possible.Why does this user want to do this upsert? If they're upserting, then
the inserted row could only reasonably have a value of (mod_count =
0). If updating, then they must have a constant value for the update
path (a value that's greater than 0, naturally - say 2), which doesn't
make any sense in the context of an upsert's auxiliary update - what
happened to the 0 value? Sorry, but I don't think your example makes
sense - I can't see what would motivate anyone to write a query like
that with those RLS policies in place. It sounds like you're talking
about an insert and a separate update that may or may not affect the
same row, and not an upsert. Then those policies make sense, but in
practice they render the upsert you describe contradictory.Whoa, hang on. I think you're being a bit quick to dismiss that
example. Why shouldn't I want an upsert where the majority of the
table columns follow the usual "make it so" pattern of an upsert, but
there is also this kind of audit column to be maintained? Then I would
write something likeINSERT INTO tbl (<some values>, 0)
ON CONFLICT UPDATE SET <same values>, mod_count=mod_count+1;
That's a good point- it doesn't make sense to compare the INSERT values
against the UPDATE policy as the result of the UPDATE could be a
completely different tuple, one we can't know the contents of until we
actually find a conflicting tuple. We can't simply combine the policies
and run them at the same time, but I'm not sure that then means we
should let an INSERT .. ON CONFLICT UPDATE pass in values to the INSERT
which are not permitted by any INSERT policy on the relation.
Further, forgetting about RLS, what happens if a user doesn't have
INSERT rights on the mod_count column at all? We still allow the above
to execute if the row already exists? That doesn't strike me as a good
idea. In this case, I do think it makes sense to consider RLS and our
regular permissions system as they are both 'OR' cases. I might have
UPDATE rights for a particular column through multiple different roles
for a given relation, but if none of them give me INSERT rights for that
column, then I'd expect to get an error if I try to do an INSERT into
that column.
The root of the problem is the way that you're proposing to combine
the RLS policies (using AND), which runs contrary to the way RLS
policies are usually combined (using OR), which is why this kind of
example fails -- RLS policies in general aren't intended to all be
true simultaneously.
I agree that RLS policies, in general, are not intended to all be true
simultaneously. I don't think it then follows that an INSERT .. ON
CONFLICT UPDATE should be allowed if, for example, there are no INSERT
policies on an RLS-enabled relation, because the call happens to end up
doing an UPDATE.
To try and outline another possible use-case, consider that you have
tuples coming in from two independent sets of users, orange and blue.
All users are allowed to INSERT, but the 'color' column must equal the
user's. For UPDATEs, the blue users can see both orange and blue
records while orange users can only see orange records. Rows resulting
from an UPDATE can be orange or blue for blue users, but must be orange
for orange users.
Further, in this environment, attempts by orange users to insert blue
records are flagged to be audited for review because orange users should
never have access to nor be handling blue data. With the approach
you're outlining, a command like:
INSERT INTO data VALUES (... , 'blue')
ON CONFLICT UPDATE set ... = ...;
run by an orange user wouldn't error if it happened to result in an
UPDATE (presuming, of course, that the UPDATE didn't try to change the
existing color), but from a security perspective, it's clearly an error
for an orange user to be attempting to insert blue data.
I don't see this as impacting the more general case of how RLS policies
are applied. An individual blue user might also be a member of some
orange-related role and it wouldn't make any sense for that user to then
be only able to see orange records (were we to AND RLS policies
together).
Where this leaves me, at least, is feeling like we should always apply
the INSERT WITH CHECK policy, then if there is a conflict, check the
UPDATE USING policy and throw an error if the row isn't visible but
otherwise perform the UPDATE and then check the UPDATE WITH CHECK
policy. I see your point that this runs counter to the 'mod_count'
example use-case and could cause problems for users using RLS with such
a strategy. For my part, I expect users of RLS to expect errors in such
a case rather than allowing it, but it's certainly a judgement call.
The only reasonable way that I can see to support both sides would be to
allow UPSERT to be a top-level policy definition in its own right and
let users specify exactly what is allowed in the UPSERT case (possibly
requiring three different expressions to represent the initial INSERT,
what the UPDATE can see, and what the UPDATE results in..). I tend to
doubt it would be worth it unless we end up supporting UPSERT-specific
triggers and permissions..
Thanks,
Stephen
On Fri, Jan 9, 2015 at 2:22 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Whoa, hang on. I think you're being a bit quick to dismiss that
example. Why shouldn't I want an upsert where the majority of the
table columns follow the usual "make it so" pattern of an upsert, but
there is also this kind of audit column to be maintained? Then I would
write something likeINSERT INTO tbl (<some values>, 0)
ON CONFLICT UPDATE SET <same values>, mod_count=mod_count+1;The root of the problem is the way that you're proposing to combine
the RLS policies (using AND), which runs contrary to the way RLS
policies are usually combined (using OR), which is why this kind of
example fails -- RLS policies in general aren't intended to all be
true simultaneously.
In case I wasn't clear, I'm proposing that we AND together the already
OR'd together UPDATE and INSERT quals.
--
Peter Geoghegan
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Peter,
* Peter Geoghegan (pg@heroku.com) wrote:
On Fri, Jan 9, 2015 at 2:22 AM, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
Whoa, hang on. I think you're being a bit quick to dismiss that
example. Why shouldn't I want an upsert where the majority of the
table columns follow the usual "make it so" pattern of an upsert, but
there is also this kind of audit column to be maintained? Then I would
write something likeINSERT INTO tbl (<some values>, 0)
ON CONFLICT UPDATE SET <same values>, mod_count=mod_count+1;The root of the problem is the way that you're proposing to combine
the RLS policies (using AND), which runs contrary to the way RLS
policies are usually combined (using OR), which is why this kind of
example fails -- RLS policies in general aren't intended to all be
true simultaneously.In case I wasn't clear, I'm proposing that we AND together the already
OR'd together UPDATE and INSERT quals.
I'm not sure how that would work exactly though, since the tuple the
UPDATE results in might be different from what the INSERT has, as Dean
pointed out. The INSERT tuple might even pass the UPDATE policy where
the resulting tuple from the actual UPDATE part doesn't.
Thanks!
Stephen
On Fri, Jan 9, 2015 at 12:26 PM, Stephen Frost <sfrost@snowman.net> wrote:
Where this leaves me, at least, is feeling like we should always apply
the INSERT WITH CHECK policy, then if there is a conflict, check the
UPDATE USING policy and throw an error if the row isn't visible but
otherwise perform the UPDATE and then check the UPDATE WITH CHECK
policy. I see your point that this runs counter to the 'mod_count'
example use-case and could cause problems for users using RLS with such
a strategy. For my part, I expect users of RLS to expect errors in such
a case rather than allowing it, but it's certainly a judgement call.
I mostly agree, but I don't know that I fully agree. Specifically, I
also think we should check the update policy even when we only insert,
on the theory that if we did go to update, the would-be inserted row
would be a proxy for what we'd check then (the existing, target
relation's tuple). What do you think of that?
I certainly agree that the correct behavior here is at least a bit
subjective. We cannot exactly generalize from other areas of the code,
nor can we look for a precedent set by other systems (AFAICT there is
none).
The only reasonable way that I can see to support both sides would be to
allow UPSERT to be a top-level policy definition in its own right and
let users specify exactly what is allowed in the UPSERT case (possibly
requiring three different expressions to represent the initial INSERT,
what the UPDATE can see, and what the UPDATE results in..). I tend to
doubt it would be worth it unless we end up supporting UPSERT-specific
triggers and permissions..
Agreed. That would technically make everyone happy, in that it defers
to the user, but is unlikely to be worth it.
--
Peter Geoghegan
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On Fri, Jan 9, 2015 at 12:53 PM, Stephen Frost <sfrost@snowman.net> wrote:
I'm not sure how that would work exactly though, since the tuple the
UPDATE results in might be different from what the INSERT has, as Dean
pointed out. The INSERT tuple might even pass the UPDATE policy where
the resulting tuple from the actual UPDATE part doesn't.
I'm not suggesting that Dean's example is totally without merit (his
revised explanation made it clearer what he meant). I just think, on
balance, that enforcing all INSERT + UPDATE policies at various points
is the best behavior. Part of the reason for that is that if you ask
people how they think it works, you'll get as many answers as the
number of people you ask. My proposal (which may or may not be the
same as yours, but if not is only slightly more restrictive) is
unlikely to affect many users negatively, and is relatively easy to
explain and reason about. There are workarounds for Dean's case, too.
--
Peter Geoghegan
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
* Peter Geoghegan (pg@heroku.com) wrote:
On Fri, Jan 9, 2015 at 12:26 PM, Stephen Frost <sfrost@snowman.net> wrote:
Where this leaves me, at least, is feeling like we should always apply
the INSERT WITH CHECK policy, then if there is a conflict, check the
UPDATE USING policy and throw an error if the row isn't visible but
otherwise perform the UPDATE and then check the UPDATE WITH CHECK
policy. I see your point that this runs counter to the 'mod_count'
example use-case and could cause problems for users using RLS with such
a strategy. For my part, I expect users of RLS to expect errors in such
a case rather than allowing it, but it's certainly a judgement call.I mostly agree, but I don't know that I fully agree. Specifically, I
also think we should check the update policy even when we only insert,
on the theory that if we did go to update, the would-be inserted row
would be a proxy for what we'd check then (the existing, target
relation's tuple). What do you think of that?
To flip it around a bit, I don't think we can avoid checking the
*resulting* tuple from the UPDATE against the UPDATE policy. Therefore,
I'm not sure that I see the point in checking the INSERT tuple against
the UPDATE policy. I also don't like the idea that a completely
legitimate command (one which allows the to-be-inserted row via the
INSERT policy AND allows the tuple-post-update via the UPDATE policy) to
throw an error because the to-be-inserted row violated the UPDATE
policy. That doesn't make sense to me.
Thanks!
Stephen
On Fri, Jan 9, 2015 at 1:09 PM, Stephen Frost <sfrost@snowman.net> wrote:
To flip it around a bit, I don't think we can avoid checking the
*resulting* tuple from the UPDATE against the UPDATE policy.
We can avoid it - by not updating. What I'm suggesting is that an
enforcement occurs that is more or less equivalent to the enforcement
that occurs when the UPDATE path is taken, without it actually being
taken. I accept that that isn't perfect, but it has its advantages.
Therefore,
I'm not sure that I see the point in checking the INSERT tuple against
the UPDATE policy.
I guess it wouldn't be hard to modify the struct WithCheckOption to
store the cmd (e.g. RowSecurityPolicy-style ACL_*_CHR permissions),
usable only in this context. That way, when ExecWithCheckOptions() is
called from the INSERT, we can tell it to only enforce
INSERT-applicable policy cmds (in particular, not
UPDATE-only-applicable policy cmds that happen to end up associated
with the same ResultRelation). Whereas, at the end of ExecUpdate(),
that final ExecWithCheckOptions() call (call 3 of 3 when the UPDATE
path is taken), INSERT-based policies can still be enforced against
the final tuple (as can USING() quals that would ordinarily not be
checked at that point either). A given ResultRelation's policies
wouldn't necessarily always need to be enforced within
ExecWithCheckOptions(), which is a change. Does that make sense to
you?
Note that I've already taught ExecWithCheckOptions() to not leak the
existing, target tuple when the UPDATE path is taken (the tuple that
can be referenced in the UPDATE using the TARGET.* alias), while still
performing enforcement against it. I can teach it this too.
--
Peter Geoghegan
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
* Peter Geoghegan (pg@heroku.com) wrote:
On Fri, Jan 9, 2015 at 1:09 PM, Stephen Frost <sfrost@snowman.net> wrote:
Therefore,
I'm not sure that I see the point in checking the INSERT tuple against
the UPDATE policy.I guess it wouldn't be hard to modify the struct WithCheckOption to
store the cmd (e.g. RowSecurityPolicy-style ACL_*_CHR permissions),
usable only in this context. That way, when ExecWithCheckOptions() is
called from the INSERT, we can tell it to only enforce
INSERT-applicable policy cmds (in particular, not
UPDATE-only-applicable policy cmds that happen to end up associated
with the same ResultRelation). Whereas, at the end of ExecUpdate(),
that final ExecWithCheckOptions() call (call 3 of 3 when the UPDATE
path is taken), INSERT-based policies can still be enforced against
the final tuple (as can USING() quals that would ordinarily not be
checked at that point either). A given ResultRelation's policies
wouldn't necessarily always need to be enforced within
ExecWithCheckOptions(), which is a change. Does that make sense to
you?
That sounds reasonable on first blush. I'll note that I've not looked
deeply at the UPSERT patch, but if the above means that the INSERT
policy is always checked against the INSERT tuple and the UPDATE policy
is always checked against the tuple-resulting-from-an-UPDATE, and that
tuples which are not visible due to the UPDATE policy throw an error in
that case, then it's working as I'd expect.
This does mean that the UPDATE policy won't be checked when the INSERT
path is used but that seems appropriate to me, as we can't check a
policy against a tuple that never exists (the result of the update
applied to an existing tuple).
Note that I've already taught ExecWithCheckOptions() to not leak the
existing, target tuple when the UPDATE path is taken (the tuple that
can be referenced in the UPDATE using the TARGET.* alias), while still
performing enforcement against it. I can teach it this too.
Ok.
Thanks!
Stephen
On 9 January 2015 at 20:26, Stephen Frost <sfrost@snowman.net> wrote:
Where this leaves me, at least, is feeling like we should always apply
the INSERT WITH CHECK policy, then if there is a conflict, check the
UPDATE USING policy and throw an error if the row isn't visible but
otherwise perform the UPDATE and then check the UPDATE WITH CHECK
policy.
Yeah, I can see the logic in that, but I'm starting to question the
final "then" in the above, i.e., the order in which the checks are
done.
Currently we're applying RLS CHECKs after the INSERT or UPDATE, like
WITH CHECK OPTIONs on views. The SQL spec says that WITH CHECK OPTIONs
on views have to be applied after the INSERT/UPDATE on the base
relation, but we're free to do something different for RLS CHECKs if
that makes more sense. If we want RLS to be more like column-level
privilege checking, then it does make sense to do the checks sooner,
so perhaps we should be checking the RLS policies before the
INSERT/UPDATE, like CHECK constraints.
Doing that, it makes sense to first check INSERT CHECK policies,
potentially throwing an error before any PK conflict is detected, so
an error would result even in the INSERT .. ON CONFLICT IGNORE case.
That way INSERT CHECK policies would always be applied regardless of
the path taken, as you suggest.
I would still say that the RLS UPDATE USING/CHECK policies should only
be applied if the UPDATE path is taken (before the UPDATE is applied),
and they need to be applied to the (old/new) update tuples only. It's
a bit like there is a WHERE <record already exists> clause attached to
the UPDATE. The RLS UPDATE policies should only be applied to the rows
affected by the UPDATE.
I see your point that this runs counter to the 'mod_count'
example use-case and could cause problems for users using RLS with such
a strategy. For my part, I expect users of RLS to expect errors in such
a case rather than allowing it, but it's certainly a judgement call.
Actually I don't think this is a problem for the mod_count example.
This isn't the same as AND'ing together the INSERT and UPDATE quals,
because each set of policies is being applied to a different tuple.
There are 3 tuples in play here:-
1). The insert tuple, which has INSERT CHECK polcies applied to it
2). The existing (old) update tuple, which has UPDATE USING policies
applied to it
3). The (new) update tuple, which has UPDATE CHECK policies applied to it
NOTE: If we do change RLS CHECKs to be executed prior to modifying any
data, that's potentially a change that could be made independently of
the UPSERT patch. We should also probably then stop referring to them
as WITH CHECK OPTIONs in the docs and in error messages because
they're not the same thing, and the code might be neater if they had
their own data-structure rather than overloading WithCheckOption.
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
Dean,
* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:
On 9 January 2015 at 20:26, Stephen Frost <sfrost@snowman.net> wrote:
Where this leaves me, at least, is feeling like we should always apply
the INSERT WITH CHECK policy, then if there is a conflict, check the
UPDATE USING policy and throw an error if the row isn't visible but
otherwise perform the UPDATE and then check the UPDATE WITH CHECK
policy.Yeah, I can see the logic in that, but I'm starting to question the
final "then" in the above, i.e., the order in which the checks are
done.Currently we're applying RLS CHECKs after the INSERT or UPDATE, like
WITH CHECK OPTIONs on views. The SQL spec says that WITH CHECK OPTIONs
on views have to be applied after the INSERT/UPDATE on the base
relation, but we're free to do something different for RLS CHECKs if
that makes more sense. If we want RLS to be more like column-level
privilege checking, then it does make sense to do the checks sooner,
so perhaps we should be checking the RLS policies before the
INSERT/UPDATE, like CHECK constraints.
I've been wondering about the placement of the checks also. I keep
meaning to go back to the SQL spec and review the WITH CHECK requirement
as it feels a bit odd to me, but then again, it is the spec. As you
say, we're not obligated to follow it for RLS in any case.
Doing that, it makes sense to first check INSERT CHECK policies,
potentially throwing an error before any PK conflict is detected, so
an error would result even in the INSERT .. ON CONFLICT IGNORE case.
That way INSERT CHECK policies would always be applied regardless of
the path taken, as you suggest.
Right.
I would still say that the RLS UPDATE USING/CHECK policies should only
be applied if the UPDATE path is taken (before the UPDATE is applied),
and they need to be applied to the (old/new) update tuples only. It's
a bit like there is a WHERE <record already exists> clause attached to
the UPDATE. The RLS UPDATE policies should only be applied to the rows
affected by the UPDATE.
Agreed. I don't see how we could possibly do otherwise. The only tuple
we have at the outset is the to-be-INSERT'd tuple. We won't find the
conflicting tuple until we try and even then that conflicting tuple
might be different from the to-be-INSERT'd tuple, so we can't check that
until we've discovered the conflict exists. Further, we won't have the
tuple to check for the UPDATE's CHECK policy until all of the
expressions (if any) are run on the UPDATE side.
I see your point that this runs counter to the 'mod_count'
example use-case and could cause problems for users using RLS with such
a strategy. For my part, I expect users of RLS to expect errors in such
a case rather than allowing it, but it's certainly a judgement call.Actually I don't think this is a problem for the mod_count example.
This isn't the same as AND'ing together the INSERT and UPDATE quals,
because each set of policies is being applied to a different tuple.
There are 3 tuples in play here:-1). The insert tuple, which has INSERT CHECK polcies applied to it
2). The existing (old) update tuple, which has UPDATE USING policies
applied to it
3). The (new) update tuple, which has UPDATE CHECK policies applied to it
Right.
NOTE: If we do change RLS CHECKs to be executed prior to modifying any
data, that's potentially a change that could be made independently of
the UPSERT patch. We should also probably then stop referring to them
as WITH CHECK OPTIONs in the docs and in error messages because
they're not the same thing, and the code might be neater if they had
their own data-structure rather than overloading WithCheckOption.
I agree that's an independent change. I'm not sure if it would end up
being neater or not offhand but I can see some advantages to breaking it
up from WithCheck as it might be simpler for users to understand
(particularly those users who are not already familiar with WithCheck).
Were you thinking about working up a patch for such a change? If not,
I'll see about finding time to do it, unless someone else wants to
volunteer. :)
Thanks!
Stephen
On 10 January 2015 at 15:12, Stephen Frost <sfrost@snowman.net> wrote:
NOTE: If we do change RLS CHECKs to be executed prior to modifying any
data, that's potentially a change that could be made independently of
the UPSERT patch. We should also probably then stop referring to them
as WITH CHECK OPTIONs in the docs and in error messages because
they're not the same thing, and the code might be neater if they had
their own data-structure rather than overloading WithCheckOption.I agree that's an independent change. I'm not sure if it would end up
being neater or not offhand but I can see some advantages to breaking it
up from WithCheck as it might be simpler for users to understand
(particularly those users who are not already familiar with WithCheck).
A new RlsCheck struct wouldn't need the cascade option that
WithCheckOption has, and it's not nice the way viewname is being
abused. For UPSERT it will need a field indicating the command type,
as Peter pointed out, so I think it's different enough to warrant it's
own struct, even if we weren't changing the firing time.
Were you thinking about working up a patch for such a change?
OK, I'll take a look at it. I started reading the existing code that
expands RLS policies and spotted a couple of bugs that should be fixed
first:-
1). In prepend_row_security_policies(), defaultDeny should be
initialised to false at the top.
2). In fireRIRrules(), activeRIRs isn't being reset properly after
recursing, which will cause a self-join to fail.
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 10 January 2015 at 21:38, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
OK, I'll take a look at it. I started reading the existing code that
expands RLS policies and spotted a couple of bugs that should be fixed
first:-1). In prepend_row_security_policies(), defaultDeny should be
initialised to false at the top.2). In fireRIRrules(), activeRIRs isn't being reset properly after
recursing, which will cause a self-join to fail.
So as I starting looking into these bugs, which looked fairly trivial,
the test case I came up with revealed another more subtle bug with
RLS, using a more obscure query -- given an update of the form UPDATE
t1 ... FROM t2 ..., if t1 is part of an inheritance hierarchy and both
t1 and t2 have RLS enabled, the inheritance planner was doing the
wrong thing. There is code in there to copy subquery RTEs into each
child plan, which needed to do the same for non-target RTEs with
security barrier quals, which haven't yet been turned into subqueries.
Similarly the rowmark preprocessing code needed to be taught about
(non-target) RTEs with security barrier quals to handle this kind of
UPDATE with a FROM clause.
The attached patch fixes those issues.
This bug can't happen with updatable security barrier views, since
non-target security barrier views are expanded in the rewriter, so
technically this doesn't need bach-patching to 9.4. However, I think
the planner changes should probably be back-patched anyway, to keep
the code in the 2 branches in sync, and more maintainable. Also it
feels like the planner ought to be able to handle any legal query
thrown at it, even if it looks like the 9.4 rewriter couldn't generate
such a query.
Regards,
Dean
Attachments:
rls.patchtext/x-diff; charset=US-ASCII; name=rls.patchDownload
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index 9cbbcfb..119a016
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** inheritance_planner(PlannerInfo *root)
*** 790,795 ****
--- 790,796 ----
{
Query *parse = root->parse;
int parentRTindex = parse->resultRelation;
+ Bitmapset *resultRTindexes = NULL;
List *final_rtable = NIL;
int save_rel_array_size = 0;
RelOptInfo **save_rel_array = NULL;
*************** inheritance_planner(PlannerInfo *root)
*** 814,820 ****
--- 815,835 ----
* (1) would result in a rangetable of length O(N^2) for N targets, with
* at least O(N^3) work expended here; and (2) would greatly complicate
* management of the rowMarks list.
+ *
+ * Note that any RTEs with security barrier quals will be turned into
+ * subqueries during planning, and so we must create copies of them too,
+ * except where they are target relations, which will each only be used
+ * in a single plan.
*/
+ resultRTindexes = bms_add_member(resultRTindexes, parentRTindex);
+ foreach(lc, root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(lc);
+ if (appinfo->parent_relid == parentRTindex)
+ resultRTindexes = bms_add_member(resultRTindexes,
+ appinfo->child_relid);
+ }
+
foreach(lc, root->append_rel_list)
{
AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(lc);
*************** inheritance_planner(PlannerInfo *root)
*** 885,905 ****
{
RangeTblEntry *rte = (RangeTblEntry *) lfirst(lr);
! if (rte->rtekind == RTE_SUBQUERY)
{
Index newrti;
/*
* The RTE can't contain any references to its own RT
! * index, so we can save a few cycles by applying
! * ChangeVarNodes before we append the RTE to the
! * rangetable.
*/
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.append_rel_list, rti, newrti, 0);
rte = copyObject(rte);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
}
--- 900,928 ----
{
RangeTblEntry *rte = (RangeTblEntry *) lfirst(lr);
! /*
! * Copy subquery RTEs and RTEs with security barrier quals
! * that will be turned into subqueries, except those that are
! * target relations.
! */
! if (rte->rtekind == RTE_SUBQUERY ||
! (rte->securityQuals != NIL &&
! !bms_is_member(rti, resultRTindexes)))
{
Index newrti;
/*
* The RTE can't contain any references to its own RT
! * index, except in the security barrier quals, so we can
! * save a few cycles by applying ChangeVarNodes before we
! * append the RTE to the rangetable.
*/
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.append_rel_list, rti, newrti, 0);
rte = copyObject(rte);
+ ChangeVarNodes((Node *) rte->securityQuals, rti, newrti, 0);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
}
*************** preprocess_rowmarks(PlannerInfo *root)
*** 2198,2206 ****
* Ignore RowMarkClauses for subqueries; they aren't real tables and
* can't support true locking. Subqueries that got flattened into the
* main query should be ignored completely. Any that didn't will get
! * ROW_MARK_COPY items in the next loop.
*/
! if (rte->rtekind != RTE_RELATION)
continue;
/*
--- 2221,2231 ----
* Ignore RowMarkClauses for subqueries; they aren't real tables and
* can't support true locking. Subqueries that got flattened into the
* main query should be ignored completely. Any that didn't will get
! * ROW_MARK_COPY items in the next loop. Any RTEs with security
! * barrier quals will get turned into subqueries later, and so fall
! * into this same category.
*/
! if (rte->rtekind != RTE_RELATION || rte->securityQuals != NIL)
continue;
/*
*************** preprocess_rowmarks(PlannerInfo *root)
*** 2256,2261 ****
--- 2281,2287 ----
newrc->rowmarkId = ++(root->glob->lastRowMarkId);
/* real tables support REFERENCE, anything else needs COPY */
if (rte->rtekind == RTE_RELATION &&
+ rte->securityQuals == NIL &&
rte->relkind != RELKIND_FOREIGN_TABLE)
newrc->markType = ROW_MARK_REFERENCE;
else
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index b8e6e7a..002f6dd
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** fireRIRrules(Query *parsetree, List *act
*** 1756,1761 ****
--- 1756,1763 ----
expression_tree_walker( (Node*) quals, fireRIRonSubLink,
(void*)activeRIRs);
+
+ activeRIRs = list_delete_first(activeRIRs);
}
}
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 35790a9..4eb8d10
*** a/src/backend/rewrite/rowsecurity.c
--- b/src/backend/rewrite/rowsecurity.c
***************
*** 57,63 ****
static List *pull_row_security_policies(CmdType cmd, Relation relation,
Oid user_id);
! static void process_policies(List *policies, int rt_index,
Expr **final_qual,
Expr **final_with_check_qual,
bool *hassublinks);
--- 57,63 ----
static List *pull_row_security_policies(CmdType cmd, Relation relation,
Oid user_id);
! static void process_policies(Query* root, List *policies, int rt_index,
Expr **final_qual,
Expr **final_with_check_qual,
bool *hassublinks);
*************** prepend_row_security_policies(Query* roo
*** 95,101 ****
Oid user_id;
int sec_context;
int rls_status;
! bool defaultDeny = true;
bool hassublinks = false;
/* This is just to get the security context */
--- 95,101 ----
Oid user_id;
int sec_context;
int rls_status;
! bool defaultDeny = false;
bool hassublinks = false;
/* This is just to get the security context */
*************** prepend_row_security_policies(Query* roo
*** 166,172 ****
defaultDeny = true;
/* Now that we have our policies, build the expressions from them. */
! process_policies(rowsec_policies, rt_index, &rowsec_expr,
&rowsec_with_check_expr, &hassublinks);
/*
--- 166,172 ----
defaultDeny = true;
/* Now that we have our policies, build the expressions from them. */
! process_policies(root, rowsec_policies, rt_index, &rowsec_expr,
&rowsec_with_check_expr, &hassublinks);
/*
*************** prepend_row_security_policies(Query* roo
*** 196,202 ****
hook_policies = (*row_security_policy_hook)(root->commandType, rel);
/* Build the expression from any policies returned. */
! process_policies(hook_policies, rt_index, &hook_expr,
&hook_with_check_expr, &hassublinks);
}
--- 196,202 ----
hook_policies = (*row_security_policy_hook)(root->commandType, rel);
/* Build the expression from any policies returned. */
! process_policies(root, hook_policies, rt_index, &hook_expr,
&hook_with_check_expr, &hassublinks);
}
*************** pull_row_security_policies(CmdType cmd,
*** 383,389 ****
* qual_eval, with_check_eval, and hassublinks are output variables
*/
static void
! process_policies(List *policies, int rt_index, Expr **qual_eval,
Expr **with_check_eval, bool *hassublinks)
{
ListCell *item;
--- 383,389 ----
* qual_eval, with_check_eval, and hassublinks are output variables
*/
static void
! process_policies(Query* root, List *policies, int rt_index, Expr **qual_eval,
Expr **with_check_eval, bool *hassublinks)
{
ListCell *item;
*************** process_policies(List *policies, int rt_
*** 392,398 ****
/*
* Extract the USING and WITH CHECK quals from each of the policies
! * and add them to our lists.
*/
foreach(item, policies)
{
--- 392,399 ----
/*
* Extract the USING and WITH CHECK quals from each of the policies
! * and add them to our lists. We only want WITH CHECK quals if this
! * RTE is the query's result relation.
*/
foreach(item, policies)
{
*************** process_policies(List *policies, int rt_
*** 401,407 ****
if (policy->qual != NULL)
quals = lcons(copyObject(policy->qual), quals);
! if (policy->with_check_qual != NULL)
with_check_quals = lcons(copyObject(policy->with_check_qual),
with_check_quals);
--- 402,409 ----
if (policy->qual != NULL)
quals = lcons(copyObject(policy->qual), quals);
! if (policy->with_check_qual != NULL &&
! rt_index == root->resultRelation)
with_check_quals = lcons(copyObject(policy->with_check_qual),
with_check_quals);
*************** process_policies(List *policies, int rt_
*** 421,427 ****
* If we end up with only USING quals, then use those as
* WITH CHECK quals also.
*/
! if (with_check_quals == NIL)
with_check_quals = copyObject(quals);
/*
--- 423,429 ----
* If we end up with only USING quals, then use those as
* WITH CHECK quals also.
*/
! if (with_check_quals == NIL && rt_index == root->resultRelation)
with_check_quals = copyObject(quals);
/*
*************** process_policies(List *policies, int rt_
*** 450,457 ****
*/
if (list_length(with_check_quals) > 1)
*with_check_eval = makeBoolExpr(OR_EXPR, with_check_quals, -1);
! else
*with_check_eval = (Expr*) linitial(with_check_quals);
return;
}
--- 452,461 ----
*/
if (list_length(with_check_quals) > 1)
*with_check_eval = makeBoolExpr(OR_EXPR, with_check_quals, -1);
! else if (with_check_quals != NIL)
*with_check_eval = (Expr*) linitial(with_check_quals);
+ else
+ *with_check_eval = NULL;
return;
}
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 430da55..494c1e4
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** SELECT * FROM t1 FOR SHARE;
*** 585,604 ****
(5 rows)
EXPLAIN (COSTS OFF) SELECT * FROM t1 FOR SHARE;
! QUERY PLAN
! -------------------------------------------------------
LockRows
-> Subquery Scan on t1
! -> LockRows
! -> Result
! -> Append
! -> Seq Scan on t1 t1_1
! Filter: ((a % 2) = 0)
! -> Seq Scan on t2
! Filter: ((a % 2) = 0)
! -> Seq Scan on t3
! Filter: ((a % 2) = 0)
! (11 rows)
SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
NOTICE: f_leak => bbb
--- 585,602 ----
(5 rows)
EXPLAIN (COSTS OFF) SELECT * FROM t1 FOR SHARE;
! QUERY PLAN
! -------------------------------------------
LockRows
-> Subquery Scan on t1
! -> Append
! -> Seq Scan on t1 t1_1
! Filter: ((a % 2) = 0)
! -> Seq Scan on t2
! Filter: ((a % 2) = 0)
! -> Seq Scan on t3
! Filter: ((a % 2) = 0)
! (9 rows)
SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
NOTICE: f_leak => bbb
*************** NOTICE: f_leak => yyy
*** 616,636 ****
(5 rows)
EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
! QUERY PLAN
! -------------------------------------------------------
LockRows
-> Subquery Scan on t1
Filter: f_leak(t1.b)
! -> LockRows
! -> Result
! -> Append
! -> Seq Scan on t1 t1_1
! Filter: ((a % 2) = 0)
! -> Seq Scan on t2
! Filter: ((a % 2) = 0)
! -> Seq Scan on t3
! Filter: ((a % 2) = 0)
! (12 rows)
-- superuser is allowed to bypass RLS checks
RESET SESSION AUTHORIZATION;
--- 614,632 ----
(5 rows)
EXPLAIN (COSTS OFF) SELECT * FROM t1 WHERE f_leak(b) FOR SHARE;
! QUERY PLAN
! -------------------------------------------
LockRows
-> Subquery Scan on t1
Filter: f_leak(t1.b)
! -> Append
! -> Seq Scan on t1 t1_1
! Filter: ((a % 2) = 0)
! -> Seq Scan on t2
! Filter: ((a % 2) = 0)
! -> Seq Scan on t3
! Filter: ((a % 2) = 0)
! (10 rows)
-- superuser is allowed to bypass RLS checks
RESET SESSION AUTHORIZATION;
*************** NOTICE: f_leak => yyyyyy
*** 1111,1116 ****
--- 1107,1181 ----
302 | 2 | yyyyyy | (2,yyyyyy)
(5 rows)
+ -- update with from clause self join
+ EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+ QUERY PLAN
+ ---------------------------------------------------------------
+ Update on t1 t1_1_3
+ -> Nested Loop
+ Join Filter: (t1_1.b = t1_2.b)
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_1_4
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2
+ Filter: f_leak(t1_2.b)
+ -> Append
+ -> Seq Scan on t1 t1_2_3
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_4
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_5
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Nested Loop
+ Join Filter: (t1_1_1.b = t1_2_1.b)
+ -> Subquery Scan on t1_1_1
+ Filter: f_leak(t1_1_1.b)
+ -> Seq Scan on t2 t1_1_5
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2_1
+ Filter: f_leak(t1_2_1.b)
+ -> Append
+ -> Seq Scan on t1 t1_2_6
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_7
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_8
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Nested Loop
+ Join Filter: (t1_1_2.b = t1_2_2.b)
+ -> Subquery Scan on t1_1_2
+ Filter: f_leak(t1_1_2.b)
+ -> Seq Scan on t3 t1_1_6
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2_2
+ Filter: f_leak(t1_2_2.b)
+ -> Append
+ -> Seq Scan on t1 t1_2_9
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_10
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_11
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ (46 rows)
+
+ UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => defdef
+ NOTICE: f_leak => defdef
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => defdef
+ a | b | a | b | t1_1 | t1_2
+ ---+-------------+---+-------------+-----------------+-----------------
+ 4 | dddddd_updt | 4 | dddddd_updt | (4,dddddd_updt) | (4,dddddd_updt)
+ 4 | defdef | 4 | defdef | (4,defdef) | (4,defdef)
+ (2 rows)
+
RESET SESSION AUTHORIZATION;
SET row_security TO OFF;
SELECT * FROM t1;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index ee28a2c..274a3c4
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
*************** UPDATE only t1 SET b = b WHERE f_leak(b)
*** 423,428 ****
--- 423,437 ----
UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ -- update with from clause self join
+ EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+
+ UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+
RESET SESSION AUTHORIZATION;
SET row_security TO OFF;
SELECT * FROM t1;
Dean,
* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:
On 10 January 2015 at 21:38, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
OK, I'll take a look at it. I started reading the existing code that
expands RLS policies and spotted a couple of bugs that should be fixed
first:-1). In prepend_row_security_policies(), defaultDeny should be
initialised to false at the top.2). In fireRIRrules(), activeRIRs isn't being reset properly after
recursing, which will cause a self-join to fail.So as I starting looking into these bugs, which looked fairly trivial,
the test case I came up with revealed another more subtle bug with
RLS, using a more obscure query -- given an update of the form UPDATE
t1 ... FROM t2 ..., if t1 is part of an inheritance hierarchy and both
t1 and t2 have RLS enabled, the inheritance planner was doing the
wrong thing. There is code in there to copy subquery RTEs into each
child plan, which needed to do the same for non-target RTEs with
security barrier quals, which haven't yet been turned into subqueries.
Similarly the rowmark preprocessing code needed to be taught about
(non-target) RTEs with security barrier quals to handle this kind of
UPDATE with a FROM clause.
Interesting, thanks for the work! I had been suspicious that there was
an issue with the recursion handling.
The attached patch fixes those issues.
Excellent. Will take a look.
This bug can't happen with updatable security barrier views, since
non-target security barrier views are expanded in the rewriter, so
technically this doesn't need bach-patching to 9.4. However, I think
the planner changes should probably be back-patched anyway, to keep
the code in the 2 branches in sync, and more maintainable. Also it
feels like the planner ought to be able to handle any legal query
thrown at it, even if it looks like the 9.4 rewriter couldn't generate
such a query.
I'm not anxious to back-patch if there's no issue in existing branches,
but I understand your point about making sure it can handle legal
queries even if we don't believe current 9.4 can't generate them. We
don't tend to back-patch just to keep things in sync as that could
possibly have unintended side-effects.
Looking at the regression tests a bit though, I notice that this removes
the lower-level LockRows.. There had been much discussion about that
last spring which I believe you were a part of..? I specifically recall
discussing it with Craig, at least.
Thanks!
Stephen
On 12 January 2015 at 14:24, Stephen Frost <sfrost@snowman.net> wrote:
Looking at the regression tests a bit though, I notice that this removes
the lower-level LockRows.. There had been much discussion about that
last spring which I believe you were a part of..? I specifically recall
discussing it with Craig, at least.
Ah, yes you're right. Looking back over that discussion it shouldn't
be removing those lower-level LockRows. I was a bit aggressive with my
change to the rowmark preprocessing -- the first loop applies to
requested locks, like SELECT .. FOR UPDATE, so it shouldn't be messing
with that, presecurity.c handles that fine. It's only the second loop
that needs to be taught about RTEs with security quals that will
become subqueries.
Here's an updated patch, that passes with the original regression test results.
Regards,
Dean
Attachments:
rls.v2.patchtext/x-diff; charset=US-ASCII; name=rls.v2.patchDownload
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index 9cbbcfb..34ddf41
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** inheritance_planner(PlannerInfo *root)
*** 790,795 ****
--- 790,796 ----
{
Query *parse = root->parse;
int parentRTindex = parse->resultRelation;
+ Bitmapset *resultRTindexes = NULL;
List *final_rtable = NIL;
int save_rel_array_size = 0;
RelOptInfo **save_rel_array = NULL;
*************** inheritance_planner(PlannerInfo *root)
*** 814,820 ****
--- 815,835 ----
* (1) would result in a rangetable of length O(N^2) for N targets, with
* at least O(N^3) work expended here; and (2) would greatly complicate
* management of the rowMarks list.
+ *
+ * Note that any RTEs with security barrier quals will be turned into
+ * subqueries during planning, and so we must create copies of them too,
+ * except where they are target relations, which will each only be used
+ * in a single plan.
*/
+ resultRTindexes = bms_add_member(resultRTindexes, parentRTindex);
+ foreach(lc, root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(lc);
+ if (appinfo->parent_relid == parentRTindex)
+ resultRTindexes = bms_add_member(resultRTindexes,
+ appinfo->child_relid);
+ }
+
foreach(lc, root->append_rel_list)
{
AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(lc);
*************** inheritance_planner(PlannerInfo *root)
*** 885,905 ****
{
RangeTblEntry *rte = (RangeTblEntry *) lfirst(lr);
! if (rte->rtekind == RTE_SUBQUERY)
{
Index newrti;
/*
* The RTE can't contain any references to its own RT
! * index, so we can save a few cycles by applying
! * ChangeVarNodes before we append the RTE to the
! * rangetable.
*/
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.append_rel_list, rti, newrti, 0);
rte = copyObject(rte);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
}
--- 900,928 ----
{
RangeTblEntry *rte = (RangeTblEntry *) lfirst(lr);
! /*
! * Copy subquery RTEs and RTEs with security barrier quals
! * that will be turned into subqueries, except those that are
! * target relations.
! */
! if (rte->rtekind == RTE_SUBQUERY ||
! (rte->securityQuals != NIL &&
! !bms_is_member(rti, resultRTindexes)))
{
Index newrti;
/*
* The RTE can't contain any references to its own RT
! * index, except in the security barrier quals, so we can
! * save a few cycles by applying ChangeVarNodes before we
! * append the RTE to the rangetable.
*/
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.append_rel_list, rti, newrti, 0);
rte = copyObject(rte);
+ ChangeVarNodes((Node *) rte->securityQuals, rti, newrti, 0);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
}
*************** preprocess_rowmarks(PlannerInfo *root)
*** 2254,2261 ****
newrc = makeNode(PlanRowMark);
newrc->rti = newrc->prti = i;
newrc->rowmarkId = ++(root->glob->lastRowMarkId);
! /* real tables support REFERENCE, anything else needs COPY */
if (rte->rtekind == RTE_RELATION &&
rte->relkind != RELKIND_FOREIGN_TABLE)
newrc->markType = ROW_MARK_REFERENCE;
else
--- 2277,2289 ----
newrc = makeNode(PlanRowMark);
newrc->rti = newrc->prti = i;
newrc->rowmarkId = ++(root->glob->lastRowMarkId);
! /*
! * Real tables support REFERENCE, anything else needs COPY. Since
! * RTEs with security barrier quals will become subqueries, they also
! * need COPY.
! */
if (rte->rtekind == RTE_RELATION &&
+ rte->securityQuals == NIL &&
rte->relkind != RELKIND_FOREIGN_TABLE)
newrc->markType = ROW_MARK_REFERENCE;
else
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index b8e6e7a..002f6dd
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** fireRIRrules(Query *parsetree, List *act
*** 1756,1761 ****
--- 1756,1763 ----
expression_tree_walker( (Node*) quals, fireRIRonSubLink,
(void*)activeRIRs);
+
+ activeRIRs = list_delete_first(activeRIRs);
}
}
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 35790a9..4eb8d10
*** a/src/backend/rewrite/rowsecurity.c
--- b/src/backend/rewrite/rowsecurity.c
***************
*** 57,63 ****
static List *pull_row_security_policies(CmdType cmd, Relation relation,
Oid user_id);
! static void process_policies(List *policies, int rt_index,
Expr **final_qual,
Expr **final_with_check_qual,
bool *hassublinks);
--- 57,63 ----
static List *pull_row_security_policies(CmdType cmd, Relation relation,
Oid user_id);
! static void process_policies(Query* root, List *policies, int rt_index,
Expr **final_qual,
Expr **final_with_check_qual,
bool *hassublinks);
*************** prepend_row_security_policies(Query* roo
*** 95,101 ****
Oid user_id;
int sec_context;
int rls_status;
! bool defaultDeny = true;
bool hassublinks = false;
/* This is just to get the security context */
--- 95,101 ----
Oid user_id;
int sec_context;
int rls_status;
! bool defaultDeny = false;
bool hassublinks = false;
/* This is just to get the security context */
*************** prepend_row_security_policies(Query* roo
*** 166,172 ****
defaultDeny = true;
/* Now that we have our policies, build the expressions from them. */
! process_policies(rowsec_policies, rt_index, &rowsec_expr,
&rowsec_with_check_expr, &hassublinks);
/*
--- 166,172 ----
defaultDeny = true;
/* Now that we have our policies, build the expressions from them. */
! process_policies(root, rowsec_policies, rt_index, &rowsec_expr,
&rowsec_with_check_expr, &hassublinks);
/*
*************** prepend_row_security_policies(Query* roo
*** 196,202 ****
hook_policies = (*row_security_policy_hook)(root->commandType, rel);
/* Build the expression from any policies returned. */
! process_policies(hook_policies, rt_index, &hook_expr,
&hook_with_check_expr, &hassublinks);
}
--- 196,202 ----
hook_policies = (*row_security_policy_hook)(root->commandType, rel);
/* Build the expression from any policies returned. */
! process_policies(root, hook_policies, rt_index, &hook_expr,
&hook_with_check_expr, &hassublinks);
}
*************** pull_row_security_policies(CmdType cmd,
*** 383,389 ****
* qual_eval, with_check_eval, and hassublinks are output variables
*/
static void
! process_policies(List *policies, int rt_index, Expr **qual_eval,
Expr **with_check_eval, bool *hassublinks)
{
ListCell *item;
--- 383,389 ----
* qual_eval, with_check_eval, and hassublinks are output variables
*/
static void
! process_policies(Query* root, List *policies, int rt_index, Expr **qual_eval,
Expr **with_check_eval, bool *hassublinks)
{
ListCell *item;
*************** process_policies(List *policies, int rt_
*** 392,398 ****
/*
* Extract the USING and WITH CHECK quals from each of the policies
! * and add them to our lists.
*/
foreach(item, policies)
{
--- 392,399 ----
/*
* Extract the USING and WITH CHECK quals from each of the policies
! * and add them to our lists. We only want WITH CHECK quals if this
! * RTE is the query's result relation.
*/
foreach(item, policies)
{
*************** process_policies(List *policies, int rt_
*** 401,407 ****
if (policy->qual != NULL)
quals = lcons(copyObject(policy->qual), quals);
! if (policy->with_check_qual != NULL)
with_check_quals = lcons(copyObject(policy->with_check_qual),
with_check_quals);
--- 402,409 ----
if (policy->qual != NULL)
quals = lcons(copyObject(policy->qual), quals);
! if (policy->with_check_qual != NULL &&
! rt_index == root->resultRelation)
with_check_quals = lcons(copyObject(policy->with_check_qual),
with_check_quals);
*************** process_policies(List *policies, int rt_
*** 421,427 ****
* If we end up with only USING quals, then use those as
* WITH CHECK quals also.
*/
! if (with_check_quals == NIL)
with_check_quals = copyObject(quals);
/*
--- 423,429 ----
* If we end up with only USING quals, then use those as
* WITH CHECK quals also.
*/
! if (with_check_quals == NIL && rt_index == root->resultRelation)
with_check_quals = copyObject(quals);
/*
*************** process_policies(List *policies, int rt_
*** 450,457 ****
*/
if (list_length(with_check_quals) > 1)
*with_check_eval = makeBoolExpr(OR_EXPR, with_check_quals, -1);
! else
*with_check_eval = (Expr*) linitial(with_check_quals);
return;
}
--- 452,461 ----
*/
if (list_length(with_check_quals) > 1)
*with_check_eval = makeBoolExpr(OR_EXPR, with_check_quals, -1);
! else if (with_check_quals != NIL)
*with_check_eval = (Expr*) linitial(with_check_quals);
+ else
+ *with_check_eval = NULL;
return;
}
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 430da55..19287fd
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** NOTICE: f_leak => yyyyyy
*** 1111,1116 ****
--- 1111,1185 ----
302 | 2 | yyyyyy | (2,yyyyyy)
(5 rows)
+ -- update with from clause self join
+ EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+ QUERY PLAN
+ ---------------------------------------------------------------
+ Update on t1 t1_1_3
+ -> Nested Loop
+ Join Filter: (t1_1.b = t1_2.b)
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_1_4
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2
+ Filter: f_leak(t1_2.b)
+ -> Append
+ -> Seq Scan on t1 t1_2_3
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_4
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_5
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Nested Loop
+ Join Filter: (t1_1_1.b = t1_2_1.b)
+ -> Subquery Scan on t1_1_1
+ Filter: f_leak(t1_1_1.b)
+ -> Seq Scan on t2 t1_1_5
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2_1
+ Filter: f_leak(t1_2_1.b)
+ -> Append
+ -> Seq Scan on t1 t1_2_6
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_7
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_8
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Nested Loop
+ Join Filter: (t1_1_2.b = t1_2_2.b)
+ -> Subquery Scan on t1_1_2
+ Filter: f_leak(t1_1_2.b)
+ -> Seq Scan on t3 t1_1_6
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2_2
+ Filter: f_leak(t1_2_2.b)
+ -> Append
+ -> Seq Scan on t1 t1_2_9
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_10
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_11
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ (46 rows)
+
+ UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => defdef
+ NOTICE: f_leak => defdef
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => defdef
+ a | b | a | b | t1_1 | t1_2
+ ---+-------------+---+-------------+-----------------+-----------------
+ 4 | dddddd_updt | 4 | dddddd_updt | (4,dddddd_updt) | (4,dddddd_updt)
+ 4 | defdef | 4 | defdef | (4,defdef) | (4,defdef)
+ (2 rows)
+
RESET SESSION AUTHORIZATION;
SET row_security TO OFF;
SELECT * FROM t1;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index ee28a2c..274a3c4
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
*************** UPDATE only t1 SET b = b WHERE f_leak(b)
*** 423,428 ****
--- 423,437 ----
UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ -- update with from clause self join
+ EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+
+ UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+
RESET SESSION AUTHORIZATION;
SET row_security TO OFF;
SELECT * FROM t1;
On 12 January 2015 at 14:24, Stephen Frost <sfrost@snowman.net> wrote:
Interesting, thanks for the work! I had been suspicious that there was
an issue with the recursion handling.
So continuing to review the RLS code, I spotted the following in
prepend_row_security_policies():
/*
* We may end up getting called multiple times for the same RTE, so check
* to make sure we aren't doing double-work.
*/
if (rte->securityQuals != NIL)
return false;
which looked suspicious. I assume that's just a hang-up from an
earlier attempt to prevent infinite recursion in RLS expansion, but
actually it's wrong because in the case of an UPDATE to a security
barrier view on top of a table with RLS enabled, the view's
securityQuals will get added to the RTE first, and that shouldn't
prevent the underlying table's RLS securityQuals from being added.
AFAICT, it should be safe to just remove the above code. I can't see
how prepend_row_security_policies() could end up getting called more
than once for the same RTE.
Also, I'm thinking that it would be better to refactor things a bit
and have prepend_row_security_policies() just return the new
securityQuals and withCheckOptions to add. Then fireRIRrules() would
only have to recurse into the new quals being added, not the
already-processed quals.
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 14 January 2015 at 08:43, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
On 12 January 2015 at 14:24, Stephen Frost <sfrost@snowman.net> wrote:
Interesting, thanks for the work! I had been suspicious that there was
an issue with the recursion handling.So continuing to review the RLS code, I spotted the following in
prepend_row_security_policies():/*
* We may end up getting called multiple times for the same RTE, so check
* to make sure we aren't doing double-work.
*/
if (rte->securityQuals != NIL)
return false;which looked suspicious. I assume that's just a hang-up from an
earlier attempt to prevent infinite recursion in RLS expansion, but
actually it's wrong because in the case of an UPDATE to a security
barrier view on top of a table with RLS enabled, the view's
securityQuals will get added to the RTE first, and that shouldn't
prevent the underlying table's RLS securityQuals from being added.AFAICT, it should be safe to just remove the above code. I can't see
how prepend_row_security_policies() could end up getting called more
than once for the same RTE.
Turns out it wasn't as simple as that. prepend_row_security_policies()
really could get called multiple times for the same RTE, because the
call to query_tree_walker() at the end of fireRIRrules() would descend
into the just-added quals again. The simplest fix seems to be to
process RLS in a separate loop at the end, so that it can have it's
own infinite recursion detection, which is different from that needed
for pre-existing security quals and with check options from security
barrier views. This approach simplifies things a bit, and ensures that
we only try to expand RLS once for each RTE.
Also, I'm thinking that it would be better to refactor things a bit
and have prepend_row_security_policies() just return the new
securityQuals and withCheckOptions to add. Then fireRIRrules() would
only have to recurse into the new quals being added, not the
already-processed quals.
Turns out that refactoring actually became necessary in order to fix
this bug, but I think it makes things cleaner and more efficient.
Here's an updated patch with a new test for this bug. I've been
developing the fixes for these RLS issues as one big patch, but I
suppose it would be easy to split up, if that's preferred.
Regards,
Dean
Attachments:
rls.v3.patchtext/x-diff; charset=US-ASCII; name=rls.v3.patchDownload
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index 9cbbcfb..34ddf41
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** inheritance_planner(PlannerInfo *root)
*** 790,795 ****
--- 790,796 ----
{
Query *parse = root->parse;
int parentRTindex = parse->resultRelation;
+ Bitmapset *resultRTindexes = NULL;
List *final_rtable = NIL;
int save_rel_array_size = 0;
RelOptInfo **save_rel_array = NULL;
*************** inheritance_planner(PlannerInfo *root)
*** 814,820 ****
--- 815,835 ----
* (1) would result in a rangetable of length O(N^2) for N targets, with
* at least O(N^3) work expended here; and (2) would greatly complicate
* management of the rowMarks list.
+ *
+ * Note that any RTEs with security barrier quals will be turned into
+ * subqueries during planning, and so we must create copies of them too,
+ * except where they are target relations, which will each only be used
+ * in a single plan.
*/
+ resultRTindexes = bms_add_member(resultRTindexes, parentRTindex);
+ foreach(lc, root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(lc);
+ if (appinfo->parent_relid == parentRTindex)
+ resultRTindexes = bms_add_member(resultRTindexes,
+ appinfo->child_relid);
+ }
+
foreach(lc, root->append_rel_list)
{
AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(lc);
*************** inheritance_planner(PlannerInfo *root)
*** 885,905 ****
{
RangeTblEntry *rte = (RangeTblEntry *) lfirst(lr);
! if (rte->rtekind == RTE_SUBQUERY)
{
Index newrti;
/*
* The RTE can't contain any references to its own RT
! * index, so we can save a few cycles by applying
! * ChangeVarNodes before we append the RTE to the
! * rangetable.
*/
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.append_rel_list, rti, newrti, 0);
rte = copyObject(rte);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
}
--- 900,928 ----
{
RangeTblEntry *rte = (RangeTblEntry *) lfirst(lr);
! /*
! * Copy subquery RTEs and RTEs with security barrier quals
! * that will be turned into subqueries, except those that are
! * target relations.
! */
! if (rte->rtekind == RTE_SUBQUERY ||
! (rte->securityQuals != NIL &&
! !bms_is_member(rti, resultRTindexes)))
{
Index newrti;
/*
* The RTE can't contain any references to its own RT
! * index, except in the security barrier quals, so we can
! * save a few cycles by applying ChangeVarNodes before we
! * append the RTE to the rangetable.
*/
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.append_rel_list, rti, newrti, 0);
rte = copyObject(rte);
+ ChangeVarNodes((Node *) rte->securityQuals, rti, newrti, 0);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
}
*************** preprocess_rowmarks(PlannerInfo *root)
*** 2254,2261 ****
newrc = makeNode(PlanRowMark);
newrc->rti = newrc->prti = i;
newrc->rowmarkId = ++(root->glob->lastRowMarkId);
! /* real tables support REFERENCE, anything else needs COPY */
if (rte->rtekind == RTE_RELATION &&
rte->relkind != RELKIND_FOREIGN_TABLE)
newrc->markType = ROW_MARK_REFERENCE;
else
--- 2277,2289 ----
newrc = makeNode(PlanRowMark);
newrc->rti = newrc->prti = i;
newrc->rowmarkId = ++(root->glob->lastRowMarkId);
! /*
! * Real tables support REFERENCE, anything else needs COPY. Since
! * RTEs with security barrier quals will become subqueries, they also
! * need COPY.
! */
if (rte->rtekind == RTE_RELATION &&
+ rte->securityQuals == NIL &&
rte->relkind != RELKIND_FOREIGN_TABLE)
newrc->markType = ROW_MARK_REFERENCE;
else
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index b8e6e7a..149191e
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** fireRIRrules(Query *parsetree, List *act
*** 1714,1764 ****
activeRIRs = list_delete_first(activeRIRs);
}
}
- /*
- * If the RTE has row security quals, apply them and recurse into the
- * securityQuals.
- */
- if (prepend_row_security_policies(parsetree, rte, rt_index))
- {
- /*
- * We applied security quals, check for infinite recursion and
- * then expand any nested queries.
- */
- if (list_member_oid(activeRIRs, RelationGetRelid(rel)))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("infinite recursion detected in policy for relation \"%s\"",
- RelationGetRelationName(rel))));
-
- /*
- * Make sure we check for recursion in either securityQuals or
- * WITH CHECK quals.
- */
- if (rte->securityQuals != NIL)
- {
- activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs);
-
- expression_tree_walker( (Node*) rte->securityQuals,
- fireRIRonSubLink, (void*)activeRIRs );
-
- activeRIRs = list_delete_first(activeRIRs);
- }
-
- if (parsetree->withCheckOptions != NIL)
- {
- WithCheckOption *wco;
- List *quals = NIL;
-
- wco = (WithCheckOption *) makeNode(WithCheckOption);
- quals = lcons(wco->qual, quals);
-
- activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs);
-
- expression_tree_walker( (Node*) quals, fireRIRonSubLink,
- (void*)activeRIRs);
- }
-
- }
heap_close(rel, NoLock);
}
--- 1714,1719 ----
*************** fireRIRrules(Query *parsetree, List *act
*** 1780,1785 ****
--- 1735,1822 ----
query_tree_walker(parsetree, fireRIRonSubLink, (void *) activeRIRs,
QTW_IGNORE_RC_SUBQUERIES);
+ /*
+ * Apply any row level security policies. We do this last because it
+ * requires special recursion detection if the new quals have sublink
+ * subqueries, and if we did it in the loop above query_tree_walker
+ * would then recurse into those quals a second time.
+ */
+ rt_index = 0;
+ foreach(lc, parsetree->rtable)
+ {
+ RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
+ Relation rel;
+ List *securityQuals;
+ List *withCheckOptions;
+ bool hasRowSecurity;
+ bool hasSubLinks;
+
+ ++rt_index;
+
+ /* Only normal relations can have RLS policies */
+ if (rte->rtekind != RTE_RELATION ||
+ rte->relkind != RELKIND_RELATION)
+ continue;
+
+ rel = heap_open(rte->relid, NoLock);
+
+ /*
+ * Fetch any new security quals that must be applied to this RTE.
+ */
+ get_row_security_policies(parsetree, rte, rt_index,
+ &securityQuals, &withCheckOptions,
+ &hasRowSecurity, &hasSubLinks);
+
+ if (securityQuals != NIL || withCheckOptions != NIL)
+ {
+ if (hasSubLinks)
+ {
+ /*
+ * Recursively process the new quals, checking for infinite
+ * recursion.
+ */
+ if (list_member_oid(activeRIRs, RelationGetRelid(rel)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected in policy for relation \"%s\"",
+ RelationGetRelationName(rel))));
+
+ activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs);
+
+ expression_tree_walker( (Node*) securityQuals,
+ fireRIRonSubLink, (void*)activeRIRs );
+
+ expression_tree_walker( (Node*) withCheckOptions,
+ fireRIRonSubLink, (void*)activeRIRs );
+
+ activeRIRs = list_delete_first(activeRIRs);
+ }
+
+ /*
+ * Add the new security quals to the start of the RTE's list so
+ * that they get applied before any existing security quals (which
+ * might have come from a user-written security barrier view, and
+ * might contain malicious code).
+ */
+ rte->securityQuals = list_concat(securityQuals,
+ rte->securityQuals);
+
+ parsetree->withCheckOptions = list_concat(withCheckOptions,
+ parsetree->withCheckOptions);
+ }
+
+ /*
+ * Make sure the query is marked correctly if row level security
+ * applies, or if the new quals had sublinks.
+ */
+ if (hasRowSecurity)
+ parsetree->hasRowSecurity = true;
+ if (hasSubLinks)
+ parsetree->hasSubLinks = true;
+
+ heap_close(rel, NoLock);
+ }
+
return parsetree;
}
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 35790a9..bb47429
*** a/src/backend/rewrite/rowsecurity.c
--- b/src/backend/rewrite/rowsecurity.c
***************
*** 57,63 ****
static List *pull_row_security_policies(CmdType cmd, Relation relation,
Oid user_id);
! static void process_policies(List *policies, int rt_index,
Expr **final_qual,
Expr **final_with_check_qual,
bool *hassublinks);
--- 57,63 ----
static List *pull_row_security_policies(CmdType cmd, Relation relation,
Oid user_id);
! static void process_policies(Query* root, List *policies, int rt_index,
Expr **final_qual,
Expr **final_with_check_qual,
bool *hassublinks);
*************** static bool check_role_for_policy(ArrayT
*** 72,87 ****
row_security_policy_hook_type row_security_policy_hook = NULL;
/*
! * Check the given RTE to see whether it's already had row security quals
! * expanded and, if not, prepend any row security rules from built-in or
! * plug-in sources to the securityQuals. The security quals are rewritten (for
! * view expansion, etc) before being added to the RTE.
*
! * Returns true if any quals were added. Note that quals may have been found
! * but not added if user rights make the user exempt from row security.
*/
! bool
! prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
{
Expr *rowsec_expr = NULL;
Expr *rowsec_with_check_expr = NULL;
--- 72,88 ----
row_security_policy_hook_type row_security_policy_hook = NULL;
/*
! * Get any row security quals and check quals that should be applied to the
! * specified RTE.
*
! * In addition hasRowSecurity is set to true if row level security is enabled
! * (even if this RTE doesn't have any row security quals), and hasSubLinks is
! * set to true if any of the quals returned contain sublinks.
*/
! void
! get_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index,
! List **securityQuals, List **withCheckOptions,
! bool *hasRowSecurity, bool *hasSubLinks)
{
Expr *rowsec_expr = NULL;
Expr *rowsec_with_check_expr = NULL;
*************** prepend_row_security_policies(Query* roo
*** 95,102 ****
Oid user_id;
int sec_context;
int rls_status;
! bool defaultDeny = true;
! bool hassublinks = false;
/* This is just to get the security context */
GetUserIdAndSecContext(&user_id, &sec_context);
--- 96,108 ----
Oid user_id;
int sec_context;
int rls_status;
! bool defaultDeny = false;
!
! /* Defaults for the return values */
! *securityQuals = NIL;
! *withCheckOptions = NIL;
! *hasRowSecurity = false;
! *hasSubLinks = false;
/* This is just to get the security context */
GetUserIdAndSecContext(&user_id, &sec_context);
*************** prepend_row_security_policies(Query* roo
*** 112,125 ****
if (rte->relid < FirstNormalObjectId
|| rte->relkind != RELKIND_RELATION
|| (sec_context & SECURITY_ROW_LEVEL_DISABLED))
! return false;
/* Determine the state of RLS for this, pass checkAsUser explicitly */
rls_status = check_enable_rls(rte->relid, rte->checkAsUser);
/* If there is no RLS on this table at all, nothing to do */
if (rls_status == RLS_NONE)
! return false;
/*
* RLS_NONE_ENV means we are not doing any RLS now, but that may change
--- 118,131 ----
if (rte->relid < FirstNormalObjectId
|| rte->relkind != RELKIND_RELATION
|| (sec_context & SECURITY_ROW_LEVEL_DISABLED))
! return;
/* Determine the state of RLS for this, pass checkAsUser explicitly */
rls_status = check_enable_rls(rte->relid, rte->checkAsUser);
/* If there is no RLS on this table at all, nothing to do */
if (rls_status == RLS_NONE)
! return;
/*
* RLS_NONE_ENV means we are not doing any RLS now, but that may change
*************** prepend_row_security_policies(Query* roo
*** 133,150 ****
* be replanned if the environment changes (GUCs, role), but we
* are not adding anything here.
*/
! root->hasRowSecurity = true;
! return false;
}
- /*
- * We may end up getting called multiple times for the same RTE, so check
- * to make sure we aren't doing double-work.
- */
- if (rte->securityQuals != NIL)
- return false;
-
/* Grab the built-in policies which should be applied to this relation. */
rel = heap_open(rte->relid, NoLock);
--- 139,149 ----
* be replanned if the environment changes (GUCs, role), but we
* are not adding anything here.
*/
! *hasRowSecurity = true;
! return;
}
/* Grab the built-in policies which should be applied to this relation. */
rel = heap_open(rte->relid, NoLock);
*************** prepend_row_security_policies(Query* roo
*** 166,173 ****
defaultDeny = true;
/* Now that we have our policies, build the expressions from them. */
! process_policies(rowsec_policies, rt_index, &rowsec_expr,
! &rowsec_with_check_expr, &hassublinks);
/*
* Also, allow extensions to add their own policies.
--- 165,172 ----
defaultDeny = true;
/* Now that we have our policies, build the expressions from them. */
! process_policies(root, rowsec_policies, rt_index, &rowsec_expr,
! &rowsec_with_check_expr, hasSubLinks);
/*
* Also, allow extensions to add their own policies.
*************** prepend_row_security_policies(Query* roo
*** 190,203 ****
* enabled on the table, then we will ignore the internally-generated
* default-deny policy and use only the policies returned by the
* extension.
*/
if (row_security_policy_hook)
{
hook_policies = (*row_security_policy_hook)(root->commandType, rel);
/* Build the expression from any policies returned. */
! process_policies(hook_policies, rt_index, &hook_expr,
! &hook_with_check_expr, &hassublinks);
}
/*
--- 189,205 ----
* enabled on the table, then we will ignore the internally-generated
* default-deny policy and use only the policies returned by the
* extension.
+ *
+ * Any extension policies are applied after any internally-generated
+ * policies, so that extensions cannot bypass built-in RLS.
*/
if (row_security_policy_hook)
{
hook_policies = (*row_security_policy_hook)(root->commandType, rel);
/* Build the expression from any policies returned. */
! process_policies(root, hook_policies, rt_index, &hook_expr,
! &hook_with_check_expr, hasSubLinks);
}
/*
*************** prepend_row_security_policies(Query* roo
*** 229,235 ****
wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) rowsec_with_check_expr;
wco->cascaded = false;
! root->withCheckOptions = lcons(wco, root->withCheckOptions);
}
/*
--- 231,237 ----
wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) rowsec_with_check_expr;
wco->cascaded = false;
! *withCheckOptions = lappend(*withCheckOptions, wco);
}
/*
*************** prepend_row_security_policies(Query* roo
*** 243,249 ****
wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) hook_with_check_expr;
wco->cascaded = false;
! root->withCheckOptions = lcons(wco, root->withCheckOptions);
}
}
--- 245,251 ----
wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) hook_with_check_expr;
wco->cascaded = false;
! *withCheckOptions = lappend(*withCheckOptions, wco);
}
}
*************** prepend_row_security_policies(Query* roo
*** 253,283 ****
|| root->commandType == CMD_DELETE)
{
if (rowsec_expr)
! rte->securityQuals = lcons(rowsec_expr, rte->securityQuals);
if (hook_expr)
! rte->securityQuals = lcons(hook_expr,
! rte->securityQuals);
}
heap_close(rel, NoLock);
/*
! * Mark this query as having row security, so plancache can invalidate
! * it when necessary (eg: role changes)
! */
! root->hasRowSecurity = true;
!
! /*
! * If we have sublinks added because of the policies being added to the
! * query, then set hasSubLinks on the Query to force subLinks to be
! * properly expanded.
*/
! if (hassublinks)
! root->hasSubLinks = hassublinks;
!
! /* If we got this far, we must have added quals */
! return true;
}
/*
--- 255,273 ----
|| root->commandType == CMD_DELETE)
{
if (rowsec_expr)
! *securityQuals = lappend(*securityQuals, rowsec_expr);
if (hook_expr)
! *securityQuals = lappend(*securityQuals, hook_expr);
}
heap_close(rel, NoLock);
/*
! * The query has row security, so plancache must invalidate it when
! * necessary (eg: role changes)
*/
! *hasRowSecurity = true;
}
/*
*************** pull_row_security_policies(CmdType cmd,
*** 383,389 ****
* qual_eval, with_check_eval, and hassublinks are output variables
*/
static void
! process_policies(List *policies, int rt_index, Expr **qual_eval,
Expr **with_check_eval, bool *hassublinks)
{
ListCell *item;
--- 373,379 ----
* qual_eval, with_check_eval, and hassublinks are output variables
*/
static void
! process_policies(Query* root, List *policies, int rt_index, Expr **qual_eval,
Expr **with_check_eval, bool *hassublinks)
{
ListCell *item;
*************** process_policies(List *policies, int rt_
*** 392,398 ****
/*
* Extract the USING and WITH CHECK quals from each of the policies
! * and add them to our lists.
*/
foreach(item, policies)
{
--- 382,389 ----
/*
* Extract the USING and WITH CHECK quals from each of the policies
! * and add them to our lists. We only want WITH CHECK quals if this
! * RTE is the query's result relation.
*/
foreach(item, policies)
{
*************** process_policies(List *policies, int rt_
*** 401,407 ****
if (policy->qual != NULL)
quals = lcons(copyObject(policy->qual), quals);
! if (policy->with_check_qual != NULL)
with_check_quals = lcons(copyObject(policy->with_check_qual),
with_check_quals);
--- 392,399 ----
if (policy->qual != NULL)
quals = lcons(copyObject(policy->qual), quals);
! if (policy->with_check_qual != NULL &&
! rt_index == root->resultRelation)
with_check_quals = lcons(copyObject(policy->with_check_qual),
with_check_quals);
*************** process_policies(List *policies, int rt_
*** 421,427 ****
* If we end up with only USING quals, then use those as
* WITH CHECK quals also.
*/
! if (with_check_quals == NIL)
with_check_quals = copyObject(quals);
/*
--- 413,419 ----
* If we end up with only USING quals, then use those as
* WITH CHECK quals also.
*/
! if (with_check_quals == NIL && rt_index == root->resultRelation)
with_check_quals = copyObject(quals);
/*
*************** process_policies(List *policies, int rt_
*** 450,457 ****
*/
if (list_length(with_check_quals) > 1)
*with_check_eval = makeBoolExpr(OR_EXPR, with_check_quals, -1);
! else
*with_check_eval = (Expr*) linitial(with_check_quals);
return;
}
--- 442,451 ----
*/
if (list_length(with_check_quals) > 1)
*with_check_eval = makeBoolExpr(OR_EXPR, with_check_quals, -1);
! else if (with_check_quals != NIL)
*with_check_eval = (Expr*) linitial(with_check_quals);
+ else
+ *with_check_eval = NULL;
return;
}
diff --git a/src/include/rewrite/rowsecurity.h b/src/include/rewrite/rowsecurity.h
new file mode 100644
index aa1b45b..43f528d
*** a/src/include/rewrite/rowsecurity.h
--- b/src/include/rewrite/rowsecurity.h
*************** typedef List *(*row_security_policy_hook
*** 73,80 ****
extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
! extern bool prepend_row_security_policies(Query* root, RangeTblEntry* rte,
! int rt_index);
extern int check_enable_rls(Oid relid, Oid checkAsUser);
--- 73,81 ----
extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
! extern void get_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index,
! List **securityQuals, List **withCheckOptions,
! bool *hasRowSecurity, bool *hasSubLinks);
extern int check_enable_rls(Oid relid, Oid checkAsUser);
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 430da55..81f4095
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** NOTICE: f_leak => yyyyyy
*** 1111,1116 ****
--- 1111,1185 ----
302 | 2 | yyyyyy | (2,yyyyyy)
(5 rows)
+ -- update with from clause self join
+ EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+ QUERY PLAN
+ ---------------------------------------------------------------
+ Update on t1 t1_1_3
+ -> Nested Loop
+ Join Filter: (t1_1.b = t1_2.b)
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> Seq Scan on t1 t1_1_4
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2
+ Filter: f_leak(t1_2.b)
+ -> Append
+ -> Seq Scan on t1 t1_2_3
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_4
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_5
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Nested Loop
+ Join Filter: (t1_1_1.b = t1_2_1.b)
+ -> Subquery Scan on t1_1_1
+ Filter: f_leak(t1_1_1.b)
+ -> Seq Scan on t2 t1_1_5
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2_1
+ Filter: f_leak(t1_2_1.b)
+ -> Append
+ -> Seq Scan on t1 t1_2_6
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_7
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_8
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Nested Loop
+ Join Filter: (t1_1_2.b = t1_2_2.b)
+ -> Subquery Scan on t1_1_2
+ Filter: f_leak(t1_1_2.b)
+ -> Seq Scan on t3 t1_1_6
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2_2
+ Filter: f_leak(t1_2_2.b)
+ -> Append
+ -> Seq Scan on t1 t1_2_9
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_10
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_11
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ (46 rows)
+
+ UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => defdef
+ NOTICE: f_leak => defdef
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => defdef
+ a | b | a | b | t1_1 | t1_2
+ ---+-------------+---+-------------+-----------------+-----------------
+ 4 | dddddd_updt | 4 | dddddd_updt | (4,dddddd_updt) | (4,dddddd_updt)
+ 4 | defdef | 4 | defdef | (4,defdef) | (4,defdef)
+ (2 rows)
+
RESET SESSION AUTHORIZATION;
SET row_security TO OFF;
SELECT * FROM t1;
*************** NOTICE: f_leak => yyyyyy
*** 1180,1185 ****
--- 1249,1349 ----
(3 rows)
--
+ -- S.b. view on top of Row-level security
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ CREATE TABLE b1 (a int, b text);
+ INSERT INTO b1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+ CREATE POLICY p1 ON b1 USING (a % 2 = 0);
+ ALTER TABLE b1 ENABLE ROW LEVEL SECURITY;
+ GRANT ALL ON b1 TO rls_regress_user1;
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ CREATE VIEW bv1 WITH (security_barrier) AS SELECT * FROM b1 WHERE a > 0 WITH CHECK OPTION;
+ GRANT ALL ON bv1 TO rls_regress_user2;
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ EXPLAIN (COSTS OFF) SELECT * FROM bv1 WHERE f_leak(b);
+ QUERY PLAN
+ ---------------------------------------------
+ Subquery Scan on bv1
+ Filter: f_leak(bv1.b)
+ -> Seq Scan on b1
+ Filter: ((a > 0) AND ((a % 2) = 0))
+ (4 rows)
+
+ SELECT * FROM bv1 WHERE f_leak(b);
+ NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c
+ NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ NOTICE: f_leak => c9f0f895fb98ab9159f51fd0297e236d
+ NOTICE: f_leak => d3d9446802a44259755d38e6d163e820
+ a | b
+ ----+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+ 8 | c9f0f895fb98ab9159f51fd0297e236d
+ 10 | d3d9446802a44259755d38e6d163e820
+ (5 rows)
+
+ INSERT INTO bv1 VALUES (-1, 'xxx'); -- should fail view WCO
+ ERROR: new row violates WITH CHECK OPTION for "b1"
+ DETAIL: Failing row contains (-1, xxx).
+ INSERT INTO bv1 VALUES (11, 'xxx'); -- should fail RLS check
+ ERROR: new row violates WITH CHECK OPTION for "b1"
+ DETAIL: Failing row contains (11, xxx).
+ INSERT INTO bv1 VALUES (12, 'xxx'); -- ok
+ EXPLAIN (COSTS OFF) UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
+ QUERY PLAN
+ ---------------------------------------------------------------
+ Update on b1 b1_1
+ -> Subquery Scan on b1
+ Filter: f_leak(b1.b)
+ -> Seq Scan on b1 b1_2
+ Filter: ((a > 0) AND (a = 4) AND ((a % 2) = 0))
+ (5 rows)
+
+ UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
+ NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ EXPLAIN (COSTS OFF) DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
+ QUERY PLAN
+ ---------------------------------------------------------------
+ Delete on b1 b1_1
+ -> Subquery Scan on b1
+ Filter: f_leak(b1.b)
+ -> Seq Scan on b1 b1_2
+ Filter: ((a > 0) AND (a = 6) AND ((a % 2) = 0))
+ (5 rows)
+
+ DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
+ NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ SELECT * FROM b1;
+ a | b
+ -----+----------------------------------
+ -10 | 1b0fd9efa5279c4203b7c70233f86dbf
+ -9 | 252e691406782824eec43d7eadc3d256
+ -8 | a8d2ec85eaf98407310b72eb73dda247
+ -7 | 74687a12d3915d3c4d83f1af7b3683d5
+ -6 | 596a3d04481816330f07e4f97510c28f
+ -5 | 47c1b025fa18ea96c33fbb6718688c0f
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+ -3 | b3149ecea4628efd23d2f86e5a723472
+ -2 | 5d7b9adcbe1c629ec722529dd12e5129
+ -1 | 6bb61e3b7bce0931da574d19d1d82c88
+ 0 | cfcd208495d565ef66e7dff9f98764da
+ 1 | c4ca4238a0b923820dcc509a6f75849b
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3
+ 5 | e4da3b7fbbce2345d7772b0674a318d5
+ 7 | 8f14e45fceea167a5a36dedd4bea2543
+ 8 | c9f0f895fb98ab9159f51fd0297e236d
+ 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26
+ 10 | d3d9446802a44259755d38e6d163e820
+ 12 | xxx
+ 4 | yyy
+ (21 rows)
+
+ --
-- ROLE/GROUP
--
SET SESSION AUTHORIZATION rls_regress_user0;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index ee28a2c..49b3201
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
*************** UPDATE only t1 SET b = b WHERE f_leak(b)
*** 423,428 ****
--- 423,437 ----
UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ -- update with from clause self join
+ EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+
+ UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+
RESET SESSION AUTHORIZATION;
SET row_security TO OFF;
SELECT * FROM t1;
*************** DELETE FROM only t1 WHERE f_leak(b) RETU
*** 436,441 ****
--- 445,483 ----
DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
--
+ -- S.b. view on top of Row-level security
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ CREATE TABLE b1 (a int, b text);
+ INSERT INTO b1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+ CREATE POLICY p1 ON b1 USING (a % 2 = 0);
+ ALTER TABLE b1 ENABLE ROW LEVEL SECURITY;
+ GRANT ALL ON b1 TO rls_regress_user1;
+
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ CREATE VIEW bv1 WITH (security_barrier) AS SELECT * FROM b1 WHERE a > 0 WITH CHECK OPTION;
+ GRANT ALL ON bv1 TO rls_regress_user2;
+
+ SET SESSION AUTHORIZATION rls_regress_user2;
+
+ EXPLAIN (COSTS OFF) SELECT * FROM bv1 WHERE f_leak(b);
+ SELECT * FROM bv1 WHERE f_leak(b);
+
+ INSERT INTO bv1 VALUES (-1, 'xxx'); -- should fail view WCO
+ INSERT INTO bv1 VALUES (11, 'xxx'); -- should fail RLS check
+ INSERT INTO bv1 VALUES (12, 'xxx'); -- ok
+
+ EXPLAIN (COSTS OFF) UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
+ UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
+
+ EXPLAIN (COSTS OFF) DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
+ DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ SELECT * FROM b1;
+
+ --
-- ROLE/GROUP
--
SET SESSION AUTHORIZATION rls_regress_user0;
* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:
Turns out it wasn't as simple as that. prepend_row_security_policies()
really could get called multiple times for the same RTE, because the
call to query_tree_walker() at the end of fireRIRrules() would descend
into the just-added quals again. The simplest fix seems to be to
process RLS in a separate loop at the end, so that it can have it's
own infinite recursion detection, which is different from that needed
for pre-existing security quals and with check options from security
barrier views. This approach simplifies things a bit, and ensures that
we only try to expand RLS once for each RTE.
Right, I specifically recall having prepend_row_security_policies()
getting called multiple times for the same RTE. I like this approach of
using a separate loop though and it strikes me that it lends more
weight to the argument that we're better off with these as independent
considerations.
Also, I'm thinking that it would be better to refactor things a bit
and have prepend_row_security_policies() just return the new
securityQuals and withCheckOptions to add. Then fireRIRrules() would
only have to recurse into the new quals being added, not the
already-processed quals.
Hmm, good point.
Turns out that refactoring actually became necessary in order to fix
this bug, but I think it makes things cleaner and more efficient.
Sounds good, I'll take a look.
Here's an updated patch with a new test for this bug. I've been
developing the fixes for these RLS issues as one big patch, but I
suppose it would be easy to split up, if that's preferred.
I'm alright with it as-is for now.
Thanks!
Stephen
On 10 January 2015 at 15:12, Stephen Frost <sfrost@snowman.net> wrote:
* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:
Currently we're applying RLS CHECKs after the INSERT or UPDATE, like
WITH CHECK OPTIONs on views. The SQL spec says that WITH CHECK OPTIONs
on views have to be applied after the INSERT/UPDATE on the base
relation, but we're free to do something different for RLS CHECKs if
that makes more sense. If we want RLS to be more like column-level
privilege checking, then it does make sense to do the checks sooner,
so perhaps we should be checking the RLS policies before the
INSERT/UPDATE, like CHECK constraints.Were you thinking about working up a patch for such a change? If not,
I'll see about finding time to do it, unless someone else wants to
volunteer. :)
Attached is a patch to make RLS checks run before attempting to
insert/update any data rather than afterwards.
In the end I decided not to create a new structure for RLS checks
because most of the code that handles them treats them the same as
WCOs. Instead, I just added a new 'kind' enum field to the existing
structure and renamed/reworded things a bit.
The patch also changes the error message for a RLS check violation, to
make the cause of the error clearer. One thing I'm not sure about is
what sqlstate code to use for this error, but I don't think that using
WITH_CHECK_OPTION_VIOLATION is appropriate, because that seems to be
specifically intended for views.
Regards,
Dean
Attachments:
rls-timing.patchtext/x-diff; charset=US-ASCII; name=rls-timing.patchDownload
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
new file mode 100644
index 5b70cc9..6762b63
*** a/src/backend/executor/execMain.c
--- b/src/backend/executor/execMain.c
*************** ExecConstraints(ResultRelInfo *resultRel
*** 1638,1646 ****
/*
* ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs
*/
void
! ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate)
{
ExprContext *econtext;
--- 1638,1647 ----
/*
* ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs
+ * of the specified kind.
*/
void
! ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate)
{
ExprContext *econtext;
*************** ExecWithCheckOptions(ResultRelInfo *resu
*** 1663,1668 ****
--- 1664,1672 ----
WithCheckOption *wco = (WithCheckOption *) lfirst(l1);
ExprState *wcoExpr = (ExprState *) lfirst(l2);
+ if (wco->kind != kind)
+ continue;
+
/*
* WITH CHECK OPTION checks are intended to ensure that the new tuple
* is visible (in the case of a view) or that it passes the
*************** ExecWithCheckOptions(ResultRelInfo *resu
*** 1673,1686 ****
* case (the opposite of what we do above for CHECK constraints).
*/
if (!ExecQual((List *) wcoExpr, econtext, false))
! ereport(ERROR,
! (errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION),
! errmsg("new row violates WITH CHECK OPTION for \"%s\"",
! wco->viewname),
! errdetail("Failing row contains %s.",
! ExecBuildSlotValueDescription(slot,
! RelationGetDescr(resultRelInfo->ri_RelationDesc),
! 64))));
}
}
--- 1677,1711 ----
* case (the opposite of what we do above for CHECK constraints).
*/
if (!ExecQual((List *) wcoExpr, econtext, false))
! {
! switch (wco->kind)
! {
! case WCO_VIEW_CHECK:
! ereport(ERROR,
! (errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION),
! errmsg("new row violates WITH CHECK OPTION for \"%s\"",
! wco->relname),
! errdetail("Failing row contains %s.",
! ExecBuildSlotValueDescription(slot,
! RelationGetDescr(resultRelInfo->ri_RelationDesc),
! 64))));
! break;
! case WCO_RLS_INSERT_CHECK:
! case WCO_RLS_UPDATE_CHECK:
! ereport(ERROR,
! (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
! errmsg("new row violates row level security policy for \"%s\"",
! wco->relname),
! errdetail("Failing row contains %s.",
! ExecBuildSlotValueDescription(slot,
! RelationGetDescr(resultRelInfo->ri_RelationDesc),
! 64))));
! break;
! default:
! elog(ERROR, "unrecognized WCO kind: %u", wco->kind);
! break;
! }
! }
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index f96fb24..be879a4
*** a/src/backend/executor/nodeModifyTable.c
--- b/src/backend/executor/nodeModifyTable.c
*************** ExecInsert(TupleTableSlot *slot,
*** 253,258 ****
--- 253,265 ----
tuple->t_tableOid = RelationGetRelid(resultRelationDesc);
/*
+ * Check any RLS INSERT WITH CHECK policies
+ */
+ if (resultRelInfo->ri_WithCheckOptions != NIL)
+ ExecWithCheckOptions(WCO_RLS_INSERT_CHECK,
+ resultRelInfo, slot, estate);
+
+ /*
* Check the constraints of the tuple
*/
if (resultRelationDesc->rd_att->constr)
*************** ExecInsert(TupleTableSlot *slot,
*** 287,295 ****
list_free(recheckIndexes);
! /* Check any WITH CHECK OPTION constraints */
if (resultRelInfo->ri_WithCheckOptions != NIL)
! ExecWithCheckOptions(resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
--- 294,302 ----
list_free(recheckIndexes);
! /* Check any WITH CHECK OPTION constraints from parent views */
if (resultRelInfo->ri_WithCheckOptions != NIL)
! ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
*************** ExecUpdate(ItemPointer tupleid,
*** 653,667 ****
tuple->t_tableOid = RelationGetRelid(resultRelationDesc);
/*
! * Check the constraints of the tuple
*
* If we generate a new candidate tuple after EvalPlanQual testing, we
! * must loop back here and recheck constraints. (We don't need to
! * redo triggers, however. If there are any BEFORE triggers then
! * trigger.c will have done heap_lock_tuple to lock the correct tuple,
! * so there's no need to do them again.)
*/
lreplace:;
if (resultRelationDesc->rd_att->constr)
ExecConstraints(resultRelInfo, slot, estate);
--- 660,681 ----
tuple->t_tableOid = RelationGetRelid(resultRelationDesc);
/*
! * Check any RLS UPDATE WITH CHECK policies
*
* If we generate a new candidate tuple after EvalPlanQual testing, we
! * must loop back here and recheck any RLS policies and constraints.
! * (We don't need to redo triggers, however. If there are any BEFORE
! * triggers then trigger.c will have done heap_lock_tuple to lock the
! * correct tuple, so there's no need to do them again.)
*/
lreplace:;
+ if (resultRelInfo->ri_WithCheckOptions != NIL)
+ ExecWithCheckOptions(WCO_RLS_UPDATE_CHECK,
+ resultRelInfo, slot, estate);
+
+ /*
+ * Check the constraints of the tuple
+ */
if (resultRelationDesc->rd_att->constr)
ExecConstraints(resultRelInfo, slot, estate);
*************** lreplace:;
*** 780,788 ****
list_free(recheckIndexes);
! /* Check any WITH CHECK OPTION constraints */
if (resultRelInfo->ri_WithCheckOptions != NIL)
! ExecWithCheckOptions(resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
--- 794,802 ----
list_free(recheckIndexes);
! /* Check any WITH CHECK OPTION constraints from parent views */
if (resultRelInfo->ri_WithCheckOptions != NIL)
! ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index f1a24f5..777df58
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyWithCheckOption(const WithCheckOpti
*** 2055,2061 ****
{
WithCheckOption *newnode = makeNode(WithCheckOption);
! COPY_STRING_FIELD(viewname);
COPY_NODE_FIELD(qual);
COPY_SCALAR_FIELD(cascaded);
--- 2055,2062 ----
{
WithCheckOption *newnode = makeNode(WithCheckOption);
! COPY_SCALAR_FIELD(kind);
! COPY_STRING_FIELD(relname);
COPY_NODE_FIELD(qual);
COPY_SCALAR_FIELD(cascaded);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index 6e8b308..0b88c84
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalRangeTblFunction(const RangeTblFun
*** 2368,2374 ****
static bool
_equalWithCheckOption(const WithCheckOption *a, const WithCheckOption *b)
{
! COMPARE_STRING_FIELD(viewname);
COMPARE_NODE_FIELD(qual);
COMPARE_SCALAR_FIELD(cascaded);
--- 2368,2375 ----
static bool
_equalWithCheckOption(const WithCheckOption *a, const WithCheckOption *b)
{
! COMPARE_SCALAR_FIELD(kind);
! COMPARE_STRING_FIELD(relname);
COMPARE_NODE_FIELD(qual);
COMPARE_SCALAR_FIELD(cascaded);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index dd1278b..f514388
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outWithCheckOption(StringInfo str, cons
*** 2319,2325 ****
{
WRITE_NODE_TYPE("WITHCHECKOPTION");
! WRITE_STRING_FIELD(viewname);
WRITE_NODE_FIELD(qual);
WRITE_BOOL_FIELD(cascaded);
}
--- 2319,2326 ----
{
WRITE_NODE_TYPE("WITHCHECKOPTION");
! WRITE_ENUM_FIELD(kind, WCOKind);
! WRITE_STRING_FIELD(relname);
WRITE_NODE_FIELD(qual);
WRITE_BOOL_FIELD(cascaded);
}
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index ae24d05..86d1d1f
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readWithCheckOption(void)
*** 266,272 ****
{
READ_LOCALS(WithCheckOption);
! READ_STRING_FIELD(viewname);
READ_NODE_FIELD(qual);
READ_BOOL_FIELD(cascaded);
--- 266,273 ----
{
READ_LOCALS(WithCheckOption);
! READ_ENUM_FIELD(kind, WCOKind);
! READ_STRING_FIELD(relname);
READ_NODE_FIELD(qual);
READ_BOOL_FIELD(cascaded);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index b8e6e7a..2ce7c23
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** rewriteTargetView(Query *parsetree, Rela
*** 2910,2916 ****
WithCheckOption *wco;
wco = makeNode(WithCheckOption);
! wco->viewname = pstrdup(RelationGetRelationName(view));
wco->qual = NULL;
wco->cascaded = cascaded;
--- 2910,2917 ----
WithCheckOption *wco;
wco = makeNode(WithCheckOption);
! wco->kind = WCO_VIEW_CHECK;
! wco->relname = pstrdup(RelationGetRelationName(view));
wco->qual = NULL;
wco->cascaded = cascaded;
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 35790a9..e55f8fe
*** a/src/backend/rewrite/rowsecurity.c
--- b/src/backend/rewrite/rowsecurity.c
*************** prepend_row_security_policies(Query* roo
*** 226,232 ****
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
! wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) rowsec_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
--- 226,234 ----
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
! wco->kind = root->commandType == CMD_INSERT ?
! WCO_RLS_INSERT_CHECK : WCO_RLS_UPDATE_CHECK;
! wco->relname = RelationGetRelationName(rel);
wco->qual = (Node *) rowsec_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
*************** prepend_row_security_policies(Query* roo
*** 240,246 ****
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
! wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) hook_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
--- 242,250 ----
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
! wco->kind = root->commandType == CMD_INSERT ?
! WCO_RLS_INSERT_CHECK : WCO_RLS_UPDATE_CHECK;
! wco->relname = RelationGetRelationName(rel);
wco->qual = (Node *) hook_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
new file mode 100644
index 40fde83..cb4f158
*** a/src/include/executor/executor.h
--- b/src/include/executor/executor.h
*************** extern ResultRelInfo *ExecGetTriggerResu
*** 194,200 ****
extern bool ExecContextForcesOids(PlanState *planstate, bool *hasoids);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
! extern void ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern ExecRowMark *ExecFindRowMark(EState *estate, Index rti);
extern ExecAuxRowMark *ExecBuildAuxRowMark(ExecRowMark *erm, List *targetlist);
--- 194,200 ----
extern bool ExecContextForcesOids(PlanState *planstate, bool *hasoids);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
! extern void ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern ExecRowMark *ExecFindRowMark(EState *estate, Index rti);
extern ExecAuxRowMark *ExecBuildAuxRowMark(ExecRowMark *erm, List *targetlist);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 41288ed..791343c
*** a/src/include/nodes/execnodes.h
--- b/src/include/nodes/execnodes.h
*************** typedef struct JunkFilter
*** 303,309 ****
* TrigInstrument optional runtime measurements for triggers
* FdwRoutine FDW callback functions, if foreign table
* FdwState available to save private state of FDW
! * WithCheckOptions list of WithCheckOption's for views
* WithCheckOptionExprs list of WithCheckOption expr states
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
--- 303,309 ----
* TrigInstrument optional runtime measurements for triggers
* FdwRoutine FDW callback functions, if foreign table
* FdwState available to save private state of FDW
! * WithCheckOptions list of WithCheckOption's to be checked
* WithCheckOptionExprs list of WithCheckOption expr states
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index b1dfa85..3fadb7d
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct RangeTblFunction
*** 854,867 ****
/*
* WithCheckOption -
* representation of WITH CHECK OPTION checks to be applied to new tuples
! * when inserting/updating an auto-updatable view.
*/
typedef struct WithCheckOption
{
NodeTag type;
! char *viewname; /* name of view that specified the WCO */
Node *qual; /* constraint qual to check */
! bool cascaded; /* true = WITH CASCADED CHECK OPTION */
} WithCheckOption;
/*
--- 854,876 ----
/*
* WithCheckOption -
* representation of WITH CHECK OPTION checks to be applied to new tuples
! * when inserting/updating an auto-updatable view, or RLS WITH CHECK
! * policies to be applied when inserting/updating a relation with RLS.
*/
+ typedef enum WCOKind
+ {
+ WCO_VIEW_CHECK, /* WCO on an auto-updatable view */
+ WCO_RLS_INSERT_CHECK, /* RLS INSERT WITH CHECK policy */
+ WCO_RLS_UPDATE_CHECK /* RLS UPDATE WITH CHECK policy */
+ } WCOKind;
+
typedef struct WithCheckOption
{
NodeTag type;
! WCOKind kind; /* kind of WCO */
! char *relname; /* name of relation that specified the WCO */
Node *qual; /* constraint qual to check */
! bool cascaded; /* true for a cascaded WCO on a view */
} WithCheckOption;
/*
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index 430da55..e7777e6
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** SELECT * FROM document WHERE did = 8; --
*** 301,306 ****
--- 301,313 ----
-----+-----+--------+---------+--------
(0 rows)
+ -- RLS policies are checked before constraints
+ INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user2', 'my third manga'); -- Should fail with RLS check violation, not duplicate key violation
+ ERROR: new row violates row level security policy for "document"
+ DETAIL: Failing row contains (8, 44, 1, rls_regress_user2, my third manga).
+ UPDATE document SET did = 8, dauthor = 'rls_regress_user2' WHERE did = 5; -- Should fail with RLS check violation, not duplicate key violation
+ ERROR: new row violates row level security policy for "document"
+ DETAIL: Failing row contains (8, 44, 2, rls_regress_user2, my second manga).
-- database superuser does bypass RLS policy when enabled
RESET SESSION AUTHORIZATION;
SET row_security TO ON;
*************** EXPLAIN (COSTS OFF) WITH cte1 AS (SELECT
*** 1682,1688 ****
(6 rows)
WITH cte1 AS (UPDATE t1 SET a = a + 1 RETURNING *) SELECT * FROM cte1; --fail
! ERROR: new row violates WITH CHECK OPTION for "t1"
DETAIL: Failing row contains (1, cfcd208495d565ef66e7dff9f98764da).
WITH cte1 AS (UPDATE t1 SET a = a RETURNING *) SELECT * FROM cte1; --ok
a | b
--- 1689,1695 ----
(6 rows)
WITH cte1 AS (UPDATE t1 SET a = a + 1 RETURNING *) SELECT * FROM cte1; --fail
! ERROR: new row violates row level security policy for "t1"
DETAIL: Failing row contains (1, cfcd208495d565ef66e7dff9f98764da).
WITH cte1 AS (UPDATE t1 SET a = a RETURNING *) SELECT * FROM cte1; --ok
a | b
*************** WITH cte1 AS (UPDATE t1 SET a = a RETURN
*** 1701,1707 ****
(11 rows)
WITH cte1 AS (INSERT INTO t1 VALUES (21, 'Fail') RETURNING *) SELECT * FROM cte1; --fail
! ERROR: new row violates WITH CHECK OPTION for "t1"
DETAIL: Failing row contains (21, Fail).
WITH cte1 AS (INSERT INTO t1 VALUES (20, 'Success') RETURNING *) SELECT * FROM cte1; --ok
a | b
--- 1708,1714 ----
(11 rows)
WITH cte1 AS (INSERT INTO t1 VALUES (21, 'Fail') RETURNING *) SELECT * FROM cte1; --fail
! ERROR: new row violates row level security policy for "t1"
DETAIL: Failing row contains (21, Fail).
WITH cte1 AS (INSERT INTO t1 VALUES (20, 'Success') RETURNING *) SELECT * FROM cte1; --ok
a | b
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index ee28a2c..9652dda
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
*************** SET SESSION AUTHORIZATION rls_regress_us
*** 146,151 ****
--- 146,155 ----
INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user1', 'my third manga'); -- Must fail with unique violation, revealing presence of did we can't see
SELECT * FROM document WHERE did = 8; -- and confirm we can't see it
+ -- RLS policies are checked before constraints
+ INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user2', 'my third manga'); -- Should fail with RLS check violation, not duplicate key violation
+ UPDATE document SET did = 8, dauthor = 'rls_regress_user2' WHERE did = 5; -- Should fail with RLS check violation, not duplicate key violation
+
-- database superuser does bypass RLS policy when enabled
RESET SESSION AUTHORIZATION;
SET row_security TO ON;
Dean,
* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:
Attached is a patch to make RLS checks run before attempting to
insert/update any data rather than afterwards.
Excellent, many thanks!
In the end I decided not to create a new structure for RLS checks
because most of the code that handles them treats them the same as
WCOs. Instead, I just added a new 'kind' enum field to the existing
structure and renamed/reworded things a bit.
Makes sense to me. I like being able to easily differentiate the two
from each other now while also not needing to duplicate a bunch of code.
I also like the reworded error messages.
I am wondering if we should, perhaps, rename ri_WithCheckOptions or
ExecWithCheckOptions() (or even more..) to indicate that they're used
for both view-based WITH CHECK options and for RLS.
We'll need to update this patch once the fixes for the WITH CHECK leaks
go in, of course.
The patch also changes the error message for a RLS check violation, to
make the cause of the error clearer. One thing I'm not sure about is
what sqlstate code to use for this error, but I don't think that using
WITH_CHECK_OPTION_VIOLATION is appropriate, because that seems to be
specifically intended for views.
Hmm, agreed. Any thoughts on what to use? We could fall back on
something like ERRCODE_INSUFFICIENT_PRIVILEGE if necessary, I suppose.
Thanks!
Stephen
Dean,
* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:
Here's an updated patch with a new test for this bug. I've been
developing the fixes for these RLS issues as one big patch, but I
suppose it would be easy to split up, if that's preferred.
Thanks for working on all of this!
I've brought this up to date with master and addressed the little bit
of bitrot. I'm still reviewing it but I'm generally happy with the
approach and would certainly welcome any additional thoughts from you or
feedback from others.
Thanks!
Stephen
Attachments:
rls.v4.patchtext/x-diff; charset=us-asciiDownload
From aaef38f8ac98ba65f6c7b90c61d95c20afe271d0 Mon Sep 17 00:00:00 2001
From: Stephen Frost <sfrost@snowman.net>
Date: Thu, 26 Feb 2015 00:21:20 -0500
Subject: [PATCH] Handle RLS independently in fireRIRrules()
The original approach to handling RLS and security policies in
fireRIRrules() had them included in the main loop through the rtable.
This turned out to be less than ideal, however, as the RLS policies
need their own infinite recursion detection apart from with check
options and security quals from security barrier views.
This is also simpler and ensures we only expand RLS policies once for
each RTE.
In passing, rename prepend_row_security_policies to
get_row_security_policies and have it explicitly return (through return
arguments) the security quals, with check options, hasRowSecurity and
hasSubLinks results.
Dean Rasheed
---
src/backend/optimizer/plan/planner.c | 38 ++++++-
src/backend/rewrite/rewriteHandler.c | 127 ++++++++++++++--------
src/backend/rewrite/rowsecurity.c | 89 ++++++++--------
src/include/rewrite/rowsecurity.h | 5 +-
src/test/regress/expected/rowsecurity.out | 170 ++++++++++++++++++++++++++++++
src/test/regress/sql/rowsecurity.sql | 42 ++++++++
6 files changed, 373 insertions(+), 98 deletions(-)
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
index b02a107..e4e4d80 100644
--- a/src/backend/optimizer/plan/planner.c
+++ b/src/backend/optimizer/plan/planner.c
@@ -791,6 +791,7 @@ inheritance_planner(PlannerInfo *root)
{
Query *parse = root->parse;
int parentRTindex = parse->resultRelation;
+ Bitmapset *resultRTindexes = NULL;
int nominalRelation = -1;
List *final_rtable = NIL;
int save_rel_array_size = 0;
@@ -816,7 +817,21 @@ inheritance_planner(PlannerInfo *root)
* (1) would result in a rangetable of length O(N^2) for N targets, with
* at least O(N^3) work expended here; and (2) would greatly complicate
* management of the rowMarks list.
+ *
+ * Note that any RTEs with security barrier quals will be turned into
+ * subqueries during planning, and so we must create copies of them too,
+ * except where they are target relations, which will each only be used
+ * in a single plan.
*/
+ resultRTindexes = bms_add_member(resultRTindexes, parentRTindex);
+ foreach(lc, root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(lc);
+ if (appinfo->parent_relid == parentRTindex)
+ resultRTindexes = bms_add_member(resultRTindexes,
+ appinfo->child_relid);
+ }
+
foreach(lc, root->append_rel_list)
{
AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(lc);
@@ -887,21 +902,29 @@ inheritance_planner(PlannerInfo *root)
{
RangeTblEntry *rte = (RangeTblEntry *) lfirst(lr);
- if (rte->rtekind == RTE_SUBQUERY)
+ /*
+ * Copy subquery RTEs and RTEs with security barrier quals
+ * that will be turned into subqueries, except those that are
+ * target relations.
+ */
+ if (rte->rtekind == RTE_SUBQUERY ||
+ (rte->securityQuals != NIL &&
+ !bms_is_member(rti, resultRTindexes)))
{
Index newrti;
/*
* The RTE can't contain any references to its own RT
- * index, so we can save a few cycles by applying
- * ChangeVarNodes before we append the RTE to the
- * rangetable.
+ * index, except in the security barrier quals, so we can
+ * save a few cycles by applying ChangeVarNodes before we
+ * append the RTE to the rangetable.
*/
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.append_rel_list, rti, newrti, 0);
rte = copyObject(rte);
+ ChangeVarNodes((Node *) rte->securityQuals, rti, newrti, 0);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
}
@@ -2271,8 +2294,13 @@ preprocess_rowmarks(PlannerInfo *root)
newrc = makeNode(PlanRowMark);
newrc->rti = newrc->prti = i;
newrc->rowmarkId = ++(root->glob->lastRowMarkId);
- /* real tables support REFERENCE, anything else needs COPY */
+ /*
+ * Real tables support REFERENCE, anything else needs COPY. Since
+ * RTEs with security barrier quals will become subqueries, they also
+ * need COPY.
+ */
if (rte->rtekind == RTE_RELATION &&
+ rte->securityQuals == NIL &&
rte->relkind != RELKIND_FOREIGN_TABLE)
newrc->markType = ROW_MARK_REFERENCE;
else
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 9d2c280..08ec13c 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -1714,51 +1714,6 @@ fireRIRrules(Query *parsetree, List *activeRIRs, bool forUpdatePushedDown)
activeRIRs = list_delete_first(activeRIRs);
}
}
- /*
- * If the RTE has row security quals, apply them and recurse into the
- * securityQuals.
- */
- if (prepend_row_security_policies(parsetree, rte, rt_index))
- {
- /*
- * We applied security quals, check for infinite recursion and
- * then expand any nested queries.
- */
- if (list_member_oid(activeRIRs, RelationGetRelid(rel)))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("infinite recursion detected in policy for relation \"%s\"",
- RelationGetRelationName(rel))));
-
- /*
- * Make sure we check for recursion in either securityQuals or
- * WITH CHECK quals.
- */
- if (rte->securityQuals != NIL)
- {
- activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs);
-
- expression_tree_walker( (Node*) rte->securityQuals,
- fireRIRonSubLink, (void*)activeRIRs );
-
- activeRIRs = list_delete_first(activeRIRs);
- }
-
- if (parsetree->withCheckOptions != NIL)
- {
- WithCheckOption *wco;
- List *quals = NIL;
-
- wco = (WithCheckOption *) makeNode(WithCheckOption);
- quals = lcons(wco->qual, quals);
-
- activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs);
-
- expression_tree_walker( (Node*) quals, fireRIRonSubLink,
- (void*)activeRIRs);
- }
-
- }
heap_close(rel, NoLock);
}
@@ -1780,6 +1735,88 @@ fireRIRrules(Query *parsetree, List *activeRIRs, bool forUpdatePushedDown)
query_tree_walker(parsetree, fireRIRonSubLink, (void *) activeRIRs,
QTW_IGNORE_RC_SUBQUERIES);
+ /*
+ * Apply any row level security policies. We do this last because it
+ * requires special recursion detection if the new quals have sublink
+ * subqueries, and if we did it in the loop above query_tree_walker
+ * would then recurse into those quals a second time.
+ */
+ rt_index = 0;
+ foreach(lc, parsetree->rtable)
+ {
+ RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
+ Relation rel;
+ List *securityQuals;
+ List *withCheckOptions;
+ bool hasRowSecurity;
+ bool hasSubLinks;
+
+ ++rt_index;
+
+ /* Only normal relations can have RLS policies */
+ if (rte->rtekind != RTE_RELATION ||
+ rte->relkind != RELKIND_RELATION)
+ continue;
+
+ rel = heap_open(rte->relid, NoLock);
+
+ /*
+ * Fetch any new security quals that must be applied to this RTE.
+ */
+ get_row_security_policies(parsetree, rte, rt_index,
+ &securityQuals, &withCheckOptions,
+ &hasRowSecurity, &hasSubLinks);
+
+ if (securityQuals != NIL || withCheckOptions != NIL)
+ {
+ if (hasSubLinks)
+ {
+ /*
+ * Recursively process the new quals, checking for infinite
+ * recursion.
+ */
+ if (list_member_oid(activeRIRs, RelationGetRelid(rel)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected in policy for relation \"%s\"",
+ RelationGetRelationName(rel))));
+
+ activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs);
+
+ expression_tree_walker( (Node*) securityQuals,
+ fireRIRonSubLink, (void*)activeRIRs );
+
+ expression_tree_walker( (Node*) withCheckOptions,
+ fireRIRonSubLink, (void*)activeRIRs );
+
+ activeRIRs = list_delete_first(activeRIRs);
+ }
+
+ /*
+ * Add the new security quals to the start of the RTE's list so
+ * that they get applied before any existing security quals (which
+ * might have come from a user-written security barrier view, and
+ * might contain malicious code).
+ */
+ rte->securityQuals = list_concat(securityQuals,
+ rte->securityQuals);
+
+ parsetree->withCheckOptions = list_concat(withCheckOptions,
+ parsetree->withCheckOptions);
+ }
+
+ /*
+ * Make sure the query is marked correctly if row level security
+ * applies, or if the new quals had sublinks.
+ */
+ if (hasRowSecurity)
+ parsetree->hasRowSecurity = true;
+ if (hasSubLinks)
+ parsetree->hasSubLinks = true;
+
+ heap_close(rel, NoLock);
+ }
+
return parsetree;
}
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
index 7669130..73a5794 100644
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -58,7 +58,7 @@
static List *pull_row_security_policies(CmdType cmd, Relation relation,
Oid user_id);
-static void process_policies(List *policies, int rt_index,
+static void process_policies(Query* root, List *policies, int rt_index,
Expr **final_qual,
Expr **final_with_check_qual,
bool *hassublinks);
@@ -73,16 +73,17 @@ static bool check_role_for_policy(ArrayType *policy_roles, Oid user_id);
row_security_policy_hook_type row_security_policy_hook = NULL;
/*
- * Check the given RTE to see whether it's already had row security quals
- * expanded and, if not, prepend any row security rules from built-in or
- * plug-in sources to the securityQuals. The security quals are rewritten (for
- * view expansion, etc) before being added to the RTE.
+ * Get any row security quals and check quals that should be applied to the
+ * specified RTE.
*
- * Returns true if any quals were added. Note that quals may have been found
- * but not added if user rights make the user exempt from row security.
+ * In addition hasRowSecurity is set to true if row level security is enabled
+ * (even if this RTE doesn't have any row security quals), and hasSubLinks is
+ * set to true if any of the quals returned contain sublinks.
*/
-bool
-prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
+void
+get_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index,
+ List **securityQuals, List **withCheckOptions,
+ bool *hasRowSecurity, bool *hasSubLinks)
{
Expr *rowsec_expr = NULL;
Expr *rowsec_with_check_expr = NULL;
@@ -96,8 +97,13 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
Oid user_id;
int sec_context;
int rls_status;
- bool defaultDeny = true;
- bool hassublinks = false;
+ bool defaultDeny = false;
+
+ /* Defaults for the return values */
+ *securityQuals = NIL;
+ *withCheckOptions = NIL;
+ *hasRowSecurity = false;
+ *hasSubLinks = false;
/* This is just to get the security context */
GetUserIdAndSecContext(&user_id, &sec_context);
@@ -113,14 +119,14 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
if (rte->relid < FirstNormalObjectId
|| rte->relkind != RELKIND_RELATION
|| (sec_context & SECURITY_ROW_LEVEL_DISABLED))
- return false;
+ return;
/* Determine the state of RLS for this, pass checkAsUser explicitly */
rls_status = check_enable_rls(rte->relid, rte->checkAsUser, false);
/* If there is no RLS on this table at all, nothing to do */
if (rls_status == RLS_NONE)
- return false;
+ return;
/*
* RLS_NONE_ENV means we are not doing any RLS now, but that may change
@@ -134,18 +140,11 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
* be replanned if the environment changes (GUCs, role), but we
* are not adding anything here.
*/
- root->hasRowSecurity = true;
+ *hasRowSecurity = true;
- return false;
+ return;
}
- /*
- * We may end up getting called multiple times for the same RTE, so check
- * to make sure we aren't doing double-work.
- */
- if (rte->securityQuals != NIL)
- return false;
-
/* Grab the built-in policies which should be applied to this relation. */
rel = heap_open(rte->relid, NoLock);
@@ -167,8 +166,8 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
defaultDeny = true;
/* Now that we have our policies, build the expressions from them. */
- process_policies(rowsec_policies, rt_index, &rowsec_expr,
- &rowsec_with_check_expr, &hassublinks);
+ process_policies(root, rowsec_policies, rt_index, &rowsec_expr,
+ &rowsec_with_check_expr, hasSubLinks);
/*
* Also, allow extensions to add their own policies.
@@ -191,14 +190,17 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
* enabled on the table, then we will ignore the internally-generated
* default-deny policy and use only the policies returned by the
* extension.
+ *
+ * Any extension policies are applied after any internally-generated
+ * policies, so that extensions cannot bypass built-in RLS.
*/
if (row_security_policy_hook)
{
hook_policies = (*row_security_policy_hook)(root->commandType, rel);
/* Build the expression from any policies returned. */
- process_policies(hook_policies, rt_index, &hook_expr,
- &hook_with_check_expr, &hassublinks);
+ process_policies(root, hook_policies, rt_index, &hook_expr,
+ &hook_with_check_expr, hasSubLinks);
}
/*
@@ -230,7 +232,7 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) rowsec_with_check_expr;
wco->cascaded = false;
- root->withCheckOptions = lcons(wco, root->withCheckOptions);
+ *withCheckOptions = lappend(*withCheckOptions, wco);
}
/*
@@ -244,7 +246,7 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) hook_with_check_expr;
wco->cascaded = false;
- root->withCheckOptions = lcons(wco, root->withCheckOptions);
+ *withCheckOptions = lappend(*withCheckOptions, wco);
}
}
@@ -254,11 +256,10 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
|| root->commandType == CMD_DELETE)
{
if (rowsec_expr)
- rte->securityQuals = lcons(rowsec_expr, rte->securityQuals);
+ *securityQuals = lappend(*securityQuals, rowsec_expr);
if (hook_expr)
- rte->securityQuals = lcons(hook_expr,
- rte->securityQuals);
+ *securityQuals = lappend(*securityQuals, hook_expr);
}
heap_close(rel, NoLock);
@@ -267,17 +268,9 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
* Mark this query as having row security, so plancache can invalidate
* it when necessary (eg: role changes)
*/
- root->hasRowSecurity = true;
+ *hasRowSecurity = true;
- /*
- * If we have sublinks added because of the policies being added to the
- * query, then set hasSubLinks on the Query to force subLinks to be
- * properly expanded.
- */
- root->hasSubLinks |= hassublinks;
-
- /* If we got this far, we must have added quals */
- return true;
+ return;
}
/*
@@ -383,7 +376,7 @@ pull_row_security_policies(CmdType cmd, Relation relation, Oid user_id)
* qual_eval, with_check_eval, and hassublinks are output variables
*/
static void
-process_policies(List *policies, int rt_index, Expr **qual_eval,
+process_policies(Query* root, List *policies, int rt_index, Expr **qual_eval,
Expr **with_check_eval, bool *hassublinks)
{
ListCell *item;
@@ -392,7 +385,8 @@ process_policies(List *policies, int rt_index, Expr **qual_eval,
/*
* Extract the USING and WITH CHECK quals from each of the policies
- * and add them to our lists.
+ * and add them to our lists. We only want WITH CHECK quals if this
+ * RTE is the query's result relation.
*/
foreach(item, policies)
{
@@ -401,7 +395,8 @@ process_policies(List *policies, int rt_index, Expr **qual_eval,
if (policy->qual != NULL)
quals = lcons(copyObject(policy->qual), quals);
- if (policy->with_check_qual != NULL)
+ if (policy->with_check_qual != NULL &&
+ rt_index == root->resultRelation)
with_check_quals = lcons(copyObject(policy->with_check_qual),
with_check_quals);
@@ -421,7 +416,7 @@ process_policies(List *policies, int rt_index, Expr **qual_eval,
* If we end up with only USING quals, then use those as
* WITH CHECK quals also.
*/
- if (with_check_quals == NIL)
+ if (with_check_quals == NIL && rt_index == root->resultRelation)
with_check_quals = copyObject(quals);
/*
@@ -450,8 +445,10 @@ process_policies(List *policies, int rt_index, Expr **qual_eval,
*/
if (list_length(with_check_quals) > 1)
*with_check_eval = makeBoolExpr(OR_EXPR, with_check_quals, -1);
- else
+ else if (with_check_quals != NIL)
*with_check_eval = (Expr*) linitial(with_check_quals);
+ else
+ *with_check_eval = NULL;
return;
}
diff --git a/src/include/rewrite/rowsecurity.h b/src/include/rewrite/rowsecurity.h
index 8d19bfd..ac72427 100644
--- a/src/include/rewrite/rowsecurity.h
+++ b/src/include/rewrite/rowsecurity.h
@@ -39,7 +39,8 @@ typedef List *(*row_security_policy_hook_type)(CmdType cmdtype,
extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
-extern bool prepend_row_security_policies(Query* root, RangeTblEntry* rte,
- int rt_index);
+extern void get_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index,
+ List **securityQuals, List **withCheckOptions,
+ bool *hasRowSecurity, bool *hasSubLinks);
#endif /* ROWSECURITY_H */
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index f41bef1..6202e1c 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -1114,6 +1114,79 @@ NOTICE: f_leak => yyyyyy
302 | 2 | yyyyyy | (2,yyyyyy)
(5 rows)
+-- update with from clause self join
+EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+ QUERY PLAN
+---------------------------------------------------------------------
+ Update on t1 t1_1_3
+ -> Nested Loop
+ Join Filter: (t1_1.b = t1_2.b)
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> LockRows
+ -> Seq Scan on t1 t1_1_4
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2
+ Filter: f_leak(t1_2.b)
+ -> Append
+ -> Seq Scan on t1 t1_2_3
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_4
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_5
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Nested Loop
+ Join Filter: (t1_1_1.b = t1_2_1.b)
+ -> Subquery Scan on t1_1_1
+ Filter: f_leak(t1_1_1.b)
+ -> LockRows
+ -> Seq Scan on t2 t1_1_5
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2_1
+ Filter: f_leak(t1_2_1.b)
+ -> LockRows
+ -> Append
+ -> Seq Scan on t1 t1_2_6
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_7
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_8
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Nested Loop
+ Join Filter: (t1_1_2.b = t1_2_2.b)
+ -> Subquery Scan on t1_1_2
+ Filter: f_leak(t1_1_2.b)
+ -> LockRows
+ -> Seq Scan on t3 t1_1_6
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2_2
+ Filter: f_leak(t1_2_2.b)
+ -> LockRows
+ -> Append
+ -> Seq Scan on t1 t1_2_9
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_10
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_11
+ Filter: ((a = 4) AND ((a % 2) = 0))
+(51 rows)
+
+UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => dddddd_updt
+NOTICE: f_leak => defdef
+NOTICE: f_leak => defdef
+NOTICE: f_leak => defdef
+ a | b | a | b | t1_1 | t1_2
+---+-------------+---+-------------+-----------------+-----------------
+ 4 | dddddd_updt | 4 | dddddd_updt | (4,dddddd_updt) | (4,dddddd_updt)
+ 4 | defdef | 4 | defdef | (4,defdef) | (4,defdef)
+(2 rows)
+
RESET SESSION AUTHORIZATION;
SET row_security TO OFF;
SELECT * FROM t1;
@@ -1187,6 +1260,103 @@ NOTICE: f_leak => yyyyyy
(3 rows)
--
+-- S.b. view on top of Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE b1 (a int, b text);
+INSERT INTO b1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+CREATE POLICY p1 ON b1 USING (a % 2 = 0);
+ALTER TABLE b1 ENABLE ROW LEVEL SECURITY;
+GRANT ALL ON b1 TO rls_regress_user1;
+SET SESSION AUTHORIZATION rls_regress_user1;
+CREATE VIEW bv1 WITH (security_barrier) AS SELECT * FROM b1 WHERE a > 0 WITH CHECK OPTION;
+GRANT ALL ON bv1 TO rls_regress_user2;
+SET SESSION AUTHORIZATION rls_regress_user2;
+EXPLAIN (COSTS OFF) SELECT * FROM bv1 WHERE f_leak(b);
+ QUERY PLAN
+---------------------------------------------
+ Subquery Scan on bv1
+ Filter: f_leak(bv1.b)
+ -> Seq Scan on b1
+ Filter: ((a > 0) AND ((a % 2) = 0))
+(4 rows)
+
+SELECT * FROM bv1 WHERE f_leak(b);
+NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c
+NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+NOTICE: f_leak => c9f0f895fb98ab9159f51fd0297e236d
+NOTICE: f_leak => d3d9446802a44259755d38e6d163e820
+ a | b
+----+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+ 8 | c9f0f895fb98ab9159f51fd0297e236d
+ 10 | d3d9446802a44259755d38e6d163e820
+(5 rows)
+
+INSERT INTO bv1 VALUES (-1, 'xxx'); -- should fail view WCO
+ERROR: new row violates WITH CHECK OPTION for "b1"
+INSERT INTO bv1 VALUES (11, 'xxx'); -- should fail RLS check
+ERROR: new row violates WITH CHECK OPTION for "b1"
+INSERT INTO bv1 VALUES (12, 'xxx'); -- ok
+EXPLAIN (COSTS OFF) UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
+ QUERY PLAN
+---------------------------------------------------------------------------
+ Update on b1 b1_1
+ -> Subquery Scan on b1
+ Filter: f_leak(b1.b)
+ -> Subquery Scan on b1_2
+ -> LockRows
+ -> Seq Scan on b1 b1_3
+ Filter: ((a > 0) AND (a = 4) AND ((a % 2) = 0))
+(7 rows)
+
+UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
+NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+EXPLAIN (COSTS OFF) DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
+ QUERY PLAN
+---------------------------------------------------------------------------
+ Delete on b1 b1_1
+ -> Subquery Scan on b1
+ Filter: f_leak(b1.b)
+ -> Subquery Scan on b1_2
+ -> LockRows
+ -> Seq Scan on b1 b1_3
+ Filter: ((a > 0) AND (a = 6) AND ((a % 2) = 0))
+(7 rows)
+
+DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
+NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+SET SESSION AUTHORIZATION rls_regress_user0;
+SELECT * FROM b1;
+ a | b
+-----+----------------------------------
+ -10 | 1b0fd9efa5279c4203b7c70233f86dbf
+ -9 | 252e691406782824eec43d7eadc3d256
+ -8 | a8d2ec85eaf98407310b72eb73dda247
+ -7 | 74687a12d3915d3c4d83f1af7b3683d5
+ -6 | 596a3d04481816330f07e4f97510c28f
+ -5 | 47c1b025fa18ea96c33fbb6718688c0f
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+ -3 | b3149ecea4628efd23d2f86e5a723472
+ -2 | 5d7b9adcbe1c629ec722529dd12e5129
+ -1 | 6bb61e3b7bce0931da574d19d1d82c88
+ 0 | cfcd208495d565ef66e7dff9f98764da
+ 1 | c4ca4238a0b923820dcc509a6f75849b
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3
+ 5 | e4da3b7fbbce2345d7772b0674a318d5
+ 7 | 8f14e45fceea167a5a36dedd4bea2543
+ 8 | c9f0f895fb98ab9159f51fd0297e236d
+ 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26
+ 10 | d3d9446802a44259755d38e6d163e820
+ 12 | xxx
+ 4 | yyy
+(21 rows)
+
+--
-- ROLE/GROUP
--
SET SESSION AUTHORIZATION rls_regress_user0;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index ed7adbf..e611eeb 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -423,6 +423,15 @@ UPDATE only t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+-- update with from clause self join
+EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+
+UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+
RESET SESSION AUTHORIZATION;
SET row_security TO OFF;
SELECT * FROM t1;
@@ -436,6 +445,39 @@ DELETE FROM only t1 WHERE f_leak(b) RETURNING oid, *, t1;
DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
--
+-- S.b. view on top of Row-level security
+--
+SET SESSION AUTHORIZATION rls_regress_user0;
+CREATE TABLE b1 (a int, b text);
+INSERT INTO b1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+CREATE POLICY p1 ON b1 USING (a % 2 = 0);
+ALTER TABLE b1 ENABLE ROW LEVEL SECURITY;
+GRANT ALL ON b1 TO rls_regress_user1;
+
+SET SESSION AUTHORIZATION rls_regress_user1;
+CREATE VIEW bv1 WITH (security_barrier) AS SELECT * FROM b1 WHERE a > 0 WITH CHECK OPTION;
+GRANT ALL ON bv1 TO rls_regress_user2;
+
+SET SESSION AUTHORIZATION rls_regress_user2;
+
+EXPLAIN (COSTS OFF) SELECT * FROM bv1 WHERE f_leak(b);
+SELECT * FROM bv1 WHERE f_leak(b);
+
+INSERT INTO bv1 VALUES (-1, 'xxx'); -- should fail view WCO
+INSERT INTO bv1 VALUES (11, 'xxx'); -- should fail RLS check
+INSERT INTO bv1 VALUES (12, 'xxx'); -- ok
+
+EXPLAIN (COSTS OFF) UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
+UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
+
+EXPLAIN (COSTS OFF) DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
+DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
+
+SET SESSION AUTHORIZATION rls_regress_user0;
+SELECT * FROM b1;
+
+--
-- ROLE/GROUP
--
SET SESSION AUTHORIZATION rls_regress_user0;
--
1.9.1
Dean,
* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:
Attached is a patch to make RLS checks run before attempting to
insert/update any data rather than afterwards.
Excellent, this I really like and it's a pretty straight-forward change.
I wonder if there are some documentation updates which need to be done
for this also? I'm planning to look as I vauguely recall mentioning the
ordering of operations somewhere along the way.
I also addressed the bitrot from the column-priv leak patch. Would be
great to have you take a look at the latest and let me know if you have
any further comments or suggestions. I'm definitely looking forward to
getting these changes in soon.
Thanks!
Stephen
Attachments:
rls-timing-v2.patchtext/x-diff; charset=us-asciiDownload
From 9fb54d9119d83b00cc7839efff573d313c6fa8e5 Mon Sep 17 00:00:00 2001
From: Stephen Frost <sfrost@snowman.net>
Date: Wed, 25 Feb 2015 23:58:22 -0500
Subject: [PATCH] Run RLS checks before attempting to insert/update
The initial approach with RLS leveraged the existing WITH CHECK
OPTION capability, which meant that the tuple was essentially fully
inserted and only after that was done was it checked for policies. WITH
CHECK OPTION is required to do that per the SQL spec, but RLS would
ideally be done earlier.
This changes the RLS policies to be done prior to attempting to insert
or update the tuple, which means that an RLS violation will be thrown
(if the policy is violated) instead of, say, a primary key constraint
violation. This approach still leverages a great deal of the WITH CHECK
OPTION infrastructure, but modifies it to understand when it's operating
with RLS policies or WITH CHECK options, which allows it to produce
better error messages.
Dean Rasheed
---
src/backend/executor/execMain.c | 34 ++++++++++++++++++++++++-------
src/backend/executor/nodeModifyTable.c | 32 +++++++++++++++++++++--------
src/backend/nodes/copyfuncs.c | 3 ++-
src/backend/nodes/equalfuncs.c | 3 ++-
src/backend/nodes/outfuncs.c | 3 ++-
src/backend/nodes/readfuncs.c | 3 ++-
src/backend/rewrite/rewriteHandler.c | 3 ++-
src/backend/rewrite/rowsecurity.c | 8 ++++++--
src/include/executor/executor.h | 2 +-
src/include/nodes/execnodes.h | 2 +-
src/include/nodes/parsenodes.h | 15 +++++++++++---
src/test/regress/expected/rowsecurity.out | 9 ++++++--
src/test/regress/sql/rowsecurity.sql | 4 ++++
13 files changed, 91 insertions(+), 30 deletions(-)
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
index 33b172b..5c0ae39 100644
--- a/src/backend/executor/execMain.c
+++ b/src/backend/executor/execMain.c
@@ -1667,9 +1667,10 @@ ExecConstraints(ResultRelInfo *resultRelInfo,
/*
* ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs
+ * of the specified kind.
*/
void
-ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
+ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate)
{
Relation rel = resultRelInfo->ri_RelationDesc;
@@ -1694,6 +1695,9 @@ ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
WithCheckOption *wco = (WithCheckOption *) lfirst(l1);
ExprState *wcoExpr = (ExprState *) lfirst(l2);
+ if (wco->kind != kind)
+ continue;
+
/*
* WITH CHECK OPTION checks are intended to ensure that the new tuple
* is visible (in the case of a view) or that it passes the
@@ -1715,12 +1719,28 @@ ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
modifiedCols,
64);
- ereport(ERROR,
- (errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION),
- errmsg("new row violates WITH CHECK OPTION for \"%s\"",
- wco->viewname),
- val_desc ? errdetail("Failing row contains %s.", val_desc) :
- 0));
+ switch (wco->kind)
+ {
+ case WCO_VIEW_CHECK:
+ ereport(ERROR,
+ (errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION),
+ errmsg("new row violates WITH CHECK OPTION for \"%s\"",
+ wco->relname),
+ val_desc ? errdetail("Failing row contains %s.",
+ val_desc) : 0));
+ case WCO_RLS_INSERT_CHECK:
+ case WCO_RLS_UPDATE_CHECK:
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("new row violates row level security policy for \"%s\"",
+ wco->relname),
+ val_desc ? errdetail("Failing row contains %s.",
+ val_desc) : 0));
+ break;
+ default:
+ elog(ERROR, "unrecognized WCO kind: %u", wco->kind);
+ break;
+ }
}
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
index f96fb24..be879a4 100644
--- a/src/backend/executor/nodeModifyTable.c
+++ b/src/backend/executor/nodeModifyTable.c
@@ -253,6 +253,13 @@ ExecInsert(TupleTableSlot *slot,
tuple->t_tableOid = RelationGetRelid(resultRelationDesc);
/*
+ * Check any RLS INSERT WITH CHECK policies
+ */
+ if (resultRelInfo->ri_WithCheckOptions != NIL)
+ ExecWithCheckOptions(WCO_RLS_INSERT_CHECK,
+ resultRelInfo, slot, estate);
+
+ /*
* Check the constraints of the tuple
*/
if (resultRelationDesc->rd_att->constr)
@@ -287,9 +294,9 @@ ExecInsert(TupleTableSlot *slot,
list_free(recheckIndexes);
- /* Check any WITH CHECK OPTION constraints */
+ /* Check any WITH CHECK OPTION constraints from parent views */
if (resultRelInfo->ri_WithCheckOptions != NIL)
- ExecWithCheckOptions(resultRelInfo, slot, estate);
+ ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
@@ -653,15 +660,22 @@ ExecUpdate(ItemPointer tupleid,
tuple->t_tableOid = RelationGetRelid(resultRelationDesc);
/*
- * Check the constraints of the tuple
+ * Check any RLS UPDATE WITH CHECK policies
*
* If we generate a new candidate tuple after EvalPlanQual testing, we
- * must loop back here and recheck constraints. (We don't need to
- * redo triggers, however. If there are any BEFORE triggers then
- * trigger.c will have done heap_lock_tuple to lock the correct tuple,
- * so there's no need to do them again.)
+ * must loop back here and recheck any RLS policies and constraints.
+ * (We don't need to redo triggers, however. If there are any BEFORE
+ * triggers then trigger.c will have done heap_lock_tuple to lock the
+ * correct tuple, so there's no need to do them again.)
*/
lreplace:;
+ if (resultRelInfo->ri_WithCheckOptions != NIL)
+ ExecWithCheckOptions(WCO_RLS_UPDATE_CHECK,
+ resultRelInfo, slot, estate);
+
+ /*
+ * Check the constraints of the tuple
+ */
if (resultRelationDesc->rd_att->constr)
ExecConstraints(resultRelInfo, slot, estate);
@@ -780,9 +794,9 @@ lreplace:;
list_free(recheckIndexes);
- /* Check any WITH CHECK OPTION constraints */
+ /* Check any WITH CHECK OPTION constraints from parent views */
if (resultRelInfo->ri_WithCheckOptions != NIL)
- ExecWithCheckOptions(resultRelInfo, slot, estate);
+ ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
index 9fe8008..d49585a 100644
--- a/src/backend/nodes/copyfuncs.c
+++ b/src/backend/nodes/copyfuncs.c
@@ -2059,7 +2059,8 @@ _copyWithCheckOption(const WithCheckOption *from)
{
WithCheckOption *newnode = makeNode(WithCheckOption);
- COPY_STRING_FIELD(viewname);
+ COPY_SCALAR_FIELD(kind);
+ COPY_STRING_FIELD(relname);
COPY_NODE_FIELD(qual);
COPY_SCALAR_FIELD(cascaded);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
index fe509b0..2cf1899 100644
--- a/src/backend/nodes/equalfuncs.c
+++ b/src/backend/nodes/equalfuncs.c
@@ -2371,7 +2371,8 @@ _equalRangeTblFunction(const RangeTblFunction *a, const RangeTblFunction *b)
static bool
_equalWithCheckOption(const WithCheckOption *a, const WithCheckOption *b)
{
- COMPARE_STRING_FIELD(viewname);
+ COMPARE_SCALAR_FIELD(kind);
+ COMPARE_STRING_FIELD(relname);
COMPARE_NODE_FIELD(qual);
COMPARE_SCALAR_FIELD(cascaded);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
index 775f482..f86086c 100644
--- a/src/backend/nodes/outfuncs.c
+++ b/src/backend/nodes/outfuncs.c
@@ -2326,7 +2326,8 @@ _outWithCheckOption(StringInfo str, const WithCheckOption *node)
{
WRITE_NODE_TYPE("WITHCHECKOPTION");
- WRITE_STRING_FIELD(viewname);
+ WRITE_ENUM_FIELD(kind, WCOKind);
+ WRITE_STRING_FIELD(relname);
WRITE_NODE_FIELD(qual);
WRITE_BOOL_FIELD(cascaded);
}
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
index 563209c..b0cd95d 100644
--- a/src/backend/nodes/readfuncs.c
+++ b/src/backend/nodes/readfuncs.c
@@ -266,7 +266,8 @@ _readWithCheckOption(void)
{
READ_LOCALS(WithCheckOption);
- READ_STRING_FIELD(viewname);
+ READ_ENUM_FIELD(kind, WCOKind);
+ READ_STRING_FIELD(relname);
READ_NODE_FIELD(qual);
READ_BOOL_FIELD(cascaded);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
index 9d2c280..9ea770b 100644
--- a/src/backend/rewrite/rewriteHandler.c
+++ b/src/backend/rewrite/rewriteHandler.c
@@ -2910,7 +2910,8 @@ rewriteTargetView(Query *parsetree, Relation view)
WithCheckOption *wco;
wco = makeNode(WithCheckOption);
- wco->viewname = pstrdup(RelationGetRelationName(view));
+ wco->kind = WCO_VIEW_CHECK;
+ wco->relname = pstrdup(RelationGetRelationName(view));
wco->qual = NULL;
wco->cascaded = cascaded;
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
index 7669130..687c7f6 100644
--- a/src/backend/rewrite/rowsecurity.c
+++ b/src/backend/rewrite/rowsecurity.c
@@ -227,7 +227,9 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
- wco->viewname = RelationGetRelationName(rel);
+ wco->kind = root->commandType == CMD_INSERT ?
+ WCO_RLS_INSERT_CHECK : WCO_RLS_UPDATE_CHECK;
+ wco->relname = RelationGetRelationName(rel);
wco->qual = (Node *) rowsec_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
@@ -241,7 +243,9 @@ prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
- wco->viewname = RelationGetRelationName(rel);
+ wco->kind = root->commandType == CMD_INSERT ?
+ WCO_RLS_INSERT_CHECK : WCO_RLS_UPDATE_CHECK;
+ wco->relname = RelationGetRelationName(rel);
wco->qual = (Node *) hook_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 40fde83..cb4f158 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -194,7 +194,7 @@ extern ResultRelInfo *ExecGetTriggerResultRel(EState *estate, Oid relid);
extern bool ExecContextForcesOids(PlanState *planstate, bool *hasoids);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
-extern void ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
+extern void ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern ExecRowMark *ExecFindRowMark(EState *estate, Index rti);
extern ExecAuxRowMark *ExecBuildAuxRowMark(ExecRowMark *erm, List *targetlist);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
index 41288ed..791343c 100644
--- a/src/include/nodes/execnodes.h
+++ b/src/include/nodes/execnodes.h
@@ -303,7 +303,7 @@ typedef struct JunkFilter
* TrigInstrument optional runtime measurements for triggers
* FdwRoutine FDW callback functions, if foreign table
* FdwState available to save private state of FDW
- * WithCheckOptions list of WithCheckOption's for views
+ * WithCheckOptions list of WithCheckOption's to be checked
* WithCheckOptionExprs list of WithCheckOption expr states
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
index ac13302..2c3bbce 100644
--- a/src/include/nodes/parsenodes.h
+++ b/src/include/nodes/parsenodes.h
@@ -861,14 +861,23 @@ typedef struct RangeTblFunction
/*
* WithCheckOption -
* representation of WITH CHECK OPTION checks to be applied to new tuples
- * when inserting/updating an auto-updatable view.
+ * when inserting/updating an auto-updatable view, or RLS WITH CHECK
+ * policies to be applied when inserting/updating a relation with RLS.
*/
+typedef enum WCOKind
+{
+ WCO_VIEW_CHECK, /* WCO on an auto-updatable view */
+ WCO_RLS_INSERT_CHECK, /* RLS INSERT WITH CHECK policy */
+ WCO_RLS_UPDATE_CHECK /* RLS UPDATE WITH CHECK policy */
+} WCOKind;
+
typedef struct WithCheckOption
{
NodeTag type;
- char *viewname; /* name of view that specified the WCO */
+ WCOKind kind; /* kind of WCO */
+ char *relname; /* name of relation that specified the WCO */
Node *qual; /* constraint qual to check */
- bool cascaded; /* true = WITH CASCADED CHECK OPTION */
+ bool cascaded; /* true for a cascaded WCO on a view */
} WithCheckOption;
/*
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
index f41bef1..d318bf9 100644
--- a/src/test/regress/expected/rowsecurity.out
+++ b/src/test/regress/expected/rowsecurity.out
@@ -300,6 +300,11 @@ SELECT * FROM document WHERE did = 8; -- and confirm we can't see it
-----+-----+--------+---------+--------
(0 rows)
+-- RLS policies are checked before constraints
+INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user2', 'my third manga'); -- Should fail with RLS check violation, not duplicate key violation
+ERROR: new row violates row level security policy for "document"
+UPDATE document SET did = 8, dauthor = 'rls_regress_user2' WHERE did = 5; -- Should fail with RLS check violation, not duplicate key violation
+ERROR: new row violates row level security policy for "document"
-- database superuser does bypass RLS policy when enabled
RESET SESSION AUTHORIZATION;
SET row_security TO ON;
@@ -1689,7 +1694,7 @@ EXPLAIN (COSTS OFF) WITH cte1 AS (SELECT * FROM t1 WHERE f_leak(b)) SELECT * FRO
(6 rows)
WITH cte1 AS (UPDATE t1 SET a = a + 1 RETURNING *) SELECT * FROM cte1; --fail
-ERROR: new row violates WITH CHECK OPTION for "t1"
+ERROR: new row violates row level security policy for "t1"
WITH cte1 AS (UPDATE t1 SET a = a RETURNING *) SELECT * FROM cte1; --ok
a | b
----+----------------------------------
@@ -1707,7 +1712,7 @@ WITH cte1 AS (UPDATE t1 SET a = a RETURNING *) SELECT * FROM cte1; --ok
(11 rows)
WITH cte1 AS (INSERT INTO t1 VALUES (21, 'Fail') RETURNING *) SELECT * FROM cte1; --fail
-ERROR: new row violates WITH CHECK OPTION for "t1"
+ERROR: new row violates row level security policy for "t1"
WITH cte1 AS (INSERT INTO t1 VALUES (20, 'Success') RETURNING *) SELECT * FROM cte1; --ok
a | b
----+---------
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
index ed7adbf..a9f4b83 100644
--- a/src/test/regress/sql/rowsecurity.sql
+++ b/src/test/regress/sql/rowsecurity.sql
@@ -146,6 +146,10 @@ SET SESSION AUTHORIZATION rls_regress_user1;
INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user1', 'my third manga'); -- Must fail with unique violation, revealing presence of did we can't see
SELECT * FROM document WHERE did = 8; -- and confirm we can't see it
+-- RLS policies are checked before constraints
+INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user2', 'my third manga'); -- Should fail with RLS check violation, not duplicate key violation
+UPDATE document SET did = 8, dauthor = 'rls_regress_user2' WHERE did = 5; -- Should fail with RLS check violation, not duplicate key violation
+
-- database superuser does bypass RLS policy when enabled
RESET SESSION AUTHORIZATION;
SET row_security TO ON;
--
1.9.1
On 26 February 2015 at 05:41, Stephen Frost <sfrost@snowman.net> wrote:
Dean,
* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:
Here's an updated patch with a new test for this bug. I've been
developing the fixes for these RLS issues as one big patch, but I
suppose it would be easy to split up, if that's preferred.Thanks for working on all of this!
I've brought this up to date with master and addressed the little bit
of bitrot. I'm still reviewing it but I'm generally happy with the
approach and would certainly welcome any additional thoughts from you or
feedback from others.
Thanks for doing that. I had been meaning to update it myself, but
kept getting distracted by my day job.
I think this should probably be committed as 2 separate patches,
because there are really 2 separate issues being fixed here, and the
commit comment only mentions the second one:
1). The planner changes and the first new regression test (update with
from clause self join). This is a fix to the inheritance planner code,
which wasn't properly handling the case of a query containing both
target and non-target relations with RLS and inheritance.
2). The rewriter changes and the other regression tests (S.b. view on
top of Row-level security). This is more than just a code tidy-up --
there's a real bug there. If you run those tests against HEAD, the
update against the s.b. view fails to apply the RLS policy of the
underlying table because the old recursion detection code thinks it
has already handled that RTE.
I should get more time to look at this over the weekend.
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 26 February 2015 at 05:43, Stephen Frost <sfrost@snowman.net> wrote:
Dean,
* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:
Attached is a patch to make RLS checks run before attempting to
insert/update any data rather than afterwards.Excellent, this I really like and it's a pretty straight-forward change.
I wonder if there are some documentation updates which need to be done
for this also? I'm planning to look as I vauguely recall mentioning the
ordering of operations somewhere along the way.I also addressed the bitrot from the column-priv leak patch. Would be
great to have you take a look at the latest and let me know if you have
any further comments or suggestions. I'm definitely looking forward to
getting these changes in soon.
Thanks for the update. I don't recall any mention of ordering in the
docs, but if there isn't anything there it makes sense to add
something.
I haven't thought about this patch in a while, but I still like the
look of it. It's definitely neater to do these checks earlier and it's
good to be able to get RLS-specific errors too. I'll take a look at
the latest updates.
Regards,
Dean
--
Sent via pgsql-hackers mailing list (pgsql-hackers@postgresql.org)
To make changes to your subscription:
http://www.postgresql.org/mailpref/pgsql-hackers
On 26 February 2015 at 09:27, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
I think this should probably be committed as 2 separate patches...
On closer inspection, the 2 parts are interrelated since the new
self-join test that tests the inheritance planner changes also
requires the rewriter changes, otherwise it falls over with an
infinite recursion error in the rewriter. I suppose the rewriter
changes could be committed first with the self-join without
inheritance test to test the fix to the infinite recursion problem,
but I think it's probably easier to just treat this as a single set of
related bug fixes.
Attached is an updated patch. There are no more code changes, but I've
added a few more regression tests.
Regards,
Dean
Attachments:
rls.v5.patchtext/x-diff; charset=US-ASCII; name=rls.v5.patchDownload
diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c
new file mode 100644
index b02a107..e4e4d80
*** a/src/backend/optimizer/plan/planner.c
--- b/src/backend/optimizer/plan/planner.c
*************** inheritance_planner(PlannerInfo *root)
*** 791,796 ****
--- 791,797 ----
{
Query *parse = root->parse;
int parentRTindex = parse->resultRelation;
+ Bitmapset *resultRTindexes = NULL;
int nominalRelation = -1;
List *final_rtable = NIL;
int save_rel_array_size = 0;
*************** inheritance_planner(PlannerInfo *root)
*** 816,822 ****
--- 817,837 ----
* (1) would result in a rangetable of length O(N^2) for N targets, with
* at least O(N^3) work expended here; and (2) would greatly complicate
* management of the rowMarks list.
+ *
+ * Note that any RTEs with security barrier quals will be turned into
+ * subqueries during planning, and so we must create copies of them too,
+ * except where they are target relations, which will each only be used
+ * in a single plan.
*/
+ resultRTindexes = bms_add_member(resultRTindexes, parentRTindex);
+ foreach(lc, root->append_rel_list)
+ {
+ AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(lc);
+ if (appinfo->parent_relid == parentRTindex)
+ resultRTindexes = bms_add_member(resultRTindexes,
+ appinfo->child_relid);
+ }
+
foreach(lc, root->append_rel_list)
{
AppendRelInfo *appinfo = (AppendRelInfo *) lfirst(lc);
*************** inheritance_planner(PlannerInfo *root)
*** 887,907 ****
{
RangeTblEntry *rte = (RangeTblEntry *) lfirst(lr);
! if (rte->rtekind == RTE_SUBQUERY)
{
Index newrti;
/*
* The RTE can't contain any references to its own RT
! * index, so we can save a few cycles by applying
! * ChangeVarNodes before we append the RTE to the
! * rangetable.
*/
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.append_rel_list, rti, newrti, 0);
rte = copyObject(rte);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
}
--- 902,930 ----
{
RangeTblEntry *rte = (RangeTblEntry *) lfirst(lr);
! /*
! * Copy subquery RTEs and RTEs with security barrier quals
! * that will be turned into subqueries, except those that are
! * target relations.
! */
! if (rte->rtekind == RTE_SUBQUERY ||
! (rte->securityQuals != NIL &&
! !bms_is_member(rti, resultRTindexes)))
{
Index newrti;
/*
* The RTE can't contain any references to its own RT
! * index, except in the security barrier quals, so we can
! * save a few cycles by applying ChangeVarNodes before we
! * append the RTE to the rangetable.
*/
newrti = list_length(subroot.parse->rtable) + 1;
ChangeVarNodes((Node *) subroot.parse, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.rowMarks, rti, newrti, 0);
ChangeVarNodes((Node *) subroot.append_rel_list, rti, newrti, 0);
rte = copyObject(rte);
+ ChangeVarNodes((Node *) rte->securityQuals, rti, newrti, 0);
subroot.parse->rtable = lappend(subroot.parse->rtable,
rte);
}
*************** preprocess_rowmarks(PlannerInfo *root)
*** 2271,2278 ****
newrc = makeNode(PlanRowMark);
newrc->rti = newrc->prti = i;
newrc->rowmarkId = ++(root->glob->lastRowMarkId);
! /* real tables support REFERENCE, anything else needs COPY */
if (rte->rtekind == RTE_RELATION &&
rte->relkind != RELKIND_FOREIGN_TABLE)
newrc->markType = ROW_MARK_REFERENCE;
else
--- 2294,2306 ----
newrc = makeNode(PlanRowMark);
newrc->rti = newrc->prti = i;
newrc->rowmarkId = ++(root->glob->lastRowMarkId);
! /*
! * Real tables support REFERENCE, anything else needs COPY. Since
! * RTEs with security barrier quals will become subqueries, they also
! * need COPY.
! */
if (rte->rtekind == RTE_RELATION &&
+ rte->securityQuals == NIL &&
rte->relkind != RELKIND_FOREIGN_TABLE)
newrc->markType = ROW_MARK_REFERENCE;
else
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 9d2c280..08ec13c
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** fireRIRrules(Query *parsetree, List *act
*** 1714,1764 ****
activeRIRs = list_delete_first(activeRIRs);
}
}
- /*
- * If the RTE has row security quals, apply them and recurse into the
- * securityQuals.
- */
- if (prepend_row_security_policies(parsetree, rte, rt_index))
- {
- /*
- * We applied security quals, check for infinite recursion and
- * then expand any nested queries.
- */
- if (list_member_oid(activeRIRs, RelationGetRelid(rel)))
- ereport(ERROR,
- (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
- errmsg("infinite recursion detected in policy for relation \"%s\"",
- RelationGetRelationName(rel))));
-
- /*
- * Make sure we check for recursion in either securityQuals or
- * WITH CHECK quals.
- */
- if (rte->securityQuals != NIL)
- {
- activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs);
-
- expression_tree_walker( (Node*) rte->securityQuals,
- fireRIRonSubLink, (void*)activeRIRs );
-
- activeRIRs = list_delete_first(activeRIRs);
- }
-
- if (parsetree->withCheckOptions != NIL)
- {
- WithCheckOption *wco;
- List *quals = NIL;
-
- wco = (WithCheckOption *) makeNode(WithCheckOption);
- quals = lcons(wco->qual, quals);
-
- activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs);
-
- expression_tree_walker( (Node*) quals, fireRIRonSubLink,
- (void*)activeRIRs);
- }
-
- }
heap_close(rel, NoLock);
}
--- 1714,1719 ----
*************** fireRIRrules(Query *parsetree, List *act
*** 1780,1785 ****
--- 1735,1822 ----
query_tree_walker(parsetree, fireRIRonSubLink, (void *) activeRIRs,
QTW_IGNORE_RC_SUBQUERIES);
+ /*
+ * Apply any row level security policies. We do this last because it
+ * requires special recursion detection if the new quals have sublink
+ * subqueries, and if we did it in the loop above query_tree_walker
+ * would then recurse into those quals a second time.
+ */
+ rt_index = 0;
+ foreach(lc, parsetree->rtable)
+ {
+ RangeTblEntry *rte = (RangeTblEntry *) lfirst(lc);
+ Relation rel;
+ List *securityQuals;
+ List *withCheckOptions;
+ bool hasRowSecurity;
+ bool hasSubLinks;
+
+ ++rt_index;
+
+ /* Only normal relations can have RLS policies */
+ if (rte->rtekind != RTE_RELATION ||
+ rte->relkind != RELKIND_RELATION)
+ continue;
+
+ rel = heap_open(rte->relid, NoLock);
+
+ /*
+ * Fetch any new security quals that must be applied to this RTE.
+ */
+ get_row_security_policies(parsetree, rte, rt_index,
+ &securityQuals, &withCheckOptions,
+ &hasRowSecurity, &hasSubLinks);
+
+ if (securityQuals != NIL || withCheckOptions != NIL)
+ {
+ if (hasSubLinks)
+ {
+ /*
+ * Recursively process the new quals, checking for infinite
+ * recursion.
+ */
+ if (list_member_oid(activeRIRs, RelationGetRelid(rel)))
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_OBJECT_DEFINITION),
+ errmsg("infinite recursion detected in policy for relation \"%s\"",
+ RelationGetRelationName(rel))));
+
+ activeRIRs = lcons_oid(RelationGetRelid(rel), activeRIRs);
+
+ expression_tree_walker( (Node*) securityQuals,
+ fireRIRonSubLink, (void*)activeRIRs );
+
+ expression_tree_walker( (Node*) withCheckOptions,
+ fireRIRonSubLink, (void*)activeRIRs );
+
+ activeRIRs = list_delete_first(activeRIRs);
+ }
+
+ /*
+ * Add the new security quals to the start of the RTE's list so
+ * that they get applied before any existing security quals (which
+ * might have come from a user-written security barrier view, and
+ * might contain malicious code).
+ */
+ rte->securityQuals = list_concat(securityQuals,
+ rte->securityQuals);
+
+ parsetree->withCheckOptions = list_concat(withCheckOptions,
+ parsetree->withCheckOptions);
+ }
+
+ /*
+ * Make sure the query is marked correctly if row level security
+ * applies, or if the new quals had sublinks.
+ */
+ if (hasRowSecurity)
+ parsetree->hasRowSecurity = true;
+ if (hasSubLinks)
+ parsetree->hasSubLinks = true;
+
+ heap_close(rel, NoLock);
+ }
+
return parsetree;
}
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 7669130..73a5794
*** a/src/backend/rewrite/rowsecurity.c
--- b/src/backend/rewrite/rowsecurity.c
***************
*** 58,64 ****
static List *pull_row_security_policies(CmdType cmd, Relation relation,
Oid user_id);
! static void process_policies(List *policies, int rt_index,
Expr **final_qual,
Expr **final_with_check_qual,
bool *hassublinks);
--- 58,64 ----
static List *pull_row_security_policies(CmdType cmd, Relation relation,
Oid user_id);
! static void process_policies(Query* root, List *policies, int rt_index,
Expr **final_qual,
Expr **final_with_check_qual,
bool *hassublinks);
*************** static bool check_role_for_policy(ArrayT
*** 73,88 ****
row_security_policy_hook_type row_security_policy_hook = NULL;
/*
! * Check the given RTE to see whether it's already had row security quals
! * expanded and, if not, prepend any row security rules from built-in or
! * plug-in sources to the securityQuals. The security quals are rewritten (for
! * view expansion, etc) before being added to the RTE.
*
! * Returns true if any quals were added. Note that quals may have been found
! * but not added if user rights make the user exempt from row security.
*/
! bool
! prepend_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index)
{
Expr *rowsec_expr = NULL;
Expr *rowsec_with_check_expr = NULL;
--- 73,89 ----
row_security_policy_hook_type row_security_policy_hook = NULL;
/*
! * Get any row security quals and check quals that should be applied to the
! * specified RTE.
*
! * In addition hasRowSecurity is set to true if row level security is enabled
! * (even if this RTE doesn't have any row security quals), and hasSubLinks is
! * set to true if any of the quals returned contain sublinks.
*/
! void
! get_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index,
! List **securityQuals, List **withCheckOptions,
! bool *hasRowSecurity, bool *hasSubLinks)
{
Expr *rowsec_expr = NULL;
Expr *rowsec_with_check_expr = NULL;
*************** prepend_row_security_policies(Query* roo
*** 96,103 ****
Oid user_id;
int sec_context;
int rls_status;
! bool defaultDeny = true;
! bool hassublinks = false;
/* This is just to get the security context */
GetUserIdAndSecContext(&user_id, &sec_context);
--- 97,109 ----
Oid user_id;
int sec_context;
int rls_status;
! bool defaultDeny = false;
!
! /* Defaults for the return values */
! *securityQuals = NIL;
! *withCheckOptions = NIL;
! *hasRowSecurity = false;
! *hasSubLinks = false;
/* This is just to get the security context */
GetUserIdAndSecContext(&user_id, &sec_context);
*************** prepend_row_security_policies(Query* roo
*** 113,126 ****
if (rte->relid < FirstNormalObjectId
|| rte->relkind != RELKIND_RELATION
|| (sec_context & SECURITY_ROW_LEVEL_DISABLED))
! return false;
/* Determine the state of RLS for this, pass checkAsUser explicitly */
rls_status = check_enable_rls(rte->relid, rte->checkAsUser, false);
/* If there is no RLS on this table at all, nothing to do */
if (rls_status == RLS_NONE)
! return false;
/*
* RLS_NONE_ENV means we are not doing any RLS now, but that may change
--- 119,132 ----
if (rte->relid < FirstNormalObjectId
|| rte->relkind != RELKIND_RELATION
|| (sec_context & SECURITY_ROW_LEVEL_DISABLED))
! return;
/* Determine the state of RLS for this, pass checkAsUser explicitly */
rls_status = check_enable_rls(rte->relid, rte->checkAsUser, false);
/* If there is no RLS on this table at all, nothing to do */
if (rls_status == RLS_NONE)
! return;
/*
* RLS_NONE_ENV means we are not doing any RLS now, but that may change
*************** prepend_row_security_policies(Query* roo
*** 134,151 ****
* be replanned if the environment changes (GUCs, role), but we
* are not adding anything here.
*/
! root->hasRowSecurity = true;
! return false;
}
- /*
- * We may end up getting called multiple times for the same RTE, so check
- * to make sure we aren't doing double-work.
- */
- if (rte->securityQuals != NIL)
- return false;
-
/* Grab the built-in policies which should be applied to this relation. */
rel = heap_open(rte->relid, NoLock);
--- 140,150 ----
* be replanned if the environment changes (GUCs, role), but we
* are not adding anything here.
*/
! *hasRowSecurity = true;
! return;
}
/* Grab the built-in policies which should be applied to this relation. */
rel = heap_open(rte->relid, NoLock);
*************** prepend_row_security_policies(Query* roo
*** 167,174 ****
defaultDeny = true;
/* Now that we have our policies, build the expressions from them. */
! process_policies(rowsec_policies, rt_index, &rowsec_expr,
! &rowsec_with_check_expr, &hassublinks);
/*
* Also, allow extensions to add their own policies.
--- 166,173 ----
defaultDeny = true;
/* Now that we have our policies, build the expressions from them. */
! process_policies(root, rowsec_policies, rt_index, &rowsec_expr,
! &rowsec_with_check_expr, hasSubLinks);
/*
* Also, allow extensions to add their own policies.
*************** prepend_row_security_policies(Query* roo
*** 191,204 ****
* enabled on the table, then we will ignore the internally-generated
* default-deny policy and use only the policies returned by the
* extension.
*/
if (row_security_policy_hook)
{
hook_policies = (*row_security_policy_hook)(root->commandType, rel);
/* Build the expression from any policies returned. */
! process_policies(hook_policies, rt_index, &hook_expr,
! &hook_with_check_expr, &hassublinks);
}
/*
--- 190,206 ----
* enabled on the table, then we will ignore the internally-generated
* default-deny policy and use only the policies returned by the
* extension.
+ *
+ * Any extension policies are applied after any internally-generated
+ * policies, so that extensions cannot bypass built-in RLS.
*/
if (row_security_policy_hook)
{
hook_policies = (*row_security_policy_hook)(root->commandType, rel);
/* Build the expression from any policies returned. */
! process_policies(root, hook_policies, rt_index, &hook_expr,
! &hook_with_check_expr, hasSubLinks);
}
/*
*************** prepend_row_security_policies(Query* roo
*** 230,236 ****
wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) rowsec_with_check_expr;
wco->cascaded = false;
! root->withCheckOptions = lcons(wco, root->withCheckOptions);
}
/*
--- 232,238 ----
wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) rowsec_with_check_expr;
wco->cascaded = false;
! *withCheckOptions = lappend(*withCheckOptions, wco);
}
/*
*************** prepend_row_security_policies(Query* roo
*** 244,250 ****
wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) hook_with_check_expr;
wco->cascaded = false;
! root->withCheckOptions = lcons(wco, root->withCheckOptions);
}
}
--- 246,252 ----
wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) hook_with_check_expr;
wco->cascaded = false;
! *withCheckOptions = lappend(*withCheckOptions, wco);
}
}
*************** prepend_row_security_policies(Query* roo
*** 254,264 ****
|| root->commandType == CMD_DELETE)
{
if (rowsec_expr)
! rte->securityQuals = lcons(rowsec_expr, rte->securityQuals);
if (hook_expr)
! rte->securityQuals = lcons(hook_expr,
! rte->securityQuals);
}
heap_close(rel, NoLock);
--- 256,265 ----
|| root->commandType == CMD_DELETE)
{
if (rowsec_expr)
! *securityQuals = lappend(*securityQuals, rowsec_expr);
if (hook_expr)
! *securityQuals = lappend(*securityQuals, hook_expr);
}
heap_close(rel, NoLock);
*************** prepend_row_security_policies(Query* roo
*** 267,283 ****
* Mark this query as having row security, so plancache can invalidate
* it when necessary (eg: role changes)
*/
! root->hasRowSecurity = true;
!
! /*
! * If we have sublinks added because of the policies being added to the
! * query, then set hasSubLinks on the Query to force subLinks to be
! * properly expanded.
! */
! root->hasSubLinks |= hassublinks;
! /* If we got this far, we must have added quals */
! return true;
}
/*
--- 268,276 ----
* Mark this query as having row security, so plancache can invalidate
* it when necessary (eg: role changes)
*/
! *hasRowSecurity = true;
! return;
}
/*
*************** pull_row_security_policies(CmdType cmd,
*** 383,389 ****
* qual_eval, with_check_eval, and hassublinks are output variables
*/
static void
! process_policies(List *policies, int rt_index, Expr **qual_eval,
Expr **with_check_eval, bool *hassublinks)
{
ListCell *item;
--- 376,382 ----
* qual_eval, with_check_eval, and hassublinks are output variables
*/
static void
! process_policies(Query* root, List *policies, int rt_index, Expr **qual_eval,
Expr **with_check_eval, bool *hassublinks)
{
ListCell *item;
*************** process_policies(List *policies, int rt_
*** 392,398 ****
/*
* Extract the USING and WITH CHECK quals from each of the policies
! * and add them to our lists.
*/
foreach(item, policies)
{
--- 385,392 ----
/*
* Extract the USING and WITH CHECK quals from each of the policies
! * and add them to our lists. We only want WITH CHECK quals if this
! * RTE is the query's result relation.
*/
foreach(item, policies)
{
*************** process_policies(List *policies, int rt_
*** 401,407 ****
if (policy->qual != NULL)
quals = lcons(copyObject(policy->qual), quals);
! if (policy->with_check_qual != NULL)
with_check_quals = lcons(copyObject(policy->with_check_qual),
with_check_quals);
--- 395,402 ----
if (policy->qual != NULL)
quals = lcons(copyObject(policy->qual), quals);
! if (policy->with_check_qual != NULL &&
! rt_index == root->resultRelation)
with_check_quals = lcons(copyObject(policy->with_check_qual),
with_check_quals);
*************** process_policies(List *policies, int rt_
*** 421,427 ****
* If we end up with only USING quals, then use those as
* WITH CHECK quals also.
*/
! if (with_check_quals == NIL)
with_check_quals = copyObject(quals);
/*
--- 416,422 ----
* If we end up with only USING quals, then use those as
* WITH CHECK quals also.
*/
! if (with_check_quals == NIL && rt_index == root->resultRelation)
with_check_quals = copyObject(quals);
/*
*************** process_policies(List *policies, int rt_
*** 450,457 ****
*/
if (list_length(with_check_quals) > 1)
*with_check_eval = makeBoolExpr(OR_EXPR, with_check_quals, -1);
! else
*with_check_eval = (Expr*) linitial(with_check_quals);
return;
}
--- 445,454 ----
*/
if (list_length(with_check_quals) > 1)
*with_check_eval = makeBoolExpr(OR_EXPR, with_check_quals, -1);
! else if (with_check_quals != NIL)
*with_check_eval = (Expr*) linitial(with_check_quals);
+ else
+ *with_check_eval = NULL;
return;
}
diff --git a/src/include/rewrite/rowsecurity.h b/src/include/rewrite/rowsecurity.h
new file mode 100644
index 8d19bfd..ac72427
*** a/src/include/rewrite/rowsecurity.h
--- b/src/include/rewrite/rowsecurity.h
*************** typedef List *(*row_security_policy_hook
*** 39,45 ****
extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
! extern bool prepend_row_security_policies(Query* root, RangeTblEntry* rte,
! int rt_index);
#endif /* ROWSECURITY_H */
--- 39,46 ----
extern PGDLLIMPORT row_security_policy_hook_type row_security_policy_hook;
! extern void get_row_security_policies(Query* root, RangeTblEntry* rte, int rt_index,
! List **securityQuals, List **withCheckOptions,
! bool *hasRowSecurity, bool *hasSubLinks);
#endif /* ROWSECURITY_H */
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index f41bef1..c864c12
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** ALTER TABLE t1 DROP COLUMN junk1; --
*** 466,474 ****
--- 466,476 ----
GRANT ALL ON t1 TO public;
COPY t1 FROM stdin WITH (oids);
CREATE TABLE t2 (c float) INHERITS (t1);
+ GRANT ALL ON t2 TO public;
COPY t2 FROM stdin WITH (oids);
CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
ALTER TABLE t3 INHERIT t1;
+ GRANT ALL ON t3 TO public;
COPY t3(a,b,c) FROM stdin WITH (oids);
CREATE POLICY p1 ON t1 FOR ALL TO PUBLIC USING (a % 2 = 0); -- be even number
CREATE POLICY p2 ON t2 FOR ALL TO PUBLIC USING (a % 2 = 1); -- be odd number
*************** NOTICE: f_leak => yyyyyy
*** 1114,1135 ****
302 | 2 | yyyyyy | (2,yyyyyy)
(5 rows)
RESET SESSION AUTHORIZATION;
SET row_security TO OFF;
! SELECT * FROM t1;
a | b
---+-------------
1 | aaa
- 3 | ccc
- 2 | bbbbbb_updt
- 4 | dddddd_updt
1 | abc
- 3 | cde
- 2 | bcdbcd
- 4 | defdef
1 | xxx
! 3 | zzz
2 | yyyyyy
(11 rows)
SET SESSION AUTHORIZATION rls_regress_user1;
--- 1116,1325 ----
302 | 2 | yyyyyy | (2,yyyyyy)
(5 rows)
+ -- updates with from clause
+ EXPLAIN (COSTS OFF) UPDATE t2 SET b=t2.b FROM t3
+ WHERE t2.a = 3 and t3.a = 2 AND f_leak(t2.b) AND f_leak(t3.b);
+ QUERY PLAN
+ ---------------------------------------------------------------
+ Update on t2 t2_1
+ -> Nested Loop
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> LockRows
+ -> Seq Scan on t2 t2_2
+ Filter: ((a = 3) AND ((a % 2) = 1))
+ -> Seq Scan on t3
+ Filter: (f_leak(b) AND (a = 2))
+ (9 rows)
+
+ UPDATE t2 SET b=t2.b FROM t3
+ WHERE t2.a = 3 and t3.a = 2 AND f_leak(t2.b) AND f_leak(t3.b);
+ NOTICE: f_leak => cde
+ NOTICE: f_leak => xxx
+ NOTICE: f_leak => zzz
+ NOTICE: f_leak => yyyyyy
+ EXPLAIN (COSTS OFF) UPDATE t1 SET b=t1.b FROM t2
+ WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
+ QUERY PLAN
+ ---------------------------------------------------------------
+ Update on t1 t1_3
+ -> Nested Loop
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> LockRows
+ -> Seq Scan on t1 t1_4
+ Filter: ((a = 3) AND ((a % 2) = 0))
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> Seq Scan on t2 t2_3
+ Filter: ((a = 3) AND ((a % 2) = 1))
+ -> Nested Loop
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> LockRows
+ -> Seq Scan on t2 t2_4
+ Filter: ((a = 3) AND ((a % 2) = 0))
+ -> Subquery Scan on t2_1
+ Filter: f_leak(t2_1.b)
+ -> Seq Scan on t2 t2_5
+ Filter: ((a = 3) AND ((a % 2) = 1))
+ -> Nested Loop
+ -> Subquery Scan on t1_2
+ Filter: f_leak(t1_2.b)
+ -> LockRows
+ -> Seq Scan on t3
+ Filter: ((a = 3) AND ((a % 2) = 0))
+ -> Subquery Scan on t2_2
+ Filter: f_leak(t2_2.b)
+ -> Seq Scan on t2 t2_6
+ Filter: ((a = 3) AND ((a % 2) = 1))
+ (31 rows)
+
+ UPDATE t1 SET b=t1.b FROM t2
+ WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
+ EXPLAIN (COSTS OFF) UPDATE t2 SET b=t2.b FROM t1
+ WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
+ QUERY PLAN
+ ---------------------------------------------------------------------
+ Update on t2 t2_1
+ -> Nested Loop
+ -> Subquery Scan on t2
+ Filter: f_leak(t2.b)
+ -> LockRows
+ -> Seq Scan on t2 t2_2
+ Filter: ((a = 3) AND ((a % 2) = 1))
+ -> Subquery Scan on t1
+ Filter: f_leak(t1.b)
+ -> Result
+ -> Append
+ -> Seq Scan on t1 t1_1
+ Filter: ((a = 3) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t2_3
+ Filter: ((a = 3) AND ((a % 2) = 0))
+ -> Seq Scan on t3
+ Filter: ((a = 3) AND ((a % 2) = 0))
+ (17 rows)
+
+ UPDATE t2 SET b=t2.b FROM t1
+ WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
+ NOTICE: f_leak => cde
+ -- updates with from clause self join
+ EXPLAIN (COSTS OFF) UPDATE t2 t2_1 SET b = t2_2.b FROM t2 t2_2
+ WHERE t2_1.a = 3 AND t2_2.a = t2_1.a AND t2_2.b = t2_1.b
+ AND f_leak(t2_1.b) AND f_leak(t2_2.b) RETURNING *, t2_1, t2_2;
+ QUERY PLAN
+ ---------------------------------------------------------------
+ Update on t2 t2_1_1
+ -> Nested Loop
+ Join Filter: (t2_1.b = t2_2.b)
+ -> Subquery Scan on t2_1
+ Filter: f_leak(t2_1.b)
+ -> LockRows
+ -> Seq Scan on t2 t2_1_2
+ Filter: ((a = 3) AND ((a % 2) = 1))
+ -> Subquery Scan on t2_2
+ Filter: f_leak(t2_2.b)
+ -> Seq Scan on t2 t2_2_1
+ Filter: ((a = 3) AND ((a % 2) = 1))
+ (12 rows)
+
+ UPDATE t2 t2_1 SET b = t2_2.b FROM t2 t2_2
+ WHERE t2_1.a = 3 AND t2_2.a = t2_1.a AND t2_2.b = t2_1.b
+ AND f_leak(t2_1.b) AND f_leak(t2_2.b) RETURNING *, t2_1, t2_2;
+ NOTICE: f_leak => cde
+ NOTICE: f_leak => cde
+ a | b | c | a | b | c | t2_1 | t2_2
+ ---+-----+-----+---+-----+-----+-------------+-------------
+ 3 | cde | 3.3 | 3 | cde | 3.3 | (3,cde,3.3) | (3,cde,3.3)
+ (1 row)
+
+ EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+ QUERY PLAN
+ ---------------------------------------------------------------
+ Update on t1 t1_1_3
+ -> Nested Loop
+ Join Filter: (t1_1.b = t1_2.b)
+ -> Subquery Scan on t1_1
+ Filter: f_leak(t1_1.b)
+ -> LockRows
+ -> Seq Scan on t1 t1_1_4
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2
+ Filter: f_leak(t1_2.b)
+ -> Append
+ -> Seq Scan on t1 t1_2_3
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_4
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_5
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Nested Loop
+ Join Filter: (t1_1_1.b = t1_2_1.b)
+ -> Subquery Scan on t1_1_1
+ Filter: f_leak(t1_1_1.b)
+ -> LockRows
+ -> Seq Scan on t2 t1_1_5
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2_1
+ Filter: f_leak(t1_2_1.b)
+ -> Append
+ -> Seq Scan on t1 t1_2_6
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_7
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_8
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Nested Loop
+ Join Filter: (t1_1_2.b = t1_2_2.b)
+ -> Subquery Scan on t1_1_2
+ Filter: f_leak(t1_1_2.b)
+ -> LockRows
+ -> Seq Scan on t3 t1_1_6
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Subquery Scan on t1_2_2
+ Filter: f_leak(t1_2_2.b)
+ -> Append
+ -> Seq Scan on t1 t1_2_9
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t2 t1_2_10
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ -> Seq Scan on t3 t1_2_11
+ Filter: ((a = 4) AND ((a % 2) = 0))
+ (49 rows)
+
+ UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => defdef
+ NOTICE: f_leak => defdef
+ NOTICE: f_leak => dddddd_updt
+ NOTICE: f_leak => defdef
+ a | b | a | b | t1_1 | t1_2
+ ---+-------------+---+-------------+-----------------+-----------------
+ 4 | dddddd_updt | 4 | dddddd_updt | (4,dddddd_updt) | (4,dddddd_updt)
+ 4 | defdef | 4 | defdef | (4,defdef) | (4,defdef)
+ (2 rows)
+
RESET SESSION AUTHORIZATION;
SET row_security TO OFF;
! SELECT * FROM t1 ORDER BY a,b;
a | b
---+-------------
1 | aaa
1 | abc
1 | xxx
! 2 | bbbbbb_updt
! 2 | bcdbcd
2 | yyyyyy
+ 3 | ccc
+ 3 | cde
+ 3 | zzz
+ 4 | dddddd_updt
+ 4 | defdef
(11 rows)
SET SESSION AUTHORIZATION rls_regress_user1;
*************** NOTICE: f_leak => yyyyyy
*** 1187,1192 ****
--- 1377,1479 ----
(3 rows)
--
+ -- S.b. view on top of Row-level security
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ CREATE TABLE b1 (a int, b text);
+ INSERT INTO b1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+ CREATE POLICY p1 ON b1 USING (a % 2 = 0);
+ ALTER TABLE b1 ENABLE ROW LEVEL SECURITY;
+ GRANT ALL ON b1 TO rls_regress_user1;
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ CREATE VIEW bv1 WITH (security_barrier) AS SELECT * FROM b1 WHERE a > 0 WITH CHECK OPTION;
+ GRANT ALL ON bv1 TO rls_regress_user2;
+ SET SESSION AUTHORIZATION rls_regress_user2;
+ EXPLAIN (COSTS OFF) SELECT * FROM bv1 WHERE f_leak(b);
+ QUERY PLAN
+ ---------------------------------------------
+ Subquery Scan on bv1
+ Filter: f_leak(bv1.b)
+ -> Seq Scan on b1
+ Filter: ((a > 0) AND ((a % 2) = 0))
+ (4 rows)
+
+ SELECT * FROM bv1 WHERE f_leak(b);
+ NOTICE: f_leak => c81e728d9d4c2f636f067f89cc14862c
+ NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ NOTICE: f_leak => c9f0f895fb98ab9159f51fd0297e236d
+ NOTICE: f_leak => d3d9446802a44259755d38e6d163e820
+ a | b
+ ----+----------------------------------
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 4 | a87ff679a2f3e71d9181a67b7542122c
+ 6 | 1679091c5a880faf6fb5e6087eb1b2dc
+ 8 | c9f0f895fb98ab9159f51fd0297e236d
+ 10 | d3d9446802a44259755d38e6d163e820
+ (5 rows)
+
+ INSERT INTO bv1 VALUES (-1, 'xxx'); -- should fail view WCO
+ ERROR: new row violates WITH CHECK OPTION for "b1"
+ INSERT INTO bv1 VALUES (11, 'xxx'); -- should fail RLS check
+ ERROR: new row violates WITH CHECK OPTION for "b1"
+ INSERT INTO bv1 VALUES (12, 'xxx'); -- ok
+ EXPLAIN (COSTS OFF) UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
+ QUERY PLAN
+ ---------------------------------------------------------------------------
+ Update on b1 b1_1
+ -> Subquery Scan on b1
+ Filter: f_leak(b1.b)
+ -> Subquery Scan on b1_2
+ -> LockRows
+ -> Seq Scan on b1 b1_3
+ Filter: ((a > 0) AND (a = 4) AND ((a % 2) = 0))
+ (7 rows)
+
+ UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
+ NOTICE: f_leak => a87ff679a2f3e71d9181a67b7542122c
+ EXPLAIN (COSTS OFF) DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
+ QUERY PLAN
+ ---------------------------------------------------------------------------
+ Delete on b1 b1_1
+ -> Subquery Scan on b1
+ Filter: f_leak(b1.b)
+ -> Subquery Scan on b1_2
+ -> LockRows
+ -> Seq Scan on b1 b1_3
+ Filter: ((a > 0) AND (a = 6) AND ((a % 2) = 0))
+ (7 rows)
+
+ DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
+ NOTICE: f_leak => 1679091c5a880faf6fb5e6087eb1b2dc
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ SELECT * FROM b1;
+ a | b
+ -----+----------------------------------
+ -10 | 1b0fd9efa5279c4203b7c70233f86dbf
+ -9 | 252e691406782824eec43d7eadc3d256
+ -8 | a8d2ec85eaf98407310b72eb73dda247
+ -7 | 74687a12d3915d3c4d83f1af7b3683d5
+ -6 | 596a3d04481816330f07e4f97510c28f
+ -5 | 47c1b025fa18ea96c33fbb6718688c0f
+ -4 | 0267aaf632e87a63288a08331f22c7c3
+ -3 | b3149ecea4628efd23d2f86e5a723472
+ -2 | 5d7b9adcbe1c629ec722529dd12e5129
+ -1 | 6bb61e3b7bce0931da574d19d1d82c88
+ 0 | cfcd208495d565ef66e7dff9f98764da
+ 1 | c4ca4238a0b923820dcc509a6f75849b
+ 2 | c81e728d9d4c2f636f067f89cc14862c
+ 3 | eccbc87e4b5ce2fe28308fd9f2a7baf3
+ 5 | e4da3b7fbbce2345d7772b0674a318d5
+ 7 | 8f14e45fceea167a5a36dedd4bea2543
+ 8 | c9f0f895fb98ab9159f51fd0297e236d
+ 9 | 45c48cce2e2d7fbdea1afc51c7c6ad26
+ 10 | d3d9446802a44259755d38e6d163e820
+ 12 | xxx
+ 4 | yyy
+ (21 rows)
+
+ --
-- ROLE/GROUP
--
SET SESSION AUTHORIZATION rls_regress_user0;
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index ed7adbf..4da5035
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
*************** COPY t1 FROM stdin WITH (oids);
*** 207,212 ****
--- 207,214 ----
\.
CREATE TABLE t2 (c float) INHERITS (t1);
+ GRANT ALL ON t2 TO public;
+
COPY t2 FROM stdin WITH (oids);
201 1 abc 1.1
202 2 bcd 2.2
*************** COPY t2 FROM stdin WITH (oids);
*** 216,221 ****
--- 218,225 ----
CREATE TABLE t3 (c text, b text, a int) WITH OIDS;
ALTER TABLE t3 INHERIT t1;
+ GRANT ALL ON t3 TO public;
+
COPY t3(a,b,c) FROM stdin WITH (oids);
301 1 xxx X
302 2 yyy Y
*************** UPDATE only t1 SET b = b WHERE f_leak(b)
*** 423,431 ****
UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
RESET SESSION AUTHORIZATION;
SET row_security TO OFF;
! SELECT * FROM t1;
SET SESSION AUTHORIZATION rls_regress_user1;
SET row_security TO ON;
--- 427,471 ----
UPDATE t1 SET b = b WHERE f_leak(b) RETURNING *;
UPDATE t1 SET b = b WHERE f_leak(b) RETURNING oid, *, t1;
+ -- updates with from clause
+ EXPLAIN (COSTS OFF) UPDATE t2 SET b=t2.b FROM t3
+ WHERE t2.a = 3 and t3.a = 2 AND f_leak(t2.b) AND f_leak(t3.b);
+
+ UPDATE t2 SET b=t2.b FROM t3
+ WHERE t2.a = 3 and t3.a = 2 AND f_leak(t2.b) AND f_leak(t3.b);
+
+ EXPLAIN (COSTS OFF) UPDATE t1 SET b=t1.b FROM t2
+ WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
+
+ UPDATE t1 SET b=t1.b FROM t2
+ WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
+
+ EXPLAIN (COSTS OFF) UPDATE t2 SET b=t2.b FROM t1
+ WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
+
+ UPDATE t2 SET b=t2.b FROM t1
+ WHERE t1.a = 3 and t2.a = 3 AND f_leak(t1.b) AND f_leak(t2.b);
+
+ -- updates with from clause self join
+ EXPLAIN (COSTS OFF) UPDATE t2 t2_1 SET b = t2_2.b FROM t2 t2_2
+ WHERE t2_1.a = 3 AND t2_2.a = t2_1.a AND t2_2.b = t2_1.b
+ AND f_leak(t2_1.b) AND f_leak(t2_2.b) RETURNING *, t2_1, t2_2;
+
+ UPDATE t2 t2_1 SET b = t2_2.b FROM t2 t2_2
+ WHERE t2_1.a = 3 AND t2_2.a = t2_1.a AND t2_2.b = t2_1.b
+ AND f_leak(t2_1.b) AND f_leak(t2_2.b) RETURNING *, t2_1, t2_2;
+
+ EXPLAIN (COSTS OFF) UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+
+ UPDATE t1 t1_1 SET b = t1_2.b FROM t1 t1_2
+ WHERE t1_1.a = 4 AND t1_2.a = t1_1.a AND t1_2.b = t1_1.b
+ AND f_leak(t1_1.b) AND f_leak(t1_2.b) RETURNING *, t1_1, t1_2;
+
RESET SESSION AUTHORIZATION;
SET row_security TO OFF;
! SELECT * FROM t1 ORDER BY a,b;
SET SESSION AUTHORIZATION rls_regress_user1;
SET row_security TO ON;
*************** DELETE FROM only t1 WHERE f_leak(b) RETU
*** 436,441 ****
--- 476,514 ----
DELETE FROM t1 WHERE f_leak(b) RETURNING oid, *, t1;
--
+ -- S.b. view on top of Row-level security
+ --
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ CREATE TABLE b1 (a int, b text);
+ INSERT INTO b1 (SELECT x, md5(x::text) FROM generate_series(-10,10) x);
+
+ CREATE POLICY p1 ON b1 USING (a % 2 = 0);
+ ALTER TABLE b1 ENABLE ROW LEVEL SECURITY;
+ GRANT ALL ON b1 TO rls_regress_user1;
+
+ SET SESSION AUTHORIZATION rls_regress_user1;
+ CREATE VIEW bv1 WITH (security_barrier) AS SELECT * FROM b1 WHERE a > 0 WITH CHECK OPTION;
+ GRANT ALL ON bv1 TO rls_regress_user2;
+
+ SET SESSION AUTHORIZATION rls_regress_user2;
+
+ EXPLAIN (COSTS OFF) SELECT * FROM bv1 WHERE f_leak(b);
+ SELECT * FROM bv1 WHERE f_leak(b);
+
+ INSERT INTO bv1 VALUES (-1, 'xxx'); -- should fail view WCO
+ INSERT INTO bv1 VALUES (11, 'xxx'); -- should fail RLS check
+ INSERT INTO bv1 VALUES (12, 'xxx'); -- ok
+
+ EXPLAIN (COSTS OFF) UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
+ UPDATE bv1 SET b = 'yyy' WHERE a = 4 AND f_leak(b);
+
+ EXPLAIN (COSTS OFF) DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
+ DELETE FROM bv1 WHERE a = 6 AND f_leak(b);
+
+ SET SESSION AUTHORIZATION rls_regress_user0;
+ SELECT * FROM b1;
+
+ --
-- ROLE/GROUP
--
SET SESSION AUTHORIZATION rls_regress_user0;
On 26 February 2015 at 09:50, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
On 26 February 2015 at 05:43, Stephen Frost <sfrost@snowman.net> wrote:
I wonder if there are some documentation updates which need to be done
for this also? I'm planning to look as I vauguely recall mentioning the
ordering of operations somewhere along the way.
I couldn't find any mention of the timing of the check in the existing
documentation, although it does vaguely imply that the check is done
before inserting any new data. There is an existing paragraph
describing the timing of USING conditions, so I added a new paragraph
immediately after that to explain when CHECK expressions are enforced,
since that seemed like the best place for it.
I also addressed the bitrot from the column-priv leak patch. Would be
great to have you take a look at the latest and let me know if you have
any further comments or suggestions.
I looked it over again, and I'm happy with these updates, except there
was a missing "break" in the switch statement in
ExecWithCheckOptions().
Here's an updated patch.
Regards,
Dean
Attachments:
rls-timing-v3.patchtext/x-diff; charset=US-ASCII; name=rls-timing-v3.patchDownload
diff --git a/doc/src/sgml/ref/create_policy.sgml b/doc/src/sgml/ref/create_policy.sgml
new file mode 100644
index 868a6c1..49eaadc
*** a/doc/src/sgml/ref/create_policy.sgml
--- b/doc/src/sgml/ref/create_policy.sgml
*************** CREATE POLICY <replaceable class="parame
*** 61,66 ****
--- 61,74 ----
</para>
<para>
+ For INSERT and UPDATE queries, WITH CHECK expressions are enforced after
+ BEFORE triggers are fired, and before any data modifications are made.
+ Thus a BEFORE ROW trigger may modify the data to be inserted, affecting
+ the result of the security policy check. WITH CHECK expressions are
+ enforced before any other constraints.
+ </para>
+
+ <para>
Policy names are per-table, therefore one policy name can be used for many
different tables and have a definition for each table which is appropriate to
that table.
diff --git a/src/backend/executor/execMain.c b/src/backend/executor/execMain.c
new file mode 100644
index 33b172b..8dfb952
*** a/src/backend/executor/execMain.c
--- b/src/backend/executor/execMain.c
*************** ExecConstraints(ResultRelInfo *resultRel
*** 1667,1675 ****
/*
* ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs
*/
void
! ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate)
{
Relation rel = resultRelInfo->ri_RelationDesc;
--- 1667,1676 ----
/*
* ExecWithCheckOptions -- check that tuple satisfies any WITH CHECK OPTIONs
+ * of the specified kind.
*/
void
! ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate)
{
Relation rel = resultRelInfo->ri_RelationDesc;
*************** ExecWithCheckOptions(ResultRelInfo *resu
*** 1694,1699 ****
--- 1695,1703 ----
WithCheckOption *wco = (WithCheckOption *) lfirst(l1);
ExprState *wcoExpr = (ExprState *) lfirst(l2);
+ if (wco->kind != kind)
+ continue;
+
/*
* WITH CHECK OPTION checks are intended to ensure that the new tuple
* is visible (in the case of a view) or that it passes the
*************** ExecWithCheckOptions(ResultRelInfo *resu
*** 1715,1726 ****
modifiedCols,
64);
! ereport(ERROR,
! (errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION),
! errmsg("new row violates WITH CHECK OPTION for \"%s\"",
! wco->viewname),
! val_desc ? errdetail("Failing row contains %s.", val_desc) :
! 0));
}
}
}
--- 1719,1747 ----
modifiedCols,
64);
! switch (wco->kind)
! {
! case WCO_VIEW_CHECK:
! ereport(ERROR,
! (errcode(ERRCODE_WITH_CHECK_OPTION_VIOLATION),
! errmsg("new row violates WITH CHECK OPTION for \"%s\"",
! wco->relname),
! val_desc ? errdetail("Failing row contains %s.",
! val_desc) : 0));
! break;
! case WCO_RLS_INSERT_CHECK:
! case WCO_RLS_UPDATE_CHECK:
! ereport(ERROR,
! (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
! errmsg("new row violates row level security policy for \"%s\"",
! wco->relname),
! val_desc ? errdetail("Failing row contains %s.",
! val_desc) : 0));
! break;
! default:
! elog(ERROR, "unrecognized WCO kind: %u", wco->kind);
! break;
! }
}
}
}
diff --git a/src/backend/executor/nodeModifyTable.c b/src/backend/executor/nodeModifyTable.c
new file mode 100644
index f96fb24..be879a4
*** a/src/backend/executor/nodeModifyTable.c
--- b/src/backend/executor/nodeModifyTable.c
*************** ExecInsert(TupleTableSlot *slot,
*** 253,258 ****
--- 253,265 ----
tuple->t_tableOid = RelationGetRelid(resultRelationDesc);
/*
+ * Check any RLS INSERT WITH CHECK policies
+ */
+ if (resultRelInfo->ri_WithCheckOptions != NIL)
+ ExecWithCheckOptions(WCO_RLS_INSERT_CHECK,
+ resultRelInfo, slot, estate);
+
+ /*
* Check the constraints of the tuple
*/
if (resultRelationDesc->rd_att->constr)
*************** ExecInsert(TupleTableSlot *slot,
*** 287,295 ****
list_free(recheckIndexes);
! /* Check any WITH CHECK OPTION constraints */
if (resultRelInfo->ri_WithCheckOptions != NIL)
! ExecWithCheckOptions(resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
--- 294,302 ----
list_free(recheckIndexes);
! /* Check any WITH CHECK OPTION constraints from parent views */
if (resultRelInfo->ri_WithCheckOptions != NIL)
! ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
*************** ExecUpdate(ItemPointer tupleid,
*** 653,667 ****
tuple->t_tableOid = RelationGetRelid(resultRelationDesc);
/*
! * Check the constraints of the tuple
*
* If we generate a new candidate tuple after EvalPlanQual testing, we
! * must loop back here and recheck constraints. (We don't need to
! * redo triggers, however. If there are any BEFORE triggers then
! * trigger.c will have done heap_lock_tuple to lock the correct tuple,
! * so there's no need to do them again.)
*/
lreplace:;
if (resultRelationDesc->rd_att->constr)
ExecConstraints(resultRelInfo, slot, estate);
--- 660,681 ----
tuple->t_tableOid = RelationGetRelid(resultRelationDesc);
/*
! * Check any RLS UPDATE WITH CHECK policies
*
* If we generate a new candidate tuple after EvalPlanQual testing, we
! * must loop back here and recheck any RLS policies and constraints.
! * (We don't need to redo triggers, however. If there are any BEFORE
! * triggers then trigger.c will have done heap_lock_tuple to lock the
! * correct tuple, so there's no need to do them again.)
*/
lreplace:;
+ if (resultRelInfo->ri_WithCheckOptions != NIL)
+ ExecWithCheckOptions(WCO_RLS_UPDATE_CHECK,
+ resultRelInfo, slot, estate);
+
+ /*
+ * Check the constraints of the tuple
+ */
if (resultRelationDesc->rd_att->constr)
ExecConstraints(resultRelInfo, slot, estate);
*************** lreplace:;
*** 780,788 ****
list_free(recheckIndexes);
! /* Check any WITH CHECK OPTION constraints */
if (resultRelInfo->ri_WithCheckOptions != NIL)
! ExecWithCheckOptions(resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
--- 794,802 ----
list_free(recheckIndexes);
! /* Check any WITH CHECK OPTION constraints from parent views */
if (resultRelInfo->ri_WithCheckOptions != NIL)
! ExecWithCheckOptions(WCO_VIEW_CHECK, resultRelInfo, slot, estate);
/* Process RETURNING if present */
if (resultRelInfo->ri_projectReturning)
diff --git a/src/backend/nodes/copyfuncs.c b/src/backend/nodes/copyfuncs.c
new file mode 100644
index 9fe8008..d49585a
*** a/src/backend/nodes/copyfuncs.c
--- b/src/backend/nodes/copyfuncs.c
*************** _copyWithCheckOption(const WithCheckOpti
*** 2059,2065 ****
{
WithCheckOption *newnode = makeNode(WithCheckOption);
! COPY_STRING_FIELD(viewname);
COPY_NODE_FIELD(qual);
COPY_SCALAR_FIELD(cascaded);
--- 2059,2066 ----
{
WithCheckOption *newnode = makeNode(WithCheckOption);
! COPY_SCALAR_FIELD(kind);
! COPY_STRING_FIELD(relname);
COPY_NODE_FIELD(qual);
COPY_SCALAR_FIELD(cascaded);
diff --git a/src/backend/nodes/equalfuncs.c b/src/backend/nodes/equalfuncs.c
new file mode 100644
index fe509b0..2cf1899
*** a/src/backend/nodes/equalfuncs.c
--- b/src/backend/nodes/equalfuncs.c
*************** _equalRangeTblFunction(const RangeTblFun
*** 2371,2377 ****
static bool
_equalWithCheckOption(const WithCheckOption *a, const WithCheckOption *b)
{
! COMPARE_STRING_FIELD(viewname);
COMPARE_NODE_FIELD(qual);
COMPARE_SCALAR_FIELD(cascaded);
--- 2371,2378 ----
static bool
_equalWithCheckOption(const WithCheckOption *a, const WithCheckOption *b)
{
! COMPARE_SCALAR_FIELD(kind);
! COMPARE_STRING_FIELD(relname);
COMPARE_NODE_FIELD(qual);
COMPARE_SCALAR_FIELD(cascaded);
diff --git a/src/backend/nodes/outfuncs.c b/src/backend/nodes/outfuncs.c
new file mode 100644
index 775f482..f86086c
*** a/src/backend/nodes/outfuncs.c
--- b/src/backend/nodes/outfuncs.c
*************** _outWithCheckOption(StringInfo str, cons
*** 2326,2332 ****
{
WRITE_NODE_TYPE("WITHCHECKOPTION");
! WRITE_STRING_FIELD(viewname);
WRITE_NODE_FIELD(qual);
WRITE_BOOL_FIELD(cascaded);
}
--- 2326,2333 ----
{
WRITE_NODE_TYPE("WITHCHECKOPTION");
! WRITE_ENUM_FIELD(kind, WCOKind);
! WRITE_STRING_FIELD(relname);
WRITE_NODE_FIELD(qual);
WRITE_BOOL_FIELD(cascaded);
}
diff --git a/src/backend/nodes/readfuncs.c b/src/backend/nodes/readfuncs.c
new file mode 100644
index 563209c..b0cd95d
*** a/src/backend/nodes/readfuncs.c
--- b/src/backend/nodes/readfuncs.c
*************** _readWithCheckOption(void)
*** 266,272 ****
{
READ_LOCALS(WithCheckOption);
! READ_STRING_FIELD(viewname);
READ_NODE_FIELD(qual);
READ_BOOL_FIELD(cascaded);
--- 266,273 ----
{
READ_LOCALS(WithCheckOption);
! READ_ENUM_FIELD(kind, WCOKind);
! READ_STRING_FIELD(relname);
READ_NODE_FIELD(qual);
READ_BOOL_FIELD(cascaded);
diff --git a/src/backend/rewrite/rewriteHandler.c b/src/backend/rewrite/rewriteHandler.c
new file mode 100644
index 9d2c280..9ea770b
*** a/src/backend/rewrite/rewriteHandler.c
--- b/src/backend/rewrite/rewriteHandler.c
*************** rewriteTargetView(Query *parsetree, Rela
*** 2910,2916 ****
WithCheckOption *wco;
wco = makeNode(WithCheckOption);
! wco->viewname = pstrdup(RelationGetRelationName(view));
wco->qual = NULL;
wco->cascaded = cascaded;
--- 2910,2917 ----
WithCheckOption *wco;
wco = makeNode(WithCheckOption);
! wco->kind = WCO_VIEW_CHECK;
! wco->relname = pstrdup(RelationGetRelationName(view));
wco->qual = NULL;
wco->cascaded = cascaded;
diff --git a/src/backend/rewrite/rowsecurity.c b/src/backend/rewrite/rowsecurity.c
new file mode 100644
index 7669130..687c7f6
*** a/src/backend/rewrite/rowsecurity.c
--- b/src/backend/rewrite/rowsecurity.c
*************** prepend_row_security_policies(Query* roo
*** 227,233 ****
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
! wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) rowsec_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
--- 227,235 ----
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
! wco->kind = root->commandType == CMD_INSERT ?
! WCO_RLS_INSERT_CHECK : WCO_RLS_UPDATE_CHECK;
! wco->relname = RelationGetRelationName(rel);
wco->qual = (Node *) rowsec_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
*************** prepend_row_security_policies(Query* roo
*** 241,247 ****
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
! wco->viewname = RelationGetRelationName(rel);
wco->qual = (Node *) hook_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
--- 243,251 ----
WithCheckOption *wco;
wco = (WithCheckOption *) makeNode(WithCheckOption);
! wco->kind = root->commandType == CMD_INSERT ?
! WCO_RLS_INSERT_CHECK : WCO_RLS_UPDATE_CHECK;
! wco->relname = RelationGetRelationName(rel);
wco->qual = (Node *) hook_with_check_expr;
wco->cascaded = false;
root->withCheckOptions = lcons(wco, root->withCheckOptions);
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
new file mode 100644
index 40fde83..cb4f158
*** a/src/include/executor/executor.h
--- b/src/include/executor/executor.h
*************** extern ResultRelInfo *ExecGetTriggerResu
*** 194,200 ****
extern bool ExecContextForcesOids(PlanState *planstate, bool *hasoids);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
! extern void ExecWithCheckOptions(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern ExecRowMark *ExecFindRowMark(EState *estate, Index rti);
extern ExecAuxRowMark *ExecBuildAuxRowMark(ExecRowMark *erm, List *targetlist);
--- 194,200 ----
extern bool ExecContextForcesOids(PlanState *planstate, bool *hasoids);
extern void ExecConstraints(ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
! extern void ExecWithCheckOptions(WCOKind kind, ResultRelInfo *resultRelInfo,
TupleTableSlot *slot, EState *estate);
extern ExecRowMark *ExecFindRowMark(EState *estate, Index rti);
extern ExecAuxRowMark *ExecBuildAuxRowMark(ExecRowMark *erm, List *targetlist);
diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h
new file mode 100644
index 59b17f3..ee36a64
*** a/src/include/nodes/execnodes.h
--- b/src/include/nodes/execnodes.h
*************** typedef struct JunkFilter
*** 303,309 ****
* TrigInstrument optional runtime measurements for triggers
* FdwRoutine FDW callback functions, if foreign table
* FdwState available to save private state of FDW
! * WithCheckOptions list of WithCheckOption's for views
* WithCheckOptionExprs list of WithCheckOption expr states
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
--- 303,309 ----
* TrigInstrument optional runtime measurements for triggers
* FdwRoutine FDW callback functions, if foreign table
* FdwState available to save private state of FDW
! * WithCheckOptions list of WithCheckOption's to be checked
* WithCheckOptionExprs list of WithCheckOption expr states
* ConstraintExprs array of constraint-checking expr states
* junkFilter for removing junk attributes from tuples
diff --git a/src/include/nodes/parsenodes.h b/src/include/nodes/parsenodes.h
new file mode 100644
index ac13302..2c3bbce
*** a/src/include/nodes/parsenodes.h
--- b/src/include/nodes/parsenodes.h
*************** typedef struct RangeTblFunction
*** 861,874 ****
/*
* WithCheckOption -
* representation of WITH CHECK OPTION checks to be applied to new tuples
! * when inserting/updating an auto-updatable view.
*/
typedef struct WithCheckOption
{
NodeTag type;
! char *viewname; /* name of view that specified the WCO */
Node *qual; /* constraint qual to check */
! bool cascaded; /* true = WITH CASCADED CHECK OPTION */
} WithCheckOption;
/*
--- 861,883 ----
/*
* WithCheckOption -
* representation of WITH CHECK OPTION checks to be applied to new tuples
! * when inserting/updating an auto-updatable view, or RLS WITH CHECK
! * policies to be applied when inserting/updating a relation with RLS.
*/
+ typedef enum WCOKind
+ {
+ WCO_VIEW_CHECK, /* WCO on an auto-updatable view */
+ WCO_RLS_INSERT_CHECK, /* RLS INSERT WITH CHECK policy */
+ WCO_RLS_UPDATE_CHECK /* RLS UPDATE WITH CHECK policy */
+ } WCOKind;
+
typedef struct WithCheckOption
{
NodeTag type;
! WCOKind kind; /* kind of WCO */
! char *relname; /* name of relation that specified the WCO */
Node *qual; /* constraint qual to check */
! bool cascaded; /* true for a cascaded WCO on a view */
} WithCheckOption;
/*
diff --git a/src/test/regress/expected/rowsecurity.out b/src/test/regress/expected/rowsecurity.out
new file mode 100644
index f41bef1..d318bf9
*** a/src/test/regress/expected/rowsecurity.out
--- b/src/test/regress/expected/rowsecurity.out
*************** SELECT * FROM document WHERE did = 8; --
*** 300,305 ****
--- 300,310 ----
-----+-----+--------+---------+--------
(0 rows)
+ -- RLS policies are checked before constraints
+ INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user2', 'my third manga'); -- Should fail with RLS check violation, not duplicate key violation
+ ERROR: new row violates row level security policy for "document"
+ UPDATE document SET did = 8, dauthor = 'rls_regress_user2' WHERE did = 5; -- Should fail with RLS check violation, not duplicate key violation
+ ERROR: new row violates row level security policy for "document"
-- database superuser does bypass RLS policy when enabled
RESET SESSION AUTHORIZATION;
SET row_security TO ON;
*************** EXPLAIN (COSTS OFF) WITH cte1 AS (SELECT
*** 1689,1695 ****
(6 rows)
WITH cte1 AS (UPDATE t1 SET a = a + 1 RETURNING *) SELECT * FROM cte1; --fail
! ERROR: new row violates WITH CHECK OPTION for "t1"
WITH cte1 AS (UPDATE t1 SET a = a RETURNING *) SELECT * FROM cte1; --ok
a | b
----+----------------------------------
--- 1694,1700 ----
(6 rows)
WITH cte1 AS (UPDATE t1 SET a = a + 1 RETURNING *) SELECT * FROM cte1; --fail
! ERROR: new row violates row level security policy for "t1"
WITH cte1 AS (UPDATE t1 SET a = a RETURNING *) SELECT * FROM cte1; --ok
a | b
----+----------------------------------
*************** WITH cte1 AS (UPDATE t1 SET a = a RETURN
*** 1707,1713 ****
(11 rows)
WITH cte1 AS (INSERT INTO t1 VALUES (21, 'Fail') RETURNING *) SELECT * FROM cte1; --fail
! ERROR: new row violates WITH CHECK OPTION for "t1"
WITH cte1 AS (INSERT INTO t1 VALUES (20, 'Success') RETURNING *) SELECT * FROM cte1; --ok
a | b
----+---------
--- 1712,1718 ----
(11 rows)
WITH cte1 AS (INSERT INTO t1 VALUES (21, 'Fail') RETURNING *) SELECT * FROM cte1; --fail
! ERROR: new row violates row level security policy for "t1"
WITH cte1 AS (INSERT INTO t1 VALUES (20, 'Success') RETURNING *) SELECT * FROM cte1; --ok
a | b
----+---------
diff --git a/src/test/regress/sql/rowsecurity.sql b/src/test/regress/sql/rowsecurity.sql
new file mode 100644
index ed7adbf..a9f4b83
*** a/src/test/regress/sql/rowsecurity.sql
--- b/src/test/regress/sql/rowsecurity.sql
*************** SET SESSION AUTHORIZATION rls_regress_us
*** 146,151 ****
--- 146,155 ----
INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user1', 'my third manga'); -- Must fail with unique violation, revealing presence of did we can't see
SELECT * FROM document WHERE did = 8; -- and confirm we can't see it
+ -- RLS policies are checked before constraints
+ INSERT INTO document VALUES (8, 44, 1, 'rls_regress_user2', 'my third manga'); -- Should fail with RLS check violation, not duplicate key violation
+ UPDATE document SET did = 8, dauthor = 'rls_regress_user2' WHERE did = 5; -- Should fail with RLS check violation, not duplicate key violation
+
-- database superuser does bypass RLS policy when enabled
RESET SESSION AUTHORIZATION;
SET row_security TO ON;
* Dean Rasheed (dean.a.rasheed@gmail.com) wrote:
On 26 February 2015 at 09:50, Dean Rasheed <dean.a.rasheed@gmail.com> wrote:
On 26 February 2015 at 05:43, Stephen Frost <sfrost@snowman.net> wrote:
I wonder if there are some documentation updates which need to be done
for this also? I'm planning to look as I vauguely recall mentioning the
ordering of operations somewhere along the way.I couldn't find any mention of the timing of the check in the existing
documentation, although it does vaguely imply that the check is done
before inserting any new data. There is an existing paragraph
describing the timing of USING conditions, so I added a new paragraph
immediately after that to explain when CHECK expressions are enforced,
since that seemed like the best place for it.
Ah, ok, I must have been thinking about the USING discussion. Thanks
for adding the paragraph about the CHECK timing.
I also addressed the bitrot from the column-priv leak patch. Would be
great to have you take a look at the latest and let me know if you have
any further comments or suggestions.I looked it over again, and I'm happy with these updates, except there
was a missing "break" in the switch statement in
ExecWithCheckOptions().
Oh, thanks for catching that!
Here's an updated patch.
Excellent, will review.
Thanks again!
Stephen