Обсуждение: Proposal: Introduce row-level security templates

Поиск
Список
Период
Сортировка

Proposal: Introduce row-level security templates

От
Aadhav Vignesh
Дата:
Hi,

I'm looking at an open proposal to introduce row-level security policy templates [0], and I have been making some progress on it.

The proposal aims to introduce templates for RLS policies, where the idea is to allow users to define policies as a template, and apply it to multiple tables. The proposed benefit is that there is reduction in management overhead as there are situations where policies are similar across multiple tables.

However, ever since I started working on this proposal, I noticed that there are a few open questions I wanted to ask to existing contributors regarding how this functionality should be exposed.

There are two ways to address this proposal:

1. Introduction of new keywords/statements

Expected usage:

-- create template
CREATE RLS TEMPLATE rls_template
USING (user_id = current_user)
WITH (SELECT);

-- attach templates to tables
ALTER TABLE employees
ATTACH RLS TEMPLATE rls_template;

ALTER TABLE customers
ATTACH RLS TEMPLATE rls_template;

-- alter template
ALTER RLS TEMPLATE rls_template
WITH (SELECT, UPDATE);

This option is non-intrusive, and can possibly operate in complete isolation from existing row-level security logic, however, this also brings the difficulty of introducing divergent behavior between normal RLS policy creation and template creation as both of them would have a different SQL syntax. This is undesired. This also requires users to learn the newly-introduced syntax.

2. Modifying existing CREATE POLICY logic (or introduce a new CREATE POLICY TEMPLATE statement)

We could consider adding a new statement called CREATE POLICY TEMPLATE with the similar options but without the table name:

CREATE POLICY TEMPLATE name
    [ AS { PERMISSIVE | RESTRICTIVE } ]
    [ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ]
    [ TO { role_name | PUBLIC | CURRENT_ROLE | CURRENT_USER | SESSION_USER } [, ...] ]
    [ USING ( using_expression ) ]
    [ WITH CHECK ( check_expression ) ]

The major challenge here is the construction of the qualifiers for the policy, as the entire process [1] relies on a table ID, however, we don’t have access to any table names in this statement.

I also find the aspect of constructing qualifiers directly from the plain-text state less ideal, and I honestly have no clue if this is possible.

or, we could integrate it in CREATE POLICY as an option (but in this case, the table name is required, rendering the template creation table-dependent):

CREATE POLICY name ON table_name
    [ AS { PERMISSIVE | RESTRICTIVE } ]
    [ FOR { ALL | SELECT | INSERT | UPDATE | DELETE } ]
    [ TO { role_name | PUBLIC | CURRENT_ROLE | CURRENT_USER | SESSION_USER } [, ...] ]
    [ USING ( using_expression ) ]
    [ WITH CHECK ( check_expression ) ]
    [ TEMPLATE template_name ]

Would love to hear any thoughts on the preferred way to introduce this functionality.

Apologies for any mistakes I might have made in the above statements, I'm fairly new to pgsql-hackers (this is my first post here!), and this is my first time taking a look at existing RLS logic, so I might be wrong on the interpretation of qualifier expr constructions.

Regards,
Aadhav

[0]: https://wiki.postgresql.org/wiki/GSoC_2024#Row-level_security_templates
[1]: https://github.com/postgres/postgres/blob/bb5604ba9e53e3a0fb9967f960e36cff4d36b0ab/src/backend/commands/policy.c#L633-L659

Re: Proposal: Introduce row-level security templates

От
Stojan Dimitrovski
Дата:
Hi Aadhav Vignesh,

Interestingly, Alexander asked for ideas for GSoC projects at Supabase
(I'm on the Auth team), and I proposed the RLS templates idea. As you
already pointed out, the idea comes out of us seeing how RLS policies
are used in real-life and the pain points associated with managing a
non-trivial number of them.

I am not a code contributor to Postgres, so please excuse my lack of
knowledge on how internals work or what's best to implement.

From your original message:
https://www.postgresql.org/message-id/CAMuaUMJ10_4CDxtHOTHbp%2BY%2Bh2uR2wxcVtJPbBvp9A9Njs5kUA%40mail.gmail.com

In my opinion, as a user of the policies, I prefer the direction of
option (2) with `CREATE POLICY TEMPLATE`. It’s much closer to the
existing `CREATE POLICY` statements, a lot of the internals could
probably be reused, which would make the project move along faster.

I do think there should be the option to attach it immediately to
tables, something like `CREATE POLICY TEMPLATE <name> ATTACH TO
<tables> AS ...` but it’s not a deal-breaker for me.

I don’t really know enough about how this actually works internally,
but on this point:

> The major challenge here is the construction of the qualifiers for the
> policy, as the entire process [1] relies on a table ID, however, we don’t
> have access to any table names in this statement.
>
> I also find the aspect of constructing qualifiers directly from the
> plain-text state less ideal, and I honestly have no clue if this is
> possible.

I would say that because templates are not active until they’re
attached, the logic for “validating” the query part should come when
the template is being attached on the table. So a non-qualifying
`USING` clause would error out on the `ATTACH` command. Similarly,
altering the policy template would force validation on the clause over
the already attached tables to the policy.

I would also suggest looking into the ability to attach a template to
a schema. There are some obvious benefits of this — creating a table
in the schema automatically gets the correct RLS behavior without
having to explicitly attach a template or policies, making it
secure-by-default. There is some interesting behavior of this as well
which _may_ be beneficial. For example, if you create a schema-wide
template, say `user_id = current_user`, and you create a table that
doesn’t have a `user_id` column — the creation would fail. This is in
many practical situations beneficial, as it lessens the likelihood of
creating a table that can’t be properly secured in that application
context. On the flip side, it’s probably going to be challenging to
implement this and there may be other implications too (like adding it
in `ALTER TABLE`, etc).

Some fringe ideas that I also think are worth exploring are:

1. Make the `ON table_name` part of the `CREATE POLICY` statement
optional, which would create the “template.” This would require
altering the internals of the policy-table relationship to support 0,
1, 2, … tables instead of the current 1. Again I have no idea how this
is implemented internally, but it could be a fairly simple change
without having to introduce new concepts, objects, and commands.
2. Have templates only as the object that enables the one-to-many
relationship between a policy and table. For example, you could create
a policy like `CREATE POLICY owned_by_user ON table ...`, and then you
could do something like `CREATE POLICY TEMPLATE owned_by_user AS
POLICY schema.table.owned_by_user ATTACH TO tables...`. So essentially
the “template object” just references an already existing policy
attached to a table, and it allows you to attach it to other tables
too.

Let me know what you think!

(Apology if this email is not threaded in your email client, I'm
writing it after subscribing to the list, which means the Reply option
is not available.)



Re: Proposal: Introduce row-level security templates

От
Aadhav Vignesh
Дата:
Hi Stojan,
 
I do think there should be the option to attach it immediately to
tables, something like `CREATE POLICY TEMPLATE <name> ATTACH TO
<tables> AS ...` but it’s not a deal-breaker for me.

I would say that because templates are not active until they’re
attached, the logic for “validating” the query part should come when
the template is being attached on the table. So a non-qualifying
`USING` clause would error out on the `ATTACH` command. Similarly,
altering the policy template would force validation on the clause over
the already attached tables to the policy.

That's an interesting idea. I believe that it could be achieved with some modification, but I'm thinking about the preferred behavior on attaching templates to tables: do we want to error out instantly if we encounter a case where the qualifier isn't applicable to a particular table, or do we let the template get attached to other tables silently?

I would also suggest looking into the ability to attach a template to
a schema. There are some obvious benefits of this — creating a table
in the schema automatically gets the correct RLS behavior without
having to explicitly attach a template or policies, making it
secure-by-default. There is some interesting behavior of this as well
which _may_ be beneficial. For example, if you create a schema-wide
template, say `user_id = current_user`, and you create a table that
doesn’t have a `user_id` column — the creation would fail. This is in
many practical situations beneficial, as it lessens the likelihood of
creating a table that can’t be properly secured in that application
context.

I like this idea, as a schema-level template would be beneficial in some cases, but this change would introduce more rigidity or disruptions. For example, if a table needs to be created in a schema without any restrictions on access, it would fail as the schema now enforces RLS checks on table creation. I do feel that this proposal has its benefits, but this also introduces a binary/dichotomous decision: either you enable RLS on each table in the schema, or you don't.

One way to solve this is to manually modify each table that doesn't need RLS checks by disabling it: `ALTER TABLE <table_name> DISABLE ROW LEVEL SECURITY;`, but I'm not sure if this is ideal, as this introduces more operational/administration complexity.

1. Make the `ON table_name` part of the `CREATE POLICY` statement
optional, which would create the “template.” This would require
altering the internals of the policy-table relationship to support 0,
1, 2, … tables instead of the current 1. Again I have no idea how this
is implemented internally, but it could be a fairly simple change
without having to introduce new concepts, objects, and commands.

Interesting, what does `0` entail in this case? Current behavior is to enforce a policy on a table, if that's made optional, would that mean if no tables are specified in `CREATE POLICY`, would it be considered as a schema-level policy?

Wouldn't it be better if we had a way to explicitly specify when a schema-level policy is required to be created? With the proposed behavior, there might be cases where users might accidentally trigger/enforce a schema-level policy if they failed to specify any table names.

2. Have templates only as the object that enables the one-to-many
relationship between a policy and table. For example, you could create
a policy like `CREATE POLICY owned_by_user ON table ...`, and then you
could do something like `CREATE POLICY TEMPLATE owned_by_user AS
POLICY schema.table.owned_by_user ATTACH TO tables...`. So essentially
the “template object” just references an already existing policy
attached to a table, and it allows you to attach it to other tables
too.

I believe that's possible by utilizing the system catalogs, and finding references to the policy as you mentioned, but it's highly sensitive to cases where the original policy is deleted, as now you can't refer to the original policy. There can be modifications made to `DROP POLICY` to also remove the top-level/parent template when the original policy is deleted, but I'm not sure if that behavior is preferred.

Thanks,
Aadhav