Re: CREATEROLE and role ownership hierarchies
Greetings,
* Robert Haas (robertmhaas@gmail.com) wrote:
1. Don't allow a CREATEROLE user to give out membership in groups
which that user does not possess. Leaving aside the details of any
previously-proposed patches and just speaking theoretically, how can
this be implemented? I can think of a few ideas. We could (1A) just
change CREATEROLE to work that way, but IIUC that would break the use
case you outline here, so I guess that's off the table unless I am
misunderstanding the situation. We could also (1B) add a second role
attribute with a different name, like, err, CREATEROLEWEAKLY, that
behaves in that way, leaving the existing one untouched. But we could
also take it a lot further, because someone might want to let an
account hand out a set of privileges which corresponds neither to the
privileges of that account nor to the full set of available
privileges. That leads to another idea: (1C) implement an in-database
system that lets you specify which privileges an account has, and,
separately, which ones it can assign to others. I am skeptical of that
idea because it seems really, really complicated, not only from an
implementation standpoint but even just from a user-experience
standpoint. Suppose user 'userbot' has rights to grant a suitable set
of groups to the new users that it creates -- but then someone creates
a new group. Should that also be added to the things 'userbot' can
grant or not? What if we have 'userbot1' through 'userbot6' and each
of them can grant a different set of roles? I wouldn't mind (1D)
providing a hook that allows the system administrator to install a
loadable module that can enforce any rules it likes, but it seems way
too complicated to me to expose all of this configuration as SQL,
especially because for what I want to do, either (1A) or (1B) is
adequate, and (1B) is a LOT simpler than (1C). It also caters to what
I believe to be a common thing to want, without prejudice to the
possibility that other people want other things.
I'm generally in support of changing CREATEROLE to only allow roles that
the role with CREATEROLE is an admin of to be allowed as part of the
command (throwing an error in other cases). That doesn't solve other
use-cases which would certainly be nice to solve but it would at least
reduce the shock people have when they discover how CREATEROLE actually
works (that is, the way we document it to work, but that's ultimately
not what people expect).
If that's all this was about then that would be one thing, but folks are
interested in doing more here and that's good because there's a lot here
that could be (and I'd say should be..) done.
I'm not a fan of 1B. In general, I'm in support of 1C but I don't feel
that absolutely everything must be done for 1C right from the start-
rather, I would argue that we'd be better off building a way for 1C to
be improved upon in the future, akin to our existing privilege system
where we've added things like the ability to GRANT TRUNCATE rights which
didn't originally exist. I don't think 1D is a reasonable way to
accomplish that though, particularly as this involves storing
information about roles which needs to be cleaned up if those roles are
removed or modified. I also don't really agree with the statement that
this ends up being too complicated for SQL.
2. Only allow a CREATEROLE user to drop users which that account
created, and not just any role that isn't a superuser. Again leaving
aside previous proposals, this cannot be implemented without providing
some means by which we know which CREATEROLE user created which other
user. I believe there are a variety of words we could use to describe
that linkage, and I don't deeply care which ones we pick, although I
have my own preferences. We could speak of the CREATEROLE user being
the owner, manager, or administrator of the created role. We could
speak of a new kind of object, a TENANT, of which the CREATEROLE user
is the administrator and to which the created user is linked. I
proposed this previously and it's still my favorite idea. There are no
doubt other options as well. But it's axiomatic that we cannot
restrict the rights of a CREATEROLE user to drop other roles to a
subset of roles without having some way to define which subset is at
issue.
I don't think it's a great plan to limit who is allowed to DROP roles to
be just those that a given role created. I also don't like the idea of
introducing a single field for owner/manager/tenant/whatever to the role
system- instead we should add other ways that roles can be associated to
each other by extending the existing system that we have for that, which
is role membership. Role membership today is pretty limited but I don't
see any reason why we couldn't improve on that in a way that's flexible
and allows us to define new associations in the future. The biggest
difference between a 'tenant' or such as proposed vs. a role association
is in where the information is tracked and what exactly it means.
Saying "I want a owner" or such is easy because it's basically punting
on the complciated bit of asking the question: what does that *mean*
when it comes to what rights that includes vs. doesn't? What if I only
want some of those rights to be given away but not all of them? We have
that system for tables/schemas/etc, and it hasn't been great as we've
seen through the various requests to add things like GRANT TRUNCATE.
But if you DO want the userbot to be able to access that
functionality, then things are more complicated, because now the
linkage has to be special-purpose. In that scenario, we can't say that
the right of a CREATEROLE user to drop a certain other role implies
having the privileges of that other role, because in your use case,
you don't want that, whereas in mine, I do. What makes this
particularly ugly is that we can't, as things currently stand, use a
role as the grouping mechanism, because of the fact that a role can
revoke membership in itself from some other role. It will not do for
roles to remove themselves from the set of roles that the CREATEROLE
user can drop. If we changed that behavior, then perhaps we could just
define a way to say that role X can drop roles if they are members of
group G. In my tenant scenario, G would be granted to X, and in your
userbot scenario, it wouldn't. Everybody wins, except for any people
who like the ability of roles to revoke themselves from any group
whatsoever.
The ability of a role to revoke itself from some other role is just
something we need to accept as being a change that needs to be made, and
I do believe that such a change is supported by the standard, in that a
REVOKE will only work if you have the right to make it as the user who
performed the GRANT in the first place.
So that leads to these questions: (2A) Do you care about restricting
which roles the userbot can drop? (2B) If yes, do you endorse
restricting the ability of roles to revoke themselves from other
roles?
As with Joshua, and as hopefully came across from the above discussion,
I'm also a 'yes and yes' on these two.
I think that we don't have any great problems here, at least as far as
this very specific issue is concerned, if either the answer to (2A) is
no or the answer to (2B) is yes. However, if the answer to (2A) is yes
and the answer to (2B) is no, there are difficulties. Evidently in
that case we need some new kind of thing that behaves mostly likes a
group of roles but isn't actually a group of roles -- and that thing
needs to prohibit self-revocation. Given what I've written above, you
may be able to guess my preferred solution: let's call it a TENANT.
Then, my pseudo-super-user can have permission to (i) create roles in
that tenant, (ii) drop roles in that tenant, and (iii) assume the
privileges of roles in that tenant -- and your userbot can have
privileges to do (i) and (ii) but not (iii). All we need do is add a
roltenant column to pg_authid and find three bits someplace
corresponding to (i)-(iii), and we are home.
Where are those bits going to go though..? I don't think they should go
into pg_authid, nor do I feel that this 'tenant' or such should go there
either because pg_authid is about describing individual roles, not about
role associations. Instead, I'd suggest that those bits go into
pg_auth_members in the form of additional columns to describe the role
associations. That is, instead of the existance of a row in
pg_auth_members meaning that one role has membership in another role, we
give users the choice of if that's the case or not with a separate
column. That would then neatly give us a way for a role to have admin
rights over another role but not membership in that role. We could then
further extend this by adding other columns to pg_auth_members for other
rights as users decide they need them- such as the ability for a role to
DROP some set of roles.
2A, yes
2B, yes, and IIUC this already exists:
postgres=> select current_user;
current_user
--------------
joshua
(1 row)postgres=> REVOKE employees FROM joshua;
ERROR: must have admin option on role "employees"
That's not the right direction though, or, at least, might not be in the
case being discussed (though, I suppose, we could discuss that..). In
what you're showing, employees doesn't have the rights of joshua, but
joshua has the rights of employees. If, instead, joshua was GRANT'd to
admin and joshua decided that they didn't care for that, they can:
=> select current_user;
current_user
--------------
joshua
(1 row)
=> \du
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-------------
admin | Cannot login | {joshua}
employees | Cannot login | {}
joshua | | {employees}
sfrost | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
=> revoke joshua from admin;
REVOKE ROLE
=*> \du
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-------------
admin | Cannot login | {}
employees | Cannot login | {}
joshua | | {employees}
sfrost | Superuser, Create role, Create DB, Replication, Bypass RLS | {}
Even though, in this case, it was 'sfrost' (a superuser) who GRANT'd
joshua to admin.
Thanks,
Stephen
Import Notes
Reply to msg id not found: CAGB+Vh4Qb4+5zrJ2pJio6rxy2+hnt2zhTmonxgzfF3wY4H6WRw@mail.gmail.comCA+Tgmob_rJtHdjMOqiOcGtk0vU6XXjOqQ0LSvVpUJjvdY341rw@mail.gmail.com
On Mon, Feb 28, 2022 at 2:09 PM Stephen Frost <sfrost@snowman.net> wrote:
I'm generally in support of changing CREATEROLE to only allow roles that
the role with CREATEROLE is an admin of to be allowed as part of the
command (throwing an error in other cases). That doesn't solve other
use-cases which would certainly be nice to solve but it would at least
reduce the shock people have when they discover how CREATEROLE actually
works (that is, the way we document it to work, but that's ultimately
not what people expect).
So I'm 100% good with that because it does exactly what I want, but my
understanding of the situation is that it breaks the userbot case that
Joshua is talking about. Right now, with stock PostgreSQL in any
released version, his userbot can have CREATEROLE and give out roles
that it doesn't itself possess. If we restrict CREATEROLE in the way
described here, that's no longer possible.
Now it's possible that you and/or he would take the position that
we're still coming out ahead despite that functional regression,
because as I now understand after reading Joshua's latest email, he
doesn't want the userbot to be able to grant ANY role, just the
'employees' role - and today he can't get that. So in a modified
universe where we restrict the privileges of CREATEROLE, then on the
one hand he GAINS the ability to have a userbot that can grant some
roles but not others, but on the other hand, he's forced to give the
userbot the roles he wants it to be able to hand out. Is that better
overall or worse?
To really give him EXACTLY what he wants, we need a way of specifying
administration without membership. See my last reply to the thread for
my concerns about that.
I don't think it's a great plan to limit who is allowed to DROP roles to
be just those that a given role created. I also don't like the idea of
introducing a single field for owner/manager/tenant/whatever to the role
system- instead we should add other ways that roles can be associated to
each other by extending the existing system that we have for that, which
is role membership. Role membership today is pretty limited but I don't
see any reason why we couldn't improve on that in a way that's flexible
and allows us to define new associations in the future. The biggest
difference between a 'tenant' or such as proposed vs. a role association
is in where the information is tracked and what exactly it means.
Saying "I want a owner" or such is easy because it's basically punting
on the complciated bit of asking the question: what does that *mean*
when it comes to what rights that includes vs. doesn't? What if I only
want some of those rights to be given away but not all of them? We have
that system for tables/schemas/etc, and it hasn't been great as we've
seen through the various requests to add things like GRANT TRUNCATE.
Well, there's no accounting for taste, but I guess I see this pretty
much opposite to the way you do. I think GRANT TRUNCATE is nice and
simple and clear. It does one thing and it's easy to understand what
that thing is, and it has very few surprising or undocumented side
effects. On the other hand, role membership is a mess, and it's not at
all clear how to sort that mess out. I guess I agree with you that it
would be nice if it could be done, but the list of problems is pretty
substantial. Like, membership implies the right to SET ROLE, and also
the right to implicitly exercise the privileges of the role, and
you've complained about that fuzziness. And ADMIN OPTION implies
membership, and you don't like that either. And elsewhere it's been
raised that nobody would expect to have a table end up owned by
'pg_execute_server_programs', or a user logged in directly as
'employees' rather than as some particular employee, but all that
stuff can happen, and some of it can't even be effectively prevented
with good configuration. 'toe' can be a member of 'foot' while, which
makes sense to everybody, and at the same time, 'foot' can be a member
of 'toe', which doesn't make any sense at all. And because both
directions are possible even experienced PostgreSQL users and hackers
get confused, as demonstrated by Joshua's having just got the
revoke-from-role case backwards.
Of those four problems, the last two are clearly the result of
conflating users with groups - and really also with capabilities - and
having a unified role concept that encompasses all of those things. I
think we would be better off if we had not done that, both in the
sense that I think the system would be less confusing to understand,
and also in the sense that we would likely have fewer security bugs.
And similarly I agree with you that it would be better if the right to
administer a role were clearly separated from membership in a role,
and if the right to use the privileges of a role were separated from
the ability to SET ROLE to it. However, unlike you, I see the whole
'role membership' concept as the problem, not the solution. We
conflate a bunch of different kinds of things together and call them
all 'roles' and a bunch of other things together and call them
'membership' and then we end up with an awkward mess. That's how I see
it, anyway.
The ability of a role to revoke itself from some other role is just
something we need to accept as being a change that needs to be made, and
I do believe that such a change is supported by the standard, in that a
REVOKE will only work if you have the right to make it as the user who
performed the GRANT in the first place.
Great. I propose that we sever that issue and discuss it on a new
thread to avoid confusion. I believe there is some debate to be had
about exactly what we want the behavior to be in this area, but if we
can reach consensus on that point, this shouldn't be too hard to knock
out. I will take it as an action item to get that thread going, if
that works for you.
So that leads to these questions: (2A) Do you care about restricting
which roles the userbot can drop? (2B) If yes, do you endorse
restricting the ability of roles to revoke themselves from other
roles?As with Joshua, and as hopefully came across from the above discussion,
I'm also a 'yes and yes' on these two.
Great.
I think that we don't have any great problems here, at least as far as
this very specific issue is concerned, if either the answer to (2A) is
no or the answer to (2B) is yes. However, if the answer to (2A) is yes
and the answer to (2B) is no, there are difficulties. Evidently in
that case we need some new kind of thing that behaves mostly likes a
group of roles but isn't actually a group of roles -- and that thing
needs to prohibit self-revocation. Given what I've written above, you
may be able to guess my preferred solution: let's call it a TENANT.
Then, my pseudo-super-user can have permission to (i) create roles in
that tenant, (ii) drop roles in that tenant, and (iii) assume the
privileges of roles in that tenant -- and your userbot can have
privileges to do (i) and (ii) but not (iii). All we need do is add a
roltenant column to pg_authid and find three bits someplace
corresponding to (i)-(iii), and we are home.Where are those bits going to go though..? I don't think they should go
into pg_authid, nor do I feel that this 'tenant' or such should go there
either because pg_authid is about describing individual roles, not about
role associations. Instead, I'd suggest that those bits go into
pg_auth_members in the form of additional columns to describe the role
associations. That is, instead of the existance of a row in
pg_auth_members meaning that one role has membership in another role, we
give users the choice of if that's the case or not with a separate
column. That would then neatly give us a way for a role to have admin
rights over another role but not membership in that role. We could then
further extend this by adding other columns to pg_auth_members for other
rights as users decide they need them- such as the ability for a role to
DROP some set of roles.
What I had in mind is to add a pg_tenant catalog (tenid, tenname) and
add some columns to the pg_authid catalog (roltenant, roltenantrights,
or something like that). See above for why I am not excited about
piggybacking more things onto role membership.
=> revoke joshua from admin;
REVOKE ROLE=*> \du
List of roles
Role name | Attributes | Member of
-----------+------------------------------------------------------------+-------------
admin | Cannot login | {}
employees | Cannot login | {}
joshua | | {employees}
sfrost | Superuser, Create role, Create DB, Replication, Bypass RLS | {}Even though, in this case, it was 'sfrost' (a superuser) who GRANT'd
joshua to admin.
Quite so.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mon, Feb 28, 2022 at 2:09 PM Stephen Frost <sfrost@snowman.net> wrote:
The ability of a role to revoke itself from some other role is just
something we need to accept as being a change that needs to be made, and
I do believe that such a change is supported by the standard, in that a
REVOKE will only work if you have the right to make it as the user who
performed the GRANT in the first place.
Moving this part of the discussion to a new thread to reduce confusion
and hopefully get broader input on this topic. It seems like Stephen
and I agree in principle that some change here is a good idea. If
anyone else thinks it's a bad idea, then this would be a great time to
mention that, ideally with reasons. If you agree that it's a good
idea, then it would be great to have your views on the follow-up
questions which I shall pose below. To the extent that it is
reasonably possible to do so, I would like to try to keep focused on
specific design questions rather than getting tangled up in general
discussion of long-term direction. First, a quick overview of the
issue for those who have not followed the earlier threads in their
grueling entirety:
rhaas=# create user boss;
CREATE ROLE
rhaas=# create user peon;
CREATE ROLE
rhaas=# grant peon to boss;
GRANT ROLE
rhaas=# \c - peon
You are now connected to database "rhaas" as user "peon".
rhaas=> revoke peon from boss; -- i don't like being bossed around!
REVOKE ROLE
I argue (and Stephen seems to agree) that the peon shouldn't be able
to undo the superuser's GRANT. Furthermore, we also seem to agree that
you don't necessarily have to be the exact user who performed the
grant. For example, it would be shocking if one superuser couldn't
remove a grant made by another superuser, or for that matter if a
superuser couldn't remove a grant made by a non-superuser. But there
are a few open questions in my mind:
1. What should be the exact rule for whether A can remove a grant made
by B? Is it has_privs_of_role()? is_member_of_role()? Something else?
2. What happens if the same GRANT is enacted by multiple users? For
example, suppose peon does "GRANT peon to boss" and then the superuser
does the same thing afterwards, or vice versa? One design would be to
try to track those as two separate grants, but I'm not sure if we want
to add that much complexity, since that's not how we do it now and it
would, for example, implicate the choice of PK on the pg_auth_members
table. An idea that occurs to me is to say that the first GRANT works
and becomes the grantor of record, and any duplicate GRANT that
happens later issues a NOTICE without changing anything. If the user
performing the later GRANT has sufficient privileges and wishes to do
so, s/he can REVOKE first and then re-GRANT. On the other hand, for
other types of grants, like table privileges, we do track multiple
grants by different users, so maybe we should do the same thing here:
rhaas=# create table example (a int, b int);
CREATE TABLE
rhaas=# grant select on table example to foo with grant option;
GRANT
rhaas=# grant select on table example to bar with grant option;
GRANT
rhaas=# \c - foo
You are now connected to database "rhaas" as user "foo".
rhaas=> grant select on table example to exemplar;
GRANT
rhaas=> \c - bar
You are now connected to database "rhaas" as user "bar".
rhaas=> grant select on table example to exemplar;
GRANT
rhaas=> select relacl from pg_class where relname = 'example';
relacl
-------------------------------------------------------------------------------
{rhaas=arwdDxt/rhaas,foo=r*/rhaas,bar=r*/rhaas,exemplar=r/foo,exemplar=r/bar}
(1 row)
3. What happens if a user is dropped after being recorded as a
grantor? We actually have a grantor column in pg_auth_members today,
but it's not properly maintained. If the grantor is dropped the OID
remains in the table, and could eventually end up pointing to some
other user if the OID counter wraps around and a new role is created
with the same OID. That's completely unacceptable for something we
want to use for any serious purpose. I suggest that what ought to
happen is the role should acquire a dependency on the grant, such that
DROP fails and the GRANT is listed as something to be dropped, and
DROP OWNED BY drops the GRANT. I think this would require adding an
OID column to pg_auth_members so that a dependency can point to it,
which sounds like a significant infrastructure change that would need
to be carefully validated for adverse side effects, but not a huge
crazy problem that we can't get past.
4. Should we apply this rule to other types of grants, rather than
just to role membership? Why or why not? Consider this:
rhaas=# create user accountant;
CREATE ROLE
rhaas=# create user auditor;
CREATE ROLE
rhaas=# create table money (a int, b text);
CREATE TABLE
rhaas=# alter table money owner to accountant;
ALTER TABLE
rhaas=# grant select on table money to auditor;
GRANT
rhaas=# \c - accountant
You are now connected to database "rhaas" as user "accountant".
rhaas=> revoke select on table money from auditor;
REVOKE
I would argue that's exactly the same problem. The superuser has
decreed that the auditor gets to select from the money table owned by
the accountant. The fact that the accountant may not be not in favor
of the auditor seeing what the accountant is doing with the money is
precisely the reason why we have auditors. That said, if we apply this
to all object types, it's a much bigger change. Unlike role
membership, we do record dependencies on table privileges, which makes
any change here a bit simpler, and you can't drop a role without
removing the associated grants first. However, when the superuser
performs the GRANT as in the above example, the grantor is recorded as
the table owner, not the superuser! So if we really want role
membersip and other kinds of grants to behave in the same way, we have
our work cut out for us here.
Please note that it is not really my intention to try to shove
anything into v15 here. If it so happens that we quickly agree on
something that already exists in the patches Mark's already written,
and we also agree that those patches are in good enough shape that we
can commit something in the next few weeks, fantastic, but I'm not
necessarily expecting that. What I do want to do is agree on a plan so
that, if somebody does the work to implement said plan, we do not then
end up relitigating the whole thing and coming to a different
conclusion the second time. This being a community whose membership
varies from time to time and the opinions of whose members vary from
time to time, such misadventure can never be entirely ruled out.
However, I would like to minimize the chances of such an outcome as
much as we can.
Thanks,
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
1. What should be the exact rule for whether A can remove a grant made
by B? Is it has_privs_of_role()? is_member_of_role()? Something else?
No strong opinion here, but I'd lean slightly to the more restrictive
option.
2. What happens if the same GRANT is enacted by multiple users? For
example, suppose peon does "GRANT peon to boss" and then the superuser
does the same thing afterwards, or vice versa? One design would be to
try to track those as two separate grants, but I'm not sure if we want
to add that much complexity, since that's not how we do it now and it
would, for example, implicate the choice of PK on the pg_auth_members
table.
As you note later, we *do* track such grants separately in ordinary
ACLs, and I believe this is clearly required by the SQL spec.
It says (for privileges on objects):
Each privilege is represented by a privilege descriptor.
A privilege descriptor contains:
— The identification of the object on which the privilege is granted.
— The <authorization identifier> of the grantor of the privilege.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
— The <authorization identifier> of the grantee of the privilege.
— Identification of the action that the privilege allows.
— An indication of whether or not the privilege is grantable.
— An indication of whether or not the privilege has the WITH HIERARCHY OPTION specified.
Further down (4.42.3 in SQL:2021), the granting of roles is described,
and that says:
Each role authorization is described by a role authorization descriptor.
A role authorization descriptor includes:
— The role name of the role.
— The authorization identifier of the grantor.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
— The authorization identifier of the grantee.
— An indication of whether or not the role authorization is grantable.
If we are not tracking the grantors of role authorizations,
then we are doing it wrong and we ought to fix that.
3. What happens if a user is dropped after being recorded as a
grantor?
Should work the same as it does now for ordinary ACLs, ie, you
gotta drop the grant first.
4. Should we apply this rule to other types of grants, rather than
just to role membership?
I am not sure about the reasoning behind the existing rule that
superuser-granted privileges are recorded as being granted by the
object owner. It does feel more like a wart than something we want.
It might have been a hack to deal with the lack of GRANTED BY
options in GRANT/REVOKE back in the day.
Changing it could have some bad compatibility consequences though.
In particular, I believe it would break existing pg_dump files,
in that after restore all privileges would be attributed to the
restoring superuser, and there'd be no very easy way to clean that
up.
Please note that it is not really my intention to try to shove
anything into v15 here.
Agreed, this is not something to move on quickly. We might want
to think about adjusting pg_dump to use explicit GRANTED BY
options in GRANT/REVOKE a release or two before making incompatible
changes.
regards, tom lane
On Fri, Mar 4, 2022 at 1:50 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Mon, Feb 28, 2022 at 2:09 PM Stephen Frost <sfrost@snowman.net> wrote:
The ability of a role to revoke itself from some other role is just
something we need to accept as being a change that needs to be made, and
I do believe that such a change is supported by the standard, in that a
REVOKE will only work if you have the right to make it as the user who
performed the GRANT in the first place.First, a quick overview of the
issue for those who have not followed the earlier threads in their
grueling entirety:rhaas=# create user boss;
CREATE ROLE
rhaas=# create user peon;
CREATE ROLE
rhaas=# grant peon to boss;
GRANT ROLE
rhaas=# \c - peon
You are now connected to database "rhaas" as user "peon".
rhaas=> revoke peon from boss; -- i don't like being bossed around!
REVOKE ROLE
The wording for this example is hurting my brain.
GRANT admin TO joe;
\c admin
REVOKE admin FROM joe;
I argue (and Stephen seems to agree) that the peon shouldn't be able
to undo the superuser's GRANT.
I think I disagree. Or, at least, the superuser has full control of
dictating how role membership is modified and that seems sufficient.
The example above works because of:
"A role is not considered to hold WITH ADMIN OPTION on itself, but it may
grant or revoke membership in itself from a database session where the
session user matches the role."
If a superuser doesn't want "admin" to modify its own membership then they
can prevent anyone but a superuser from being able to have a session_user
of "admin". If that happens then the only way a non-superuser can modify
group membership is by being added to said group WITH ADMIN OPTION.
Now, if two people and a superuser are all doing membership management on
the same group, and we want to add permission checks and multiple grants as
tools, instead of having them just communicate with each other, then by all
means let us do so. In that case, in answer to questions 2 and 3, we
should indeed track which session_user made the grant and only allow the
same session_user or the superuser to revoke it (we'd want to stop
"ignoring" the GRANTED BY clause of REVOKE ROLE FROM so the superuser at
least could remove grants made via WITH ADMIN OPTION).
4. Should we apply this rule to other types of grants, rather than
just to role membership? Why or why not? Consider this:
The fact that the accountant may not be not in favor
of the auditor seeing what the accountant is doing with the money is
precisely the reason why we have auditors.
[...]
However, when the superuser
performs the GRANT as in the above example, the grantor is recorded as
the table owner, not the superuser! So if we really want role
membersip and other kinds of grants to behave in the same way, we have
our work cut out for us here.
Yes, this particular choice seems unfortunate, but also not something that
I think it is necessarily mandatory for us to improve. If the accountant
is the owner then yes they get to decide permissions. In the presence of
an auditor role either you trust the accountant role to keep the
permissions in place or you define a superior authority to both the auditor
and accountant to be the owner. Or let the superuser manage everything by
witholding login and WITH ADMIN OPTION privileges from the ownership role.
If we do extend role membership tracking I suppose the design question is
whether the new role grantor dependency tracking will have a superuser be
the recorded grantor instead of some owner. Given that roles don't
presently have an owner concept, consistency with existing permissions in
this manner would be trickier. Because of this, I would probably leave
role grantor tracking at the session_user level while database objects
continue to emanate from the object owner. The global vs database
differences seem like a sufficient theoretical justification for the
difference in implementation.
David J.
On Fri, Mar 4, 2022 at 5:20 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:
I think I disagree. Or, at least, the superuser has full control of dictating how role membership is modified and that seems sufficient.
The point is that the superuser DOES NOT have full control. The
superuser cannot prevent relatively low-privileged users from undoing
things that the superuser did intentionally and doesn't want reversed.
The choice of names in my example wasn't accidental. If the granted
role is a login role, then the superuser's intention was to vest the
privileges of that role in some other role, and it is surely not right
for that role to be able to decide that it doesn't want it's
privileges to be so granted. That's why I chose the name "peon". In
your example, where you chose the name "admin", the situation is less
clear. If we imagine the granted role as a container for a bundle of
privileges, giving it the ability to administer itself feels more
reasonable. However, I am very much unconvinced that it's correct even
there. Suppose the superuser grants "admin" to both "joe" and "sally".
Now "joe" can SET ROLE to "admin" and revoke it from "sally", and the
superuser has no tool to prevent this.
Now you can imagine a situation where the superuser is totally OK with
either "joe" or "sally" having the ability to lock the other one out,
but I don't think it's right to say that this will be true in all
cases.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Mar 4, 2022 at 4:34 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
If we are not tracking the grantors of role authorizations,
then we are doing it wrong and we ought to fix that.
Hmm, so maybe that's the place to start. We are tracking it in the
sense that we record an OID in the catalog, but nothing that happens
after that makes a lot of sense.
3. What happens if a user is dropped after being recorded as a
grantor?Should work the same as it does now for ordinary ACLs, ie, you
gotta drop the grant first.
OK, that makes sense to me.
Changing it could have some bad compatibility consequences though.
In particular, I believe it would break existing pg_dump files,
in that after restore all privileges would be attributed to the
restoring superuser, and there'd be no very easy way to clean that
up.
I kind of wonder whether we ought to attribute all privileges granted
by any superuser to the bootstrap superuser. That doesn't seem to have
any meaningful downside, and it could avoid a lot of annoying
dependencies that serve no real purpose.
Agreed, this is not something to move on quickly. We might want
to think about adjusting pg_dump to use explicit GRANTED BY
options in GRANT/REVOKE a release or two before making incompatible
changes.
Uggh. I really want to make some meaningful progress here before the
heat death of the universe, and I'm not sure that this manner of
proceeding is really going in that direction. That said, I do entirely
see your point. Are you thinking we'd actually add a GRANTED BY clause
to GRANT/REVOKE, vs. just wrapping it in SET ROLE incantations of some
sort?
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
On Fri, Mar 4, 2022 at 4:34 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Agreed, this is not something to move on quickly. We might want
to think about adjusting pg_dump to use explicit GRANTED BY
options in GRANT/REVOKE a release or two before making incompatible
changes.
Uggh. I really want to make some meaningful progress here before the
heat death of the universe, and I'm not sure that this manner of
proceeding is really going in that direction. That said, I do entirely
see your point. Are you thinking we'd actually add a GRANTED BY clause
to GRANT/REVOKE, vs. just wrapping it in SET ROLE incantations of some
sort?
I was thinking the former ... however, after a bit of experimentation
I see that we accept "grant foo to bar granted by baz" a VERY long
way back, but the "granted by" option for object privileges is
(a) pretty new and (b) apparently restrictively implemented:
regression=# grant delete on alices_table to bob granted by alice;
ERROR: grantor must be current user
That's ... surprising. I guess whoever put that in was only
interested in pro-forma SQL syntax compliance and not in making
a usable feature.
So if we decide to extend this change into object privileges
it would be advisable to use SET ROLE, else we'd be giving up
an awful lot of backwards compatibility in dump scripts.
But if we're only talking about role grants then I think
GRANTED BY would work fine.
regards, tom lane
On Sun, Mar 6, 2022 at 10:19 AM Robert Haas <robertmhaas@gmail.com> wrote:
On Fri, Mar 4, 2022 at 5:20 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:I think I disagree. Or, at least, the superuser has full control of dictating how role membership is modified and that seems sufficient.
The point is that the superuser DOES NOT have full control. The
superuser cannot prevent relatively low-privileged users from undoing
things that the superuser did intentionally and doesn't want reversed.The choice of names in my example wasn't accidental. If the granted
role is a login role, then the superuser's intention was to vest the
privileges of that role in some other role, and it is surely not right
for that role to be able to decide that it doesn't want it's
privileges to be so granted. That's why I chose the name "peon". In
your example, where you chose the name "admin", the situation is less
clear. If we imagine the granted role as a container for a bundle of
privileges, giving it the ability to administer itself feels more
reasonable. However, I am very much unconvinced that it's correct even
there. Suppose the superuser grants "admin" to both "joe" and "sally".
Now "joe" can SET ROLE to "admin" and revoke it from "sally", and the
superuser has no tool to prevent this.Now you can imagine a situation where the superuser is totally OK with
either "joe" or "sally" having the ability to lock the other one out,
but I don't think it's right to say that this will be true in all
cases.
Another example here is usage of groups in pg_hba.conf, if the admin
has a group of users with stronger authentication requirements: e.g.,
hostssl all +certonlyusers all cert map=certmap clientcert=1
and one can remove their membership, they can change their
authentication requirements.
Robert Haas <robertmhaas@gmail.com> writes:
... Suppose the superuser grants "admin" to both "joe" and "sally".
Now "joe" can SET ROLE to "admin" and revoke it from "sally", and the
superuser has no tool to prevent this.
Really?
regression=# grant admin to joe;
GRANT ROLE
regression=# grant admin to sally;
GRANT ROLE
regression=# \c - joe
You are now connected to database "regression" as user "joe".
regression=> revoke admin from sally;
ERROR: must have admin option on role "admin"
regression=> set role admin;
SET
regression=> revoke admin from sally;
ERROR: must have admin option on role "admin"
I think there is an issue here around exactly what the admin option
means, but if it doesn't grant you the ability to remove grants
made by other people, it's pretty hard to see what it's for.
regards, tom lane
On Sun, Mar 6, 2022 at 9:53 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
... Suppose the superuser grants "admin" to both "joe" and "sally".
Now "joe" can SET ROLE to "admin" and revoke it from "sally", and the
superuser has no tool to prevent this.Really?
regression=# grant admin to joe;
GRANT ROLE
regression=# grant admin to sally;
GRANT ROLE
regression=# \c - joe
You are now connected to database "regression" as user "joe".
regression=> revoke admin from sally;
ERROR: must have admin option on role "admin"
regression=> set role admin;
SET
regression=> revoke admin from sally;
ERROR: must have admin option on role "admin"I think there is an issue here around exactly what the admin option
means, but if it doesn't grant you the ability to remove grants
made by other people, it's pretty hard to see what it's for.
Precisely.
The current system, with the session_user exception, basically guides a
superuser to define two kinds of roles.
Groups: No login, permission grants
Users: Login, inherits permissions from groups, can manage group membership
if given WITH ADMIN OPTION.
The original example using only users is not all that compelling to me.
IMO, DBAs should not be setting up their system that way.
Two questions remain:
1. Are we willing to get rid of the session_user exception?
2. Do we want to track who the grantor is for role membership grants and
institute a requirement that non-superusers can only revoke the grants that
they personally made?
I'm personally in favor of getting rid of the session_user exception, which
nicely prevents the problem at the beginning of this thread and further
encourages the DBA to define groups and roles with a greater
separation-of-concerns design. WITH ADMIN OPTION is sufficient.
I think tracking grantor information for role membership would allow for
greater auditing capabilities and a better degree of control in the
permissions system.
In short, I am in favor of both options. The grantor tracking seems to be
headed for acceptance.
So, do we really want to treat every single login role as a potential group
by keeping the session_user exception?
David J.
On Sun, Mar 6, 2022 at 8:19 AM Robert Haas <robertmhaas@gmail.com> wrote:
The choice of names in my example wasn't accidental. If the granted
role is a login role, then the superuser's intention was to vest the
privileges of that role in some other role, and it is surely not right
for that role to be able to decide that it doesn't want it's
privileges to be so granted. That's why I chose the name "peon".
rhaas [as peon] => revoke peon from boss; -- i don't like being bossed
around!
Well, the peon is not getting bossed around, the boss is getting peoned
around and the peon has decided that they like boss too much and don't need
to do that anymore.
When you grant a group "to" a role you place the role under the group - and
inheritance flows downward.
In the original thread Stephen wrote:
"This is because we allow 'self administration' of roles, meaning that
they can decide what other roles they are a member of.:
The example, which you moved here, then attempts to demonstrate this "fact"
but gets it wrong. Boss became a member of peon so if you want to
demonstrate self-administration of a role's membership in a different group
you have to login as boss, not peon. Doing that, and then revoking peon
from boss, yields "ERROR: must have admin option on role "peon"".
So no, without "WITH ADMIN OPTION" a role cannot decide what other roles
they are a member of.
I don't necessarily have an issue changing self-administration but if the
motivating concern is that all these new pg_* roles we are creating are
something a normal user can opt-out of/revoke that simply isn't the case
today, unless they are added to the pg_* role WITH ADMIN OPTION.
That all said, permissions SHOULD BE strictly additive. If boss doesn't
want to be a member of pg_read_all_files allowing them to revoke themself
from that role seems like it should be acceptable. If there is fear in
allowing someone to revoke (not add) themselves as a member of a different
role that suggests we have a design issue in another feature of the
system. Today, they neither grant nor revoke, and the self-revocation
doesn't seem that important to add.
David J.
On Sun, Mar 6, 2022 at 11:34 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I was thinking the former ... however, after a bit of experimentation
I see that we accept "grant foo to bar granted by baz" a VERY long
way back, but the "granted by" option for object privileges is
(a) pretty new and (b) apparently restrictively implemented:regression=# grant delete on alices_table to bob granted by alice;
ERROR: grantor must be current userThat's ... surprising. I guess whoever put that in was only
interested in pro-forma SQL syntax compliance and not in making
a usable feature.
It appears so: /messages/by-id/2073b6a9-7f79-5a00-5f26-cd19589a52c7@2ndquadrant.com
It doesn't seem like that would be hard to fix. Maybe we should just do that.
So if we decide to extend this change into object privileges
it would be advisable to use SET ROLE, else we'd be giving up
an awful lot of backwards compatibility in dump scripts.
But if we're only talking about role grants then I think
GRANTED BY would work fine.
OK.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Sun, Mar 6, 2022 at 11:53 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Really?
regression=# grant admin to joe;
GRANT ROLE
regression=# grant admin to sally;
GRANT ROLE
regression=# \c - joe
You are now connected to database "regression" as user "joe".
regression=> revoke admin from sally;
ERROR: must have admin option on role "admin"
regression=> set role admin;
SET
regression=> revoke admin from sally;
ERROR: must have admin option on role "admin"
Oops. I stand corrected.
I think there is an issue here around exactly what the admin option
means, but if it doesn't grant you the ability to remove grants
made by other people, it's pretty hard to see what it's for.
Hmm. I think the real issue is what David Johnson calls the session
user exception. I hadn't quite understood how that played into this.
According to the documentation: "If WITH ADMIN OPTION is specified,
the member can in turn grant membership in the role to others, and
revoke membership in the role as well. Without the admin option,
ordinary users cannot do that. A role is not considered to hold WITH
ADMIN OPTION on itself, but it may grant or revoke membership in
itself from a database session where the session user matches the
role."
Is there some use case for the behavior described in that last
sentence? If that exception is the only case in which an unprivileged
user can revoke a grant made by someone else, then getting rid of it
seems pretty appealing from where I sit. I can't speak to the
standards compliance end of things, but it doesn't intrinsically seem
bothersome that having "WITH ADMIN OPTION" on a role lets you control
who has membership in said role. And certainly it's not bothersome
that the superuser can change whatever they want. The problem here is
just that a user with NO special privileges on any role, including
their own, can make changes that more privileged users might not like.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Sun, Mar 6, 2022 at 2:09 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:
So, do we really want to treat every single login role as a potential group by keeping the session_user exception?
I think that we DO want to continue to treat login roles as
potentially grantable privileges. That feels fundamentally useful to
me. The superuser is essentially granted the privileges of all users
on the system, and has all the rights they have, including the right
to drop tables owned by those users as if they were the owner of those
tables. If it's useful for the superuser to implicitly have the rights
of all users on the system, why should it not be useful for some
non-superuser to implicitly have the rights of some other users on the
system? I think it pretty clearly is. If one of my colleagues leaves
the company, the DBA can say "grant jdoe to rhaas" and let me mess
around with this stuff. Or, the DBA can grant me the privileges of all
my direct reports even when they're not leaving so that I can sort out
anything I need to do without superuser involvement. That all seems
cool and OK to me.
Now I think it is fair to say that we could have chosen a different
design, and MAYBE that would have been better. Nobody forced us to
conflate users and groups into a unified thing called roles, and I
think there's pretty good evidence that it's confusing and
counterintuitive in some ways. There's also no intrinsic reason why
the superuser has to be able to directly exercise the privileges of
every role rather than, say, having a way to become any given role.
But at this point, those design decisions are pretty well baked into
the system design, and I don't really think it's likely that we want
to change them. To put that another way, just because you don't like
the idea of granting one login role to another login role, that
doesn't mean that the feature doesn't exist, and as long as that
feature does exist, trying to make it work better or differently is
fair game.
But I think that's separate from your other question about whether we
should remove the session user exception. That looks tempting to me at
first glance, because we have exchanged several hundred, and it really
feels more like several million, emails on this list about how much of
a problem it is that an unprivileged user can just log in and run a
REVOKE. It breaks the idea that the people WITH ADMIN OPTION on a role
are the ones who control membership in that role. Joshua Brindle's
note upthread about the interaction of this with pg_hba.conf is
another example of that, and I think there are more. Any idea that a
role is a general-purpose way of designating a group of users for some
security critical purpose is threatened if people can make changes to
the membership of that group without being specifically authorized to
do so.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mon, Mar 7, 2022 at 8:37 AM Robert Haas <robertmhaas@gmail.com> wrote:
A role is not considered to hold WITH
ADMIN OPTION on itself, but it may grant or revoke membership in
itself from a database session where the session user matches the
role."Is there some use case for the behavior described in that last
sentence?
I can imagine, in particular combined with CREATEROLE, that this allows for
any user to delegate their personal permissions to a separate newly created
user. Like an assistant. I'm not all that sure whether CREATEROLE is
presently safe enough to give to a normal user in order to make this use
case work but it seems reasonable.
I would be concerned about changing the behavior at this point. But I
would be in favor of at least removing the hard-coded exception and linking
it to a role attribute. That attribute can default to "SELFADMIN" to match
the existing behavior but then "NOSELFADMIN" would exist to disable that
behavior on the per-role basis. Still tied to session_user as opposed to
current_user.
David J.
P.S.
create role selfadmin admin selfadmin; -- ERROR: role "selfadmin" is a
member of role "selfadmin"
create role selfadmin;
grant selfadmin to selfadmin with admin option; -- ERROR: role "selfadmin"
is a member of role "selfadmin"
The error message seems odd. I tried this because instead of a "SELFADMIN"
attribute adding a role to itself WITH ADMIN OPTION could be defined to
have the same effect. You cannot change WITH ADMIN OPTION independently of
the adding of the role to the group.
Robert Haas <robertmhaas@gmail.com> writes:
Hmm. I think the real issue is what David Johnson calls the session
user exception. I hadn't quite understood how that played into this.
According to the documentation: "If WITH ADMIN OPTION is specified,
the member can in turn grant membership in the role to others, and
revoke membership in the role as well. Without the admin option,
ordinary users cannot do that. A role is not considered to hold WITH
ADMIN OPTION on itself, but it may grant or revoke membership in
itself from a database session where the session user matches the
role."
Is there some use case for the behavior described in that last
sentence?
Good question. You might try figuring out when that text was added
and then see if there's relevant discussion in the archives.
Just looking at it now, without having done any historical research,
I wonder why it is that we don't attach significance to WITH ADMIN
OPTION being granted to the role itself. It seems like the second
part of that sentence is effectively saying that a role DOES have
admin option on itself, contradicting the first part.
regards, tom lane
On Mon, Mar 7, 2022 at 9:04 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Just looking at it now, without having done any historical research,
I wonder why it is that we don't attach significance to WITH ADMIN
OPTION being granted to the role itself. It seems like the second
part of that sentence is effectively saying that a role DOES have
admin option on itself, contradicting the first part.
WITH ADMIN OPTION is inheritable which is really bad if the group has WITH
ADMIN OPTION on itself. The session_user exception temporarily grants WITH
ADMIN OPTION to the group but it is done in such a way so that it is not
inheritable.
There is no possible way to even assign WITH ADMIN OPTION on a role to
itself since pg_auth_members doesn't record a self-relationship and
admin_option only exists there.
David J.
P.S. Feature request; modify \du+ to show which "Member of" roles a given
role has the WITH ADMIN OPTION privilege on.
On Mon, Mar 7, 2022 at 11:04 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Is there some use case for the behavior described in that last
sentence?Good question. You might try figuring out when that text was added
and then see if there's relevant discussion in the archives.
Apparently the permission used to be broader, and commit
fea164a72a7bfd50d77ba5fb418d357f8f2bb7d0 (February 2014, Noah,
CVE-2014-0060) restricted it by requiring that (a) the user had to be
the logged-in user, rather than an identity assumed via SET ROLE (so
maybe my bogus example from before would have worked in 2013) and (b)
that we're not in a security-restricted operation at the time.
Interestingly, it appears to me that the behavior wasn't documented
prior to that commit. The previous text read simply:
If <literal>WITH ADMIN OPTION</literal> is specified, the member can
in turn grant membership in the role to others, and revoke membership
in the role as well. Without the admin option, ordinary users cannot do
that.
That doesn't give any hint that self-administration is a special case.
I reviewed the (private) discussion of this vulnerability on the
pgsql-security mailing list where various approaches were considered.
I think it's safe to share a few broad details about that conversation
publicly now, since it was many years ago and the fix has long since
been published. There was discussion of making this
self-administration behavior something that could be turned off, but
such a change was deemed too large for the back-branches. There was no
discussion that I could find about removing the behavior altogether.
It was noted that having a special case for this was different than
granting WITH ADMIN OPTION because WITH ADMIN OPTION is inherited and
being logged in as a certain user is not.
It appears to me that the actual behavior of having is_admin_of_role()
return true when member == role dates to
f9fd1764615ed5d85fab703b0ffb0c323fe7dfd5 (Tom Lane, 2005). If I'm not
reading this code wrong, prior to that commit, it seems to me that we
only searched the roles that were members of that role, directly or
indirectly, and you had to have admin_option on the last hop of the
membership chain in order to get a "true" result. But that commit,
among other changes, made member == role a special case, but the
comment just says /* Fast path for simple case */ which makes it
appear that it wasn't thought to be a behavior change at all, but it
looks to me like it was. Am I confused?
--
Robert Haas
EDB: http://www.enterprisedb.com
On Sun, Mar 6, 2022 at 11:01 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:
The example, which you moved here, then attempts to demonstrate this "fact" but gets it wrong. Boss became a member of peon so if you want to demonstrate self-administration of a role's membership in a different group you have to login as boss, not peon. Doing that, and then revoking peon from boss, yields "ERROR: must have admin option on role "peon"".
This doesn't seem to me to be making a constructive argument. I showed
an example with certain names demonstrating a certain behavior that I
find problematic. You don't have to think it's problematic, and you
can show other examples that demonstrate things you want to show. But
please don't tell me that when I literally cut and paste the output
from my terminal into an email window, what I'm showing is somehow
counterfactual. The behavior as it exists today is surely a fact, and
an easily demonstrable one at that. It's not a "fact'" in quotes, and
it doesn't "get it wrong". It is the actual behavior and the example
with the names I picked demonstrates precisely what I want to
demonstrate. When you say that I should have chosen a different
example or used different identifier names or talked about it in
different way, *that* is an opinion. I believe that you are wholly
entitled to that opinion, even if (as in this case) I disagree, but I
believe that it is not right at all to make it sound as if I don't
have the right to pick the examples I care about, or as if terminal
output is not a factual representation of how things work today.
So no, without "WITH ADMIN OPTION" a role cannot decide what other roles they are a member of.
It clearly can in some limited cases, because I showed an example
demonstrating *exactly that thing*.
I don't necessarily have an issue changing self-administration but if the motivating concern is that all these new pg_* roles we are creating are something a normal user can opt-out of/revoke that simply isn't the case today, unless they are added to the pg_* role WITH ADMIN OPTION.
I agree with this, but that's not my concern, because that's a
different use case from the one that I complained about. Since the
session user exception only applies to login roles, the problem that
I'm talking about only occurs when a login role is granted to some
other role.
That all said, permissions SHOULD BE strictly additive. If boss doesn't want to be a member of pg_read_all_files allowing them to revoke themself from that role seems like it should be acceptable. If there is fear in allowing someone to revoke (not add) themselves as a member of a different role that suggests we have a design issue in another feature of the system. Today, they neither grant nor revoke, and the self-revocation doesn't seem that important to add.
I disagree with this on principle, and I also think that's not how it
works today. On the general principle, I do not see a compelling
reason why we should have two systems for maintaining groups of users,
one of which is used for additive things and one of which is used for
subtractive things. That is a lot of extra machinery for little gain,
especially given how close we are to having it sorted out so that the
same mechanism can serve both purposes. It presently appears to me
that if we either remove the session user exception OR do the
grantor-tracking thing discussed earlier, we can get to a place where
the same facility can be used for either purpose. That would, I think,
be a significant step forward over the status quo. In terms of how
things work today, see Joshua Brindle's email about the use of groups
in pg_hba.conf. That is an excellent example of how removing oneself
from a group could enable one to bypass security restrictions intended
by the DBA.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
It appears to me that the actual behavior of having is_admin_of_role()
return true when member == role dates to
f9fd1764615ed5d85fab703b0ffb0c323fe7dfd5 (Tom Lane, 2005). If I'm not
reading this code wrong, prior to that commit, it seems to me that we
only searched the roles that were members of that role, directly or
indirectly, and you had to have admin_option on the last hop of the
membership chain in order to get a "true" result. But that commit,
among other changes, made member == role a special case, but the
comment just says /* Fast path for simple case */ which makes it
appear that it wasn't thought to be a behavior change at all, but it
looks to me like it was. Am I confused?
Ugh, I think you are right. It's been a long time of course, but it sure
looks like that was copied-and-pasted without recognizing that it was
wrong in this function because of the need to check the admin_option flag.
And then in the later security discussion we didn't realize that the
problematic behavior was a flat-out thinko, so we narrowed it as much as
we could instead of just taking it out.
Does anything interesting break if you do just take it out?
regards, tom lane
On Mon, Mar 7, 2022 at 1:28 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Ugh, I think you are right. It's been a long time of course, but it sure
looks like that was copied-and-pasted without recognizing that it was
wrong in this function because of the need to check the admin_option flag.
And then in the later security discussion we didn't realize that the
problematic behavior was a flat-out thinko, so we narrowed it as much as
we could instead of just taking it out.Does anything interesting break if you do just take it out?
That is an excellent question, but I haven't had time yet to
investigate the matter.
--
Robert Haas
EDB: http://www.enterprisedb.com
Greetings,
* Tom Lane (tgl@sss.pgh.pa.us) wrote:
Robert Haas <robertmhaas@gmail.com> writes:
1. What should be the exact rule for whether A can remove a grant made
by B? Is it has_privs_of_role()? is_member_of_role()? Something else?No strong opinion here, but I'd lean slightly to the more restrictive
option.2. What happens if the same GRANT is enacted by multiple users? For
example, suppose peon does "GRANT peon to boss" and then the superuser
does the same thing afterwards, or vice versa? One design would be to
try to track those as two separate grants, but I'm not sure if we want
to add that much complexity, since that's not how we do it now and it
would, for example, implicate the choice of PK on the pg_auth_members
table.As you note later, we *do* track such grants separately in ordinary
ACLs, and I believe this is clearly required by the SQL spec.
Agreed.
It says (for privileges on objects):
Each privilege is represented by a privilege descriptor.
A privilege descriptor contains:
— The identification of the object on which the privilege is granted.
— The <authorization identifier> of the grantor of the privilege.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
— The <authorization identifier> of the grantee of the privilege.
— Identification of the action that the privilege allows.
— An indication of whether or not the privilege is grantable.
— An indication of whether or not the privilege has the WITH HIERARCHY OPTION specified.Further down (4.42.3 in SQL:2021), the granting of roles is described,
and that says:Each role authorization is described by a role authorization descriptor.
A role authorization descriptor includes:
— The role name of the role.
— The authorization identifier of the grantor.
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
— The authorization identifier of the grantee.
— An indication of whether or not the role authorization is grantable.If we are not tracking the grantors of role authorizations,
then we are doing it wrong and we ought to fix that.
Yup, and as noted elsewhere, we are tracking it but not properly dealing
with dependencies nor are we considering the grantor when REVOKE is run.
Looking at the spec for REVOKE is quite useful when trying to understand
how this is all supposed to work (and, admittedly, isn't something I did
enough of when I did the original work on roles... sorry about that, was
early on). In particular, a REVOKE only works when it finds something
to revoke/remove, and part of that search includes basically "was it the
current role who was the grantor?"
The specific language here being: A role authorization descriptor is
said to be identified if it defines the grant of any of the specified
roles revoked to grantee with grantor A.
Basically, a role authorization descriptor isn't identified unless it's
one that this user/role had previously granted.
3. What happens if a user is dropped after being recorded as a
grantor?Should work the same as it does now for ordinary ACLs, ie, you
gotta drop the grant first.
Agreed.
4. Should we apply this rule to other types of grants, rather than
just to role membership?I am not sure about the reasoning behind the existing rule that
superuser-granted privileges are recorded as being granted by the
object owner. It does feel more like a wart than something we want.
It might have been a hack to deal with the lack of GRANTED BY
options in GRANT/REVOKE back in the day.
Yeah, that doesn't seem right and isn't great.
Changing it could have some bad compatibility consequences though.
In particular, I believe it would break existing pg_dump files,
in that after restore all privileges would be attributed to the
restoring superuser, and there'd be no very easy way to clean that
up.
Ugh, that's pretty grotty, certainly.
Please note that it is not really my intention to try to shove
anything into v15 here.Agreed, this is not something to move on quickly. We might want
to think about adjusting pg_dump to use explicit GRANTED BY
options in GRANT/REVOKE a release or two before making incompatible
changes.
I'm with Robert on this though- folks should know already that they need
to use the pg_dump of the version of PG that they want to move to and
not try to re-use older pg_dump output with newer versions, for a number
of reasons and this is just another.
Thanks,
Stephen
On Mon, Mar 7, 2022 at 11:18 AM Robert Haas <robertmhaas@gmail.com> wrote:
On Sun, Mar 6, 2022 at 11:01 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:The example, which you moved here, then attempts to demonstrate this
"fact" but gets it wrong. Boss became a member of peon so if you want to
demonstrate self-administration of a role's membership in a different group
you have to login as boss, not peon. Doing that, and then revoking peon
from boss, yields "ERROR: must have admin option on role "peon"".This doesn't seem to me to be making a constructive argument. I showed
an example with certain names demonstrating a certain behavior that I
find problematic.
Whether you choose the wording of the original thread:
"This is because we allow 'self administration' of roles, meaning that
they can decide what other roles they are a member of."
/messages/by-id/20211005025746.GN20998@tamriel.snowman.net
Or you quote at the top of this one:
The ability of a role to revoke itself from some other role is just
something we need to accept as being a change that needs to be made,
This example:
rhaas=# create user boss;
CREATE ROLE
rhaas=# create user peon;
CREATE ROLE
rhaas=# grant peon to boss;
GRANT ROLE
rhaas=# \c - peon
You are now connected to database "rhaas" as user "peon".
rhaas=> revoke peon from boss; -- i don't like being bossed around!
REVOKE ROLE
Fails to demonstrate the boss "can revoke itself from peon" / "boss can
decide what other roles they are a member of."
You are logged in as peon when you do the revoke, not boss, so the extent
of what "boss" can or cannot do has not been shown.
boss is a member of peon, not the other way around. That the wording
"grant peon to boss" makes you think otherwise is unfortunate.
David J.
Greetings,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Sun, Mar 6, 2022 at 11:34 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I was thinking the former ... however, after a bit of experimentation
I see that we accept "grant foo to bar granted by baz" a VERY long
way back, but the "granted by" option for object privileges is
(a) pretty new and (b) apparently restrictively implemented:regression=# grant delete on alices_table to bob granted by alice;
ERROR: grantor must be current userThat's ... surprising. I guess whoever put that in was only
interested in pro-forma SQL syntax compliance and not in making
a usable feature.It appears so: /messages/by-id/2073b6a9-7f79-5a00-5f26-cd19589a52c7@2ndquadrant.com
It doesn't seem like that would be hard to fix. Maybe we should just do that.
Yeah, that seems like something that should be fixed. Superusers should
be allowed to set GRANTED BY to whatever they feel like, and I'd argue
that a role who wants a GRANT to actually be GRANTED BY some other role
they're a member of should also be allowed to (as they could anyway by
doing a SET ROLE), provided that role also has the privileges to do the
GRANT itself, of course.
So if we decide to extend this change into object privileges
it would be advisable to use SET ROLE, else we'd be giving up
an awful lot of backwards compatibility in dump scripts.
But if we're only talking about role grants then I think
GRANTED BY would work fine.OK.
I'm not quite following this bit. Where would SET ROLE come into play
when we're talking about old dump scripts and how the commands in those
scripts might be interpreted by newer versions of PG..?
Thanks,
Stephen
Stephen Frost <sfrost@snowman.net> writes:
* Tom Lane (tgl@sss.pgh.pa.us) wrote:
Agreed, this is not something to move on quickly. We might want
to think about adjusting pg_dump to use explicit GRANTED BY
options in GRANT/REVOKE a release or two before making incompatible
changes.
I'm with Robert on this though- folks should know already that they need
to use the pg_dump of the version of PG that they want to move to and
not try to re-use older pg_dump output with newer versions, for a number
of reasons and this is just another.
Yeah, in an ideal world you'd do that, but our users don't always have
the luxury of living in an ideal world. Sometimes all you've got is
an old pg_dump file. Perhaps this behavior wouldn't mess things up
enough to make the restored database unusable, but we need to think
about (and test) that case while we're considering changes.
regards, tom lane
Stephen Frost <sfrost@snowman.net> writes:
I'm not quite following this bit. Where would SET ROLE come into play
when we're talking about old dump scripts and how the commands in those
scripts might be interpreted by newer versions of PG..?
No, the concern there is the other way around: what if you take a
script made by newer pg_dump and try to load it into an older server
that doesn't have the GRANTED BY option?
We're accustomed to saying that that doesn't work if you use a
database feature that didn't exist in the old server, but
privilege grants are hardly that. I don't want us to change the
pg_dump output in such a way that the grants can't be restored at all
to an older server, just because of a syntax choice that we could
make backwards-compatibly instead of not-backwards-compatibly.
regards, tom lane
Greetings,
* Tom Lane (tgl@sss.pgh.pa.us) wrote:
Stephen Frost <sfrost@snowman.net> writes:
* Tom Lane (tgl@sss.pgh.pa.us) wrote:
Agreed, this is not something to move on quickly. We might want
to think about adjusting pg_dump to use explicit GRANTED BY
options in GRANT/REVOKE a release or two before making incompatible
changes.I'm with Robert on this though- folks should know already that they need
to use the pg_dump of the version of PG that they want to move to and
not try to re-use older pg_dump output with newer versions, for a number
of reasons and this is just another.Yeah, in an ideal world you'd do that, but our users don't always have
the luxury of living in an ideal world. Sometimes all you've got is
an old pg_dump file. Perhaps this behavior wouldn't mess things up
enough to make the restored database unusable, but we need to think
about (and test) that case while we're considering changes.
I agree it's something to consider and deal with if we're able to do so
sanely, but I disagree that we should be beholden to old dump files when
considering how to move the project forward. Further, they can surely
build and install the version of PG that goes with that dump file in a
great many cases and then dump the data out using a newer version of
pg_dump. For 5 years they could do that with a completely supported
version of PG, but we've recently agreed to make an effort to do more
here by supporting the building of even older versions on modern
systems.
Thanks,
Stephen
On Mon, Mar 7, 2022 at 1:58 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Stephen Frost <sfrost@snowman.net> writes:
I'm not quite following this bit. Where would SET ROLE come into play
when we're talking about old dump scripts and how the commands in those
scripts might be interpreted by newer versions of PG..?No, the concern there is the other way around: what if you take a
script made by newer pg_dump and try to load it into an older server
that doesn't have the GRANTED BY option?We're accustomed to saying that that doesn't work if you use a
database feature that didn't exist in the old server, but
privilege grants are hardly that. I don't want us to change the
pg_dump output in such a way that the grants can't be restored at all
to an older server, just because of a syntax choice that we could
make backwards-compatibly instead of not-backwards-compatibly.
Are you absolutely positive that it's that simple? I mean, what if the
SET ROLE command has other side effects, or if the GRANT command
behaves differently in some way as a result of the SET ROLE having
been done? I feel like a solution that involves explicitly specifying
the behavior that we want (i.e. GRANTED BY) is likely to be more
reliable and more secure than a solution which involves absorbing a
key value from a session property (i.e. the role established by SET
ROLE). Even if we decide that SET ROLE is the way to go for
compatibility reasons, I would personally say that it's an inferior
hack only worth accepting for that reason than a truly desirable
design.
See CVE-2018-1058 for an example of what I'm talking about. The
prevailing search_path turned out to affect not only the creation
schema, as intended, but also the resolution of references to other
objects mentioned in the CREATE COMMAND, as not intended. I don't see
a similar hazard here, but I'm worried that there might be one.
Declarative syntax is a very powerful tool for avoiding those kinds of
mishaps, and I think we should make as much use of it as we can.
--
Robert Haas
EDB: http://www.enterprisedb.com
Greetings,
* Tom Lane (tgl@sss.pgh.pa.us) wrote:
Stephen Frost <sfrost@snowman.net> writes:
I'm not quite following this bit. Where would SET ROLE come into play
when we're talking about old dump scripts and how the commands in those
scripts might be interpreted by newer versions of PG..?No, the concern there is the other way around: what if you take a
script made by newer pg_dump and try to load it into an older server
that doesn't have the GRANTED BY option?
Wow. No, I really don't think I can agree that we need to care about
this.
We're accustomed to saying that that doesn't work if you use a
database feature that didn't exist in the old server, but
privilege grants are hardly that. I don't want us to change the
pg_dump output in such a way that the grants can't be restored at all
to an older server, just because of a syntax choice that we could
make backwards-compatibly instead of not-backwards-compatibly.
GRANTED BY is clearly such a feature that exists in the newer version
and doesn't exist in the older and I can't agree that we should
complicate things for ourselves and bend over backwards to try and make
it work to take a dump from a newer version of PG and make it work on
random older versions.
Folks are also able to exclude privileges from dumps if they want to.
Where do we document that we are going to put in effort to make these
kinds of things work? What other guarantees are we supposed to be
providing regarding using output from a newer pg_dump against older
servers? What about newer custom format dumps? Surely you're not
suggesting that we need to back-patch support for them to released
versions of pg_restore.
Thanks,
Stephen
On Mon, Mar 7, 2022 at 11:18 AM Robert Haas <robertmhaas@gmail.com> wrote:
In terms of how
things work today, see Joshua Brindle's email about the use of groups
in pg_hba.conf. That is an excellent example of how removing oneself
from a group could enable one to bypass security restrictions intended
by the DBA.
You mean the one that was based upon your "ooops"...I discounted that
immediately because members cannot revoke their own membership in a group
unless they were given WITH ADMIN OPTION on that group.
The mere fact that the pg_hba.conf concern raised there hasn't been
reported as a live issue suggests the lack of any meaningful design flaw
here.
That isn't to say that having a LOGIN role get an automatic temporary WITH
ADMIN OPTION on itself is a good thing - but there isn't any privilege
escalation vector here to be squashed. There is just a "DBAs should treat
LOGIN roles as leaf nodes" expectation in which case there would be no
superuser granted memberships to be removed.
David J.
On Mon, Mar 7, 2022 at 2:29 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:
You mean the one that was based upon your "ooops"...I discounted that immediately because members cannot revoke their own membership in a group unless they were given WITH ADMIN OPTION on that group.
Oh, hmm. That example might be backwards from the case I'm talking about.
The mere fact that the pg_hba.conf concern raised there hasn't been reported as a live issue suggests the lack of any meaningful design flaw here.
Not really. The system is full of old bugs, just as all software
system are, and the particular role self-administration behavior that
is at issue here appears to be something that was accidentally
introduced 16 years years ago in a commit that did something else and
never scrutinized from a design perspective since then.
Personally, I've been shocked by the degree to which this entire area
seems to be full of design flaws and half-baked code. I mean, just the
fact that the pg_auth_members.grantor can be left pointing to a role
OID that no longer exists is pretty crazy, right? I don't think anyone
today would consider something with that kind of wart committable.
That isn't to say that having a LOGIN role get an automatic temporary WITH ADMIN OPTION on itself is a good thing - but there isn't any privilege escalation vector here to be squashed. There is just a "DBAs should treat LOGIN roles as leaf nodes" expectation in which case there would be no superuser granted memberships to be removed.
Well, we may not have found one yet, but that doesn't prove none
exists. In any case, if we can agree that it's not necessarily a
desirable behavior, that's good enough for me.
(I still disagree with the idea that LOGIN roles have to be leaf
nodes. We could have a system where that's true, but that's not how
the system we actually have is designed.)
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mar 7, 2022, at 10:28 AM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
Does anything interesting break if you do just take it out?
SET SESSION AUTHORIZATION regress_priv_group2;
GRANT regress_priv_group2 TO regress_priv_user5; -- ok: a role can self-admin
-NOTICE: role "regress_priv_user5" is already a member of role "regress_priv_group2"
+ERROR: must have admin option on role "regress_priv_group2"
This test failure is just a manifestation of the intended change, but assuming we make no other changes, the error message would clearly need to be updated, because it suggests the role should have admin_option on itself, a situation which is not currently supported.
Perhaps we should support that, though, by adding a reflexive aclitem[] to pg_authid (meaning it tracks which privileges a role has on itself) with tracking of who granted it, so that revocation can be handled properly. The aclitem could start out null, meaning the role has by default the traditional limited self-admin which the code comments discuss:
/*
* A role can admin itself when it matches the session user and we're
* outside any security-restricted operation, SECURITY DEFINER or
* similar context. SQL-standard roles cannot self-admin. However,
* SQL-standard users are distinct from roles, and they are not
* grantable like roles: PostgreSQL's role-user duality extends the
* standard. Checking for a session user match has the effect of
* letting a role self-admin only when it's conspicuously behaving
* like a user. Note that allowing self-admin under a mere SET ROLE
* would make WITH ADMIN OPTION largely irrelevant; any member could
* SET ROLE to issue the otherwise-forbidden command.
*
* Withholding self-admin in a security-restricted operation prevents
* object owners from harnessing the session user identity during
* administrative maintenance. Suppose Alice owns a database, has
* issued "GRANT alice TO bob", and runs a daily ANALYZE. Bob creates
* an alice-owned SECURITY DEFINER function that issues "REVOKE alice
* FROM carol". If he creates an expression index calling that
* function, Alice will attempt the REVOKE during each ANALYZE.
* Checking InSecurityRestrictedOperation() thwarts that attack.
*
* Withholding self-admin in SECURITY DEFINER functions makes their
* behavior independent of the calling user. There's no security or
* SQL-standard-conformance need for that restriction, though.
*
* A role cannot have actual WITH ADMIN OPTION on itself, because that
* would imply a membership loop. Therefore, we're done either way.
*/
For non-null aclitem[], we could support REVOKE ADMIN OPTION FOR joe FROM joe, and for explicit re-grants, we could track who granted it, such that further revocations could properly refuse if the revoker doesn't have sufficient privileges vis-a-vis the role that granted it in the first place.
I have not yet tried to implement this, and might quickly hit problems with the idea, but will take a stab at a proof-of-concept patch unless you suggest a better approach.
Thoughts?
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Mon, Mar 7, 2022 at 2:59 PM Mark Dilger <mark.dilger@enterprisedb.com> wrote:
This test failure is just a manifestation of the intended change, but assuming we make no other changes, the error message would clearly need to be updated, because it suggests the role should have admin_option on itself, a situation which is not currently supported.
It's been pointed out upthread that this would have undesirable
security implications, because the admin option would be inherited,
and the implicit permission isn't.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mar 7, 2022, at 12:01 PM, Robert Haas <robertmhaas@gmail.com> wrote:
It's been pointed out upthread that this would have undesirable
security implications, because the admin option would be inherited,
and the implicit permission isn't.
Right, but with a reflexive self-admin-option, we could document that it works in a non-inherited way. We'd just be saying the current hard-coded behavior is an option which can be revoked rather than something you're stuck with.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Mar 7, 2022, at 12:03 PM, Mark Dilger <mark.dilger@enterprisedb.com> wrote:
Right, but with a reflexive self-admin-option, we could document that it works in a non-inherited way. We'd just be saying the current hard-coded behavior is an option which can be revoked rather than something you're stuck with.
We could also say that the default is to not have admin option on yourself, with that being something grantable, but that is a larger change from the historical behavior and might have more consequences for dump/restore, etc.
My concern about just nuking self-admin is that there may be sites which use self-admin and we'd be leaving them without a simple work-around after upgrade, because they couldn't restore the behavior by executing a grant. They'd have to more fundamentally restructure their role relationships to not depend on self-admin, something which might be harder for them to do. Perhaps nobody is using self-admin, or very few people are using it, and I'm being overly concerned.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Mark Dilger <mark.dilger@enterprisedb.com> writes:
On Mar 7, 2022, at 12:01 PM, Robert Haas <robertmhaas@gmail.com> wrote:
It's been pointed out upthread that this would have undesirable
security implications, because the admin option would be inherited,
and the implicit permission isn't.
Right, but with a reflexive self-admin-option, we could document that it works in a non-inherited way. We'd just be saying the current hard-coded behavior is an option which can be revoked rather than something you're stuck with.
After reflection, I think that role self-admin is probably a bad idea that
we should stay away from. It could perhaps be reasonable given some other
system design and/or syntax than what SQL gives us, but we're dealing in
SQL. It doesn't make sense to GRANT a role to itself, and therefore it
likewise doesn't make sense to GRANT WITH ADMIN OPTION.
Based on Robert's archaeological dig, it now seems that the fact that
we have any such behavior at all was just a mistake. What would be
lost if we drop it?
Having said that, one thing that I find fishy is that it's not clear
where the admin privilege for a role originates. After "CREATE ROLE
alice", alice has no members, therefore none that have admin privilege,
therefore the only way that the first member could be added is via
superuser deus ex machina. This does not seem clean. If we recorded
which user created the role, we could act as though that user has
admin privilege (whether or not it's a member). Perhaps I'm
reinventing something that was already discussed upthread. I wonder
what the SQL spec has to say on this point, too.
regards, tom lane
On Mon, Mar 7, 2022 at 1:16 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Based on Robert's archaeological dig, it now seems that the fact that
we have any such behavior at all was just a mistake. What would be
lost if we drop it?
Probably nothing that couldn't be replaced, and with a better model, but I
do have a concern that there are setups in the wild inadvertently using
this behavior. Enough so that I would vote to change it but include a
migration GUC to restore the current behavior, probably with a deprecation
warning. Kinda depends on the post-change dump/restore mechanics. But
just tearing it out wouldn't seem extraordinary for us.
Having said that, one thing that I find fishy is that it's not clear
where the admin privilege for a role originates.
I do not see a problem with there being no inherent admin privilege for a
role. A superuser or CREATEROLE user holds admin privilege on all roles in
the cluster. They can delegate the privilege to administer a role to yet
another role in the system. The necessitates creating two roles - the one
being administered and the one being delegated to. I don't see a benefit
to saving which specific superuser or CREATEROLE user "owns" the role that
is to be administered. Not unless non-owner CREATEROLE users are prevented
from exercising admin privileges on the role. That all said, I'd accept
the choice to include such ownership information as a requirement for
meeting the auditing needs of DBAs. But I would argue that such auditing
probably needs to be external to the working system - the fact that
ownership can be changed reduces the benefit of an in-database value.
If we recorded
which user created the role, we could act as though that user has
admin privilege (whether or not it's a member).
I suppose we could record the current owner of a role but that seems
unnecessary. I dislike using the "created" concept by virtue of the fact
that, for routines, "security definer" implies creator but it actually
means "security owner".
David J.
On Mar 7, 2022, at 12:16 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
What would be
lost if we drop it?
I looked into this a bit. Removing that bit of code, the only regression test changes for "check-world" are the expected ones, with nothing else breaking. Running installcheck+pg_upgrade to the patched version of HEAD from each of versions 11, 12, 13 and 14 doesn't turn up anything untoward. The change I used (for reference) is attached:
Attachments:
v1-0001-WIP-Removing-role-self-administration.patch.WIPapplication/octet-stream; name=v1-0001-WIP-Removing-role-self-administration.patch.WIP; x-unix-mode=0644Download
From 57e0282cdfe1862e234ba880b74540c8fd771157 Mon Sep 17 00:00:00 2001
From: Mark Dilger <mark.dilger@enterprisedb.com>
Date: Mon, 7 Mar 2022 20:12:09 -0800
Subject: [PATCH v1] WIP: Removing role self-administration
---
src/backend/utils/adt/acl.c | 33 ------------------------
src/test/regress/expected/privileges.out | 4 +--
src/test/regress/sql/privileges.sql | 2 +-
3 files changed, 3 insertions(+), 36 deletions(-)
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 0a16f8156c..659f6db955 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4935,39 +4935,6 @@ is_admin_of_role(Oid member, Oid role)
if (superuser_arg(member))
return true;
- if (member == role)
-
- /*
- * A role can admin itself when it matches the session user and we're
- * outside any security-restricted operation, SECURITY DEFINER or
- * similar context. SQL-standard roles cannot self-admin. However,
- * SQL-standard users are distinct from roles, and they are not
- * grantable like roles: PostgreSQL's role-user duality extends the
- * standard. Checking for a session user match has the effect of
- * letting a role self-admin only when it's conspicuously behaving
- * like a user. Note that allowing self-admin under a mere SET ROLE
- * would make WITH ADMIN OPTION largely irrelevant; any member could
- * SET ROLE to issue the otherwise-forbidden command.
- *
- * Withholding self-admin in a security-restricted operation prevents
- * object owners from harnessing the session user identity during
- * administrative maintenance. Suppose Alice owns a database, has
- * issued "GRANT alice TO bob", and runs a daily ANALYZE. Bob creates
- * an alice-owned SECURITY DEFINER function that issues "REVOKE alice
- * FROM carol". If he creates an expression index calling that
- * function, Alice will attempt the REVOKE during each ANALYZE.
- * Checking InSecurityRestrictedOperation() thwarts that attack.
- *
- * Withholding self-admin in SECURITY DEFINER functions makes their
- * behavior independent of the calling user. There's no security or
- * SQL-standard-conformance need for that restriction, though.
- *
- * A role cannot have actual WITH ADMIN OPTION on itself, because that
- * would imply a membership loop. Therefore, we're done either way.
- */
- return member == GetSessionUserId() &&
- !InLocalUserIdChange() && !InSecurityRestrictedOperation();
-
(void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &result);
return result;
}
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 291e21d7a6..73728eb23f 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -1555,8 +1555,8 @@ SET ROLE regress_priv_group2;
GRANT regress_priv_group2 TO regress_priv_user5; -- fails: SET ROLE did not help
ERROR: must have admin option on role "regress_priv_group2"
SET SESSION AUTHORIZATION regress_priv_group2;
-GRANT regress_priv_group2 TO regress_priv_user5; -- ok: a role can self-admin
-NOTICE: role "regress_priv_user5" is already a member of role "regress_priv_group2"
+GRANT regress_priv_group2 TO regress_priv_user5; -- fail: a role cannot self-admin
+ERROR: must have admin option on role "regress_priv_group2"
CREATE FUNCTION dogrant_fails() RETURNS void LANGUAGE sql SECURITY DEFINER AS
'GRANT regress_priv_group2 TO regress_priv_user5';
SELECT dogrant_fails(); -- fails: no self-admin in SECURITY DEFINER
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index c8c545b64c..2047be7d8b 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -981,7 +981,7 @@ SET ROLE regress_priv_group2;
GRANT regress_priv_group2 TO regress_priv_user5; -- fails: SET ROLE did not help
SET SESSION AUTHORIZATION regress_priv_group2;
-GRANT regress_priv_group2 TO regress_priv_user5; -- ok: a role can self-admin
+GRANT regress_priv_group2 TO regress_priv_user5; -- fail: a role cannot self-admin
CREATE FUNCTION dogrant_fails() RETURNS void LANGUAGE sql SECURITY DEFINER AS
'GRANT regress_priv_group2 TO regress_priv_user5';
SELECT dogrant_fails(); -- fails: no self-admin in SECURITY DEFINER
--
2.21.1 (Apple Git-122.3)
On 07.03.22 19:18, Robert Haas wrote:
That all said, permissions SHOULD BE strictly additive. If boss doesn't want to be a member of pg_read_all_files allowing them to revoke themself from that role seems like it should be acceptable. If there is fear in allowing someone to revoke (not add) themselves as a member of a different role that suggests we have a design issue in another feature of the system. Today, they neither grant nor revoke, and the self-revocation doesn't seem that important to add.
I disagree with this on principle, and I also think that's not how it
works today. On the general principle, I do not see a compelling
reason why we should have two systems for maintaining groups of users,
one of which is used for additive things and one of which is used for
subtractive things.
Do we have subtractive permissions today?
On Wed, Mar 9, 2022 at 7:55 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:
Do we have subtractive permissions today?
Not in the GRANT/REVOKE sense, I think, but you can put a user in a
group and then mention that group in pg_hba.conf. And that line might
be "reject" or whatever.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mon, Mar 7, 2022 at 11:14 PM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:
On Mar 7, 2022, at 12:16 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
What would be
lost if we drop it?I looked into this a bit. Removing that bit of code, the only regression test changes for "check-world" are the expected ones, with nothing else breaking. Running installcheck+pg_upgrade to the patched version of HEAD from each of versions 11, 12, 13 and 14 doesn't turn up anything untoward.
I looked into this a bit, too. I attach a draft patch for removing the
self-admin exception.
I found that having is_admin_of_role() return true matters in three
ways: (1) It lets you grant membership in the role to some other role.
(2) It lets you revoke membership in the role from some other role.
(3) It changes the return value of pg_role_aclcheck(), which is used
in the implementation of various SQL-callable functions all invoked
via the name pg_has_role(). We've mostly been discussing (2) as an
issue, but (1) and (3) are pretty interesting too. Regarding (3),
there is a comment in the code indicating that Noah considered the
self-admin exception something of a wart as far as pg_has_role() is
concerned. As to (1), I discovered that today you can do this:
rhaas=# create user foo;
CREATE ROLE
rhaas=# create user bar;
CREATE ROLE
rhaas=# \q
[rhaas ~]$ psql -U foo rhaas
psql (15devel)
Type "help" for help.
rhaas=> grant foo to bar with admin option;
GRANT ROLE
I don't know why I didn't realize that before. It's a natural result
of treating the logged-in user as if they had admin option. But it's
weird that you can't even be granted WITH ADMIN OPTION on your own
login role, but at the same time without having it you can grant it to
someone else!
I believe there are three other points worth some consideration here.
First, in the course of my investigation I re-discovered what Tom
already did a good job articulating:
tgl> Having said that, one thing that I find fishy is that it's not clear
tgl> where the admin privilege for a role originates. After "CREATE ROLE
tgl> alice", alice has no members, therefore none that have admin privilege,
tgl> therefore the only way that the first member could be added is via
tgl> superuser deus ex machina. This does not seem clean.
I agree with that, but I don't think it's a sufficient reason for
keeping the self-admin exception, because the same problem exists for
non-login roles. I don't even think it's the right idea conceptually
to suppose that the power to administer a role originates from the
role itself. If that were so, then it would be inherited by all
members of the role along with all the rest of the role's privileges,
which is so clearly not right that we've already prohibited a role
from having WITH ADMIN OPTION on itself. In my opinion, the right to
administer a role - regardless of whether or not it is a login role -
most naturally vests in the role that created it, or something in that
direction at least, if not that exact thing. Today, that means the
superuser or a CREATEROLE user who could hack superuser if they
wished. In the future, I hope for other alternatives, as recently
argued on other threads. But we need not resolve the question of how
that should work exactly in order to agree (as I hope we do) that
doubling down on the self-administration exception is not the answer.
Second, it occured to me to wonder what implications a change like
this might have for dump and restore. If privilege restoration somehow
relied on this behavior, then we'd have a problem. But I don't think
it does, because (a) pg_dump can SET ROLE but can't change the session
user without reconnecting, so it's unclear how we could be relying on
it; (b) it wouldn't work for non-login roles, and it's unlikely that
we would treat login and non-login roles different in terms of
restoring privileges, and (c) when I execute the example shown above
and then run pg_dump, there's no attempt to change the current user,
it just dumps "GRANT foo TO bar WITH ADMIN OPTION GRANTED BY foo".
Third, it occurred to me to wonder whether some users might be using
and relying upon this behavior. It's certainly possible, and it does
suck that we'd be removing it without providing a workable substitute.
But it's probably not a LOT of users because most people who have
commented on this topic on this mailing list seem to find granting
membership in a login role a super-weird thing to do, because a lot of
people really seem to want every role to be a user or a group, and a
login role with members feels like it's blurring that line. I'm
inclined to think that the small number of people who may be unhappy
is an acceptable price to pay for removing this wart, but it's a
judgement call and if someone has information to suggest that I'm
wrong, it'd be good to hear about that.
Thanks,
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
remove-self-admin.patchapplication/octet-stream; name=remove-self-admin.patchDownload
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index a897712de2..8c4edd9b0a 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -251,11 +251,10 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
in turn grant membership in the role to others, and revoke membership
in the role as well. Without the admin option, ordinary users cannot
do that. A role is not considered to hold <literal>WITH ADMIN
- OPTION</literal> on itself, but it may grant or revoke membership in
- itself from a database session where the session user matches the
- role. Database superusers can grant or revoke membership in any role
- to anyone. Roles having <literal>CREATEROLE</literal> privilege can grant
- or revoke membership in any role that is not a superuser.
+ OPTION</literal> on itself. Database superusers can grant or revoke
+ membership in any role to anyone. Roles having
+ <literal>CREATEROLE</literal> privilege can grant or revoke membership
+ in any role that is not a superuser.
</para>
<para>
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index f9d3c1246b..c263f6c8b9 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -1425,11 +1425,6 @@ AddRoleMems(const char *rolename, Oid roleid,
* The role membership grantor of record has little significance at
* present. Nonetheless, inasmuch as users might look to it for a crude
* audit trail, let only superusers impute the grant to a third party.
- *
- * Before lifting this restriction, give the member == role case of
- * is_admin_of_role() a fresh look. Ensure that the current role cannot
- * use an explicit grantor specification to take advantage of the session
- * user's self-admin right.
*/
if (grantorId != GetUserId() && !superuser())
ereport(ERROR,
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 0a16f8156c..894da4891a 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4619,11 +4619,6 @@ pg_role_aclcheck(Oid role_oid, Oid roleid, AclMode mode)
{
if (mode & ACL_GRANT_OPTION_FOR(ACL_CREATE))
{
- /*
- * XXX For roleid == role_oid, is_admin_of_role() also examines the
- * session and call stack. That suits two-argument pg_has_role(), but
- * it gives the three-argument version a lamentable whimsy.
- */
if (is_admin_of_role(roleid, role_oid))
return ACLCHECK_OK;
}
@@ -4935,38 +4930,12 @@ is_admin_of_role(Oid member, Oid role)
if (superuser_arg(member))
return true;
+ /*
+ * A role cannot have WITH ADMIN OPTION on itself, because that would
+ * imply a membership loop.
+ */
if (member == role)
-
- /*
- * A role can admin itself when it matches the session user and we're
- * outside any security-restricted operation, SECURITY DEFINER or
- * similar context. SQL-standard roles cannot self-admin. However,
- * SQL-standard users are distinct from roles, and they are not
- * grantable like roles: PostgreSQL's role-user duality extends the
- * standard. Checking for a session user match has the effect of
- * letting a role self-admin only when it's conspicuously behaving
- * like a user. Note that allowing self-admin under a mere SET ROLE
- * would make WITH ADMIN OPTION largely irrelevant; any member could
- * SET ROLE to issue the otherwise-forbidden command.
- *
- * Withholding self-admin in a security-restricted operation prevents
- * object owners from harnessing the session user identity during
- * administrative maintenance. Suppose Alice owns a database, has
- * issued "GRANT alice TO bob", and runs a daily ANALYZE. Bob creates
- * an alice-owned SECURITY DEFINER function that issues "REVOKE alice
- * FROM carol". If he creates an expression index calling that
- * function, Alice will attempt the REVOKE during each ANALYZE.
- * Checking InSecurityRestrictedOperation() thwarts that attack.
- *
- * Withholding self-admin in SECURITY DEFINER functions makes their
- * behavior independent of the calling user. There's no security or
- * SQL-standard-conformance need for that restriction, though.
- *
- * A role cannot have actual WITH ADMIN OPTION on itself, because that
- * would imply a membership loop. Therefore, we're done either way.
- */
- return member == GetSessionUserId() &&
- !InLocalUserIdChange() && !InSecurityRestrictedOperation();
+ return false;
(void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &result);
return result;
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 291e21d7a6..cc3b6e116c 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -1555,14 +1555,8 @@ SET ROLE regress_priv_group2;
GRANT regress_priv_group2 TO regress_priv_user5; -- fails: SET ROLE did not help
ERROR: must have admin option on role "regress_priv_group2"
SET SESSION AUTHORIZATION regress_priv_group2;
-GRANT regress_priv_group2 TO regress_priv_user5; -- ok: a role can self-admin
-NOTICE: role "regress_priv_user5" is already a member of role "regress_priv_group2"
-CREATE FUNCTION dogrant_fails() RETURNS void LANGUAGE sql SECURITY DEFINER AS
- 'GRANT regress_priv_group2 TO regress_priv_user5';
-SELECT dogrant_fails(); -- fails: no self-admin in SECURITY DEFINER
+GRANT regress_priv_group2 TO regress_priv_user5; -- fails: no self-admin
ERROR: must have admin option on role "regress_priv_group2"
-CONTEXT: SQL function "dogrant_fails" statement 1
-DROP FUNCTION dogrant_fails();
SET SESSION AUTHORIZATION regress_priv_user4;
DROP FUNCTION dogrant_ok();
REVOKE regress_priv_group2 FROM regress_priv_user5;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index c8c545b64c..49bf1f2a62 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -981,11 +981,7 @@ SET ROLE regress_priv_group2;
GRANT regress_priv_group2 TO regress_priv_user5; -- fails: SET ROLE did not help
SET SESSION AUTHORIZATION regress_priv_group2;
-GRANT regress_priv_group2 TO regress_priv_user5; -- ok: a role can self-admin
-CREATE FUNCTION dogrant_fails() RETURNS void LANGUAGE sql SECURITY DEFINER AS
- 'GRANT regress_priv_group2 TO regress_priv_user5';
-SELECT dogrant_fails(); -- fails: no self-admin in SECURITY DEFINER
-DROP FUNCTION dogrant_fails();
+GRANT regress_priv_group2 TO regress_priv_user5; -- fails: no self-admin
SET SESSION AUTHORIZATION regress_priv_user4;
DROP FUNCTION dogrant_ok();
Robert Haas <robertmhaas@gmail.com> writes:
On Mar 7, 2022, at 12:16 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
tgl> Having said that, one thing that I find fishy is that it's not clear
tgl> where the admin privilege for a role originates. After "CREATE ROLE
tgl> alice", alice has no members, therefore none that have admin privilege,
tgl> therefore the only way that the first member could be added is via
tgl> superuser deus ex machina. This does not seem clean.
I agree with that, but I don't think it's a sufficient reason for
keeping the self-admin exception, because the same problem exists for
non-login roles. I don't even think it's the right idea conceptually
to suppose that the power to administer a role originates from the
role itself.
Actually, that's the same thing I was trying to say. But if it doesn't
originate from the role itself, where does it originate from?
In my opinion, the right to
administer a role - regardless of whether or not it is a login role -
most naturally vests in the role that created it, or something in that
direction at least, if not that exact thing.
This seems like a reasonable answer to me too: the creating role has admin
option implicitly, and can then choose to grant that to other roles.
Obviously some work needs to be done to make that happen (and we should
see whether the SQL spec has some different idea).
regards, tom lane
On Wed, Mar 9, 2022 at 4:01 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
In my opinion, the right to
administer a role - regardless of whether or not it is a login role -
most naturally vests in the role that created it, or something in that
direction at least, if not that exact thing.This seems like a reasonable answer to me too: the creating role has admin
option implicitly, and can then choose to grant that to other roles.
Obviously some work needs to be done to make that happen (and we should
see whether the SQL spec has some different idea).
Well, the problem is that as far as I can see, the admin option is an
optional feature of membership. You can grant someone membership
without admin option, or with admin option, but you can't grant them
the admin option without membership, just like you can't purchase an
upgrade to first class without the underlying plane ticket. What would
the syntax look even like for this? GRANT foo TO bar WITH ADMIN OPTION
BUT WITHOUT MEMBERSHIP? Yikes.
But do we really have to solve this problem before we can clean up
this session exception? I hope not, because I think that's a much
bigger can of worms than this is.
--
Robert Haas
EDB: http://www.enterprisedb.com
Greetings,
* Tom Lane (tgl@sss.pgh.pa.us) wrote:
Robert Haas <robertmhaas@gmail.com> writes:
On Mar 7, 2022, at 12:16 PM, Tom Lane <tgl@sss.pgh.pa.us> wrote:
tgl> Having said that, one thing that I find fishy is that it's not clear
tgl> where the admin privilege for a role originates. After "CREATE ROLE
tgl> alice", alice has no members, therefore none that have admin privilege,
tgl> therefore the only way that the first member could be added is via
tgl> superuser deus ex machina. This does not seem clean.I agree with that, but I don't think it's a sufficient reason for
keeping the self-admin exception, because the same problem exists for
non-login roles. I don't even think it's the right idea conceptually
to suppose that the power to administer a role originates from the
role itself.Actually, that's the same thing I was trying to say. But if it doesn't
originate from the role itself, where does it originate from?In my opinion, the right to
administer a role - regardless of whether or not it is a login role -
most naturally vests in the role that created it, or something in that
direction at least, if not that exact thing.This seems like a reasonable answer to me too: the creating role has admin
option implicitly, and can then choose to grant that to other roles.
I agree that this has some appeal, but it's not desirable in all cases
and so I wouldn't want it to be fully baked into the system ala the role
'owner' concept.
Obviously some work needs to be done to make that happen (and we should
see whether the SQL spec has some different idea).
Agreed on this, though I don't recall it having much to say on it.
Thanks,
Stephen
Greetings,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Wed, Mar 9, 2022 at 4:01 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
In my opinion, the right to
administer a role - regardless of whether or not it is a login role -
most naturally vests in the role that created it, or something in that
direction at least, if not that exact thing.This seems like a reasonable answer to me too: the creating role has admin
option implicitly, and can then choose to grant that to other roles.
Obviously some work needs to be done to make that happen (and we should
see whether the SQL spec has some different idea).Well, the problem is that as far as I can see, the admin option is an
optional feature of membership. You can grant someone membership
without admin option, or with admin option, but you can't grant them
the admin option without membership, just like you can't purchase an
upgrade to first class without the underlying plane ticket. What would
the syntax look even like for this? GRANT foo TO bar WITH ADMIN OPTION
BUT WITHOUT MEMBERSHIP? Yikes.
I've been meaning to reply to your other email regarding this, but I
don't really agree that the syntax ends up being so terrible or
difficult to deal with, considering we have these same general things
for ALTER ROLE already and there hasn't been all that much complaining.
That is, we have LOGIN and NOLOGIN, CREATEROLE and NOCREATEROLE, and we
could have MEMBERSHIP and NOMEMBERSHIP pretty easily here if we wanted
to.
But do we really have to solve this problem before we can clean up
this session exception? I hope not, because I think that's a much
bigger can of worms than this is.
I do believe we can deal with the above independently and at a later
time and go ahead and clean up the session excepton bit without dealing
with the above at the same time.
Thanks,
Stephen
I wrote:
This seems like a reasonable answer to me too: the creating role has admin
option implicitly, and can then choose to grant that to other roles.
Obviously some work needs to be done to make that happen (and we should
see whether the SQL spec has some different idea).
Ah, here we go: it's buried under CREATE ROLE. SQL:2021 12.4 <role
definition> saith that when role A executes CREATE ROLE <role name>,
then
1) A grantable role authorization descriptor is created whose role name
is <role name>, whose grantor is "_SYSTEM", and whose grantee is A.
Since nobody is _SYSTEM, this grant can't be deleted except by dropping
the new role (or, maybe, dropping A?). So that has nearly the same
end result as "the creating role has admin option implicitly". The main
difference I can see is that it also means the creating role is a *member*
implicitly, which is something I'd argue we don't want to enforce. This
is analogous to the way we let an object owner revoke her own ordinary
permissions, which the SQL model doesn't allow since those permissions
were granted to her by _SYSTEM.
regards, tom lane
Robert Haas <robertmhaas@gmail.com> writes:
Well, the problem is that as far as I can see, the admin option is an
optional feature of membership. You can grant someone membership
without admin option, or with admin option, but you can't grant them
the admin option without membership, just like you can't purchase an
upgrade to first class without the underlying plane ticket. What would
the syntax look even like for this? GRANT foo TO bar WITH ADMIN OPTION
BUT WITHOUT MEMBERSHIP? Yikes.
I don't think we need syntax to describe it. As I just said in my
other reply, we have a perfectly good precedent for this already
in ordinary object permissions. That is: an object owner always,
implicitly, has GRANT OPTION for all the object's privileges, even
if she revoked the corresponding plain privilege from herself.
Yeah, this does mean that we're effectively deciding that the creator
of a role is its owner. What's the problem with that?
But do we really have to solve this problem before we can clean up
this session exception?
I think we need a plan for where we're going. I don't see "clean up
the session exception" as an end in itself; it's part of re-examining
how all of this ought to work. I don't say that we have to have a
complete patch right away, only that we need a coherent end goal.
regards, tom lane
On Wed, Mar 9, 2022 at 2:31 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Robert Haas <robertmhaas@gmail.com> writes:
Well, the problem is that as far as I can see, the admin option is an
optional feature of membership. You can grant someone membership
without admin option, or with admin option, but you can't grant them
the admin option without membership, just like you can't purchase an
upgrade to first class without the underlying plane ticket. What would
the syntax look even like for this? GRANT foo TO bar WITH ADMIN OPTION
BUT WITHOUT MEMBERSHIP? Yikes.I don't think we need syntax to describe it. As I just said in my
other reply, we have a perfectly good precedent for this already
in ordinary object permissions. That is: an object owner always,
implicitly, has GRANT OPTION for all the object's privileges, even
if she revoked the corresponding plain privilege from herself.
So CREATE ROLE will assign ownership of AND membership in the newly created
role to the session_user UNLESS the OWNER clause is present in which case
the named role, so long as the session_user can SET ROLE to the named role,
becomes the owner & member. Subsequent to that the owner can issue: REVOKE
new_role FROM role_name where role_name is again the session_user role or
one that can be SET ROLE to.
Yeah, this does mean that we're effectively deciding that the creator
of a role is its owner. What's the problem with that?
I'm fine with this. It does introduce an OWNER concept to roles and so at
minimum we need to add:
ALTER ROLE foo OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER |
SESSION_USER }
And similar for CREATE ROLE
And keep the USER alias commands in sync.
GROUP commands are only present for backward compatibility and so don't get
updated with new features by design.
Obviously a superuser can change ownership.
Playing with table ownership I find this behavior:
-- superuser
CREATE ROLE tblowner;
CREATE TABLE tblowner_test (id serial primary key);
ALTER TABLE tblowner_test OWNER TO tblowner;
CREATE ROLE boss;
GRANT boss TO tblowner;
SET SESSION AUTHORIZATION tblowner;
ALTER TABLE tblowner_test OWNER TO boss; --works
So tblowner can push their ownership attribute to any group they are a
member of. Is that the behavior we want for roles as well?
David J.
"David G. Johnston" <david.g.johnston@gmail.com> writes:
So CREATE ROLE will assign ownership of AND membership in the newly created
role to the session_user
I would NOT have it automatically assign membership in the new role,
even though the SQL spec says so. We've not done that historically
and it doesn't seem desirable. In particular, it's *really* not
desirable for a user (role with LOGIN).
I'm fine with this. It does introduce an OWNER concept to roles and so at
minimum we need to add:
ALTER ROLE foo OWNER TO { new_owner | CURRENT_ROLE | CURRENT_USER |
SESSION_USER }
Agreed.
regards, tom lane
On Wed, Mar 9, 2022 at 4:31 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I don't think we need syntax to describe it. As I just said in my
other reply, we have a perfectly good precedent for this already
in ordinary object permissions. That is: an object owner always,
implicitly, has GRANT OPTION for all the object's privileges, even
if she revoked the corresponding plain privilege from herself.Yeah, this does mean that we're effectively deciding that the creator
of a role is its owner. What's the problem with that?
I don't think that's entirely the wrong concept, but it doesn't make a
lot of sense in a world where the creator has to be a superuser. If
alice, bob, and charlie are superusers who take turns creating new
users, and then we let charlie go due to budget cuts, forcing alice
and bob to change the owner of all the users he created to some other
superuser as a condition of dropping his account is a waste of
everyone's time. They can do exactly the same things to every account
on the system after we change the role owner as before.
But wait, I hear you cry, what about CREATEROLE? Well, CREATEROLE is
generally agreed to be broken right now, and if you don't agree with
that, consider that it can grant pg_execute_server_programs to a
newly-created account and then explain to me how it's functionally
different from superuser. The whole area needs a rethink. I believe
everyone involved in the discussion on the other threads agrees that
some reform of CREATEROLE is necessary, and more generally with the
idea that it's useful for non-superusers to be able to create roles.
But the reasons why people want that vary.
I want that because I want mini-superusers, where alice can administer
the users that alice creates just as if she were a superuser,
including having their permissions implicitly and dropping them when
she wants them gone, but where alice cannot break out to the operating
system as a true superuser could do. I want this because the lack of
meaningful privilege separation that led to CVE-2019-9193 being filed
spuriously is a very real problem. It's a thing a lot of people want,
and I want to give it to them. David Steele, on the other hand, wants
to build a user-creating bot that can create accounts but otherwise
conforms to the principle of least privilege: the bot can stand up
accounts, can grant them membership in a defined set of groups, but
cannot exercise the privileges of those accounts (or hack superuser
either). Other people may well want other things.
And that's why I'm not sure it's really the right idea to say that we
don't need syntax for this admin-without-member concept. If we just
want to bolt role ownership onto the existing framework without really
changing anything else, we can do that without extra syntax and, as
you say here, make it an implicit property of role ownership. But I
don't see that as has having much value; we just end up with a bunch
of superuser owners. Whatever. Now Stephen made the argument that we
ought to actually have admin-without-member as a first class concept,
something that could be assigned to arbitrary users. Actually, I think
he wanted it even more fine grained with that. And I think that could
make the concept a lot more useful, but then it needs some kind of
understandable syntax.
There's a lot of moving parts here. It's not just about coming up with
something that sounds generally logical, but about creating a system
that has some real-world utility.
But do we really have to solve this problem before we can clean up
this session exception?I think we need a plan for where we're going. I don't see "clean up
the session exception" as an end in itself; it's part of re-examining
how all of this ought to work. I don't say that we have to have a
complete patch right away, only that we need a coherent end goal.
I'd like to have a plan, too, but if this behavior is accidental, I
still think we can remove it without making big decisions about future
direction. The perfect is the enemy of the good.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Thu, Mar 10, 2022 at 7:46 AM Robert Haas <robertmhaas@gmail.com> wrote:
On Wed, Mar 9, 2022 at 4:31 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I don't think we need syntax to describe it. As I just said in my
other reply, we have a perfectly good precedent for this already
in ordinary object permissions. That is: an object owner always,
implicitly, has GRANT OPTION for all the object's privileges, even
if she revoked the corresponding plain privilege from herself.Yeah, this does mean that we're effectively deciding that the creator
of a role is its owner. What's the problem with that?I don't think that's entirely the wrong concept, but it doesn't make a
lot of sense in a world where the creator has to be a superuser. If
alice, bob, and charlie are superusers who take turns creating new
users, and then we let charlie go due to budget cuts, forcing alice
and bob to change the owner of all the users he created to some other
superuser as a condition of dropping his account is a waste of
everyone's time. They can do exactly the same things to every account
on the system after we change the role owner as before.
Then maybe we should just implement the idea that if a superuser would
become the owner we instead substitute in the bootstrap user. Or give the
DBA the choice whether they want to retain knowledge of specific roles -
and thus are willing to accept the "waste of time".
But wait, I hear you cry, what about CREATEROLE? Well, CREATEROLE is
generally agreed to be broken right now, and if you don't agree with
that, consider that it can grant pg_execute_server_programs to a
newly-created account and then explain to me how it's functionally
different from superuser.
CREATEROLE has long been defined as basically having "with admin option" on
every role in the system. The failure to special-case the roles that grant
different aspects of superuser-ness to its members doesn't make CREATEROLE
itself broken, it makes the implementation of pg_execute_server_programs
broken. Only superusers should be considered to have with admin option on
these roles. They can delegate through the usual membership+admin mechanism
to a CREATEROLE role if they desire.
The whole area needs a rethink. I believe
everyone involved in the discussion on the other threads agrees that
some reform of CREATEROLE is necessary, and more generally with the
idea that it's useful for non-superusers to be able to create roles.
As the documentation says, using SUPERUSER for day-to-day administration is
contrary to good security practices. Role management is considered to be a
day-to-day administration activity. I agree with this principle. It was
designed to neither be a superuser nor grant superuser, so removing the
ability to grant the pg_* role memberships remains consistent with its
original intent.
I want that because I want mini-superusers, where alice can administer
the users that alice creates just as if she were a superuser,
including having their permissions implicitly and dropping them when
she wants them gone, but where alice cannot break out to the operating
system as a true superuser could do.
CREATEROLE (once the pg_* with admin rules are fixed) + Ownership and rules
restricting interfering with another role's objects (unless superuser)
seems to handle this.
the bot can stand up
accounts, can grant them membership in a defined set of groups, but
cannot exercise the privileges of those accounts (or hack superuser
either).
The bot should be provided a security definer procedure that encapsulates
all of this rather than us trying to hack the permission system. This
isn't a user permission concern, it is an unauthorized privilege escalation
concern. Anyone with the bot's credentials can trivially overcome the
third restriction by creating a role with the desired membership and then
logging in as that role - and there is nothing the system can do to prevent
that while also allowing the other two permissions.
And that's why I'm not sure it's really the right idea to say that we
don't need syntax for this admin-without-member concept.
We already have this syntax in the form of CREATEROLE. But we do need a
fix, just on the group side. We need a way to define a group as having no
ADMINS.
ALTER ROLE pg_superuser WITH [NO] ADMIN;
Then adding a role membership including the WITH ADMIN OPTION can be
rejected, as can the non-superuser situation. Setting WITH NO ADMIN should
fail if any existing members have admin. You must be a superuser to
execute WITH ADMIN (maybe WITH NO ADMIN as well...). And possibly even a
new pg_* role that grants this ability (and maybe some others) for use by a
backup/restore user.
Or just special-case pg_* roles.
The advantage of exposing this to the DBA is that they can then package
pg_* roles into a custom group and still have the benefit of superuser only
administration. In the special-case implementation the presence of a pg_*
role in a group hierarchy would then preclude a non-superuser from having
admin on the entire tree (the pg_* roles are all roots, or in the case of
pg_monitor, directly emanate from a root role).
David J.
David J.
Greetings,
* David G. Johnston (david.g.johnston@gmail.com) wrote:
On Thu, Mar 10, 2022 at 7:46 AM Robert Haas <robertmhaas@gmail.com> wrote:
On Wed, Mar 9, 2022 at 4:31 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I don't think we need syntax to describe it. As I just said in my
other reply, we have a perfectly good precedent for this already
in ordinary object permissions. That is: an object owner always,
implicitly, has GRANT OPTION for all the object's privileges, even
if she revoked the corresponding plain privilege from herself.Yeah, this does mean that we're effectively deciding that the creator
of a role is its owner. What's the problem with that?I don't think that's entirely the wrong concept, but it doesn't make a
lot of sense in a world where the creator has to be a superuser. If
alice, bob, and charlie are superusers who take turns creating new
users, and then we let charlie go due to budget cuts, forcing alice
and bob to change the owner of all the users he created to some other
superuser as a condition of dropping his account is a waste of
everyone's time. They can do exactly the same things to every account
on the system after we change the role owner as before.Then maybe we should just implement the idea that if a superuser would
become the owner we instead substitute in the bootstrap user. Or give the
DBA the choice whether they want to retain knowledge of specific roles -
and thus are willing to accept the "waste of time".
This doesn't strike me as going in the right direction. Falling back to
the bootstrap superuser is generally a hack and not a great one. I'll
also point out that the SQL spec hasn't got a concept of role ownership
either.
But wait, I hear you cry, what about CREATEROLE? Well, CREATEROLE is
generally agreed to be broken right now, and if you don't agree with
that, consider that it can grant pg_execute_server_programs to a
newly-created account and then explain to me how it's functionally
different from superuser.CREATEROLE has long been defined as basically having "with admin option" on
every role in the system. The failure to special-case the roles that grant
different aspects of superuser-ness to its members doesn't make CREATEROLE
itself broken, it makes the implementation of pg_execute_server_programs
broken. Only superusers should be considered to have with admin option on
these roles. They can delegate through the usual membership+admin mechanism
to a CREATEROLE role if they desire.
No, CREATEROLE having admin option on every role in the system is broken
and always has been. It's not just an issue for predefined roles like
pg_execute_server_program, it's an issue for any role that could become
a superuser either directly or indirectly and that extends beyond the
predefined ones. As this issue with CREATEROLE existed way before
predefined roles were added to PG, claiming that it's an issue with
predefined roles doesn't make a bit of sense.
The whole area needs a rethink. I believe
everyone involved in the discussion on the other threads agrees that
some reform of CREATEROLE is necessary, and more generally with the
idea that it's useful for non-superusers to be able to create roles.As the documentation says, using SUPERUSER for day-to-day administration is
contrary to good security practices. Role management is considered to be a
day-to-day administration activity. I agree with this principle. It was
designed to neither be a superuser nor grant superuser, so removing the
ability to grant the pg_* role memberships remains consistent with its
original intent.
That would not be sufficient to make CREATEROLE safe. Far, far from it.
I want that because I want mini-superusers, where alice can administer
the users that alice creates just as if she were a superuser,
including having their permissions implicitly and dropping them when
she wants them gone, but where alice cannot break out to the operating
system as a true superuser could do.CREATEROLE (once the pg_* with admin rules are fixed) + Ownership and rules
restricting interfering with another role's objects (unless superuser)
seems to handle this.
This is not sufficient- roles can be not-superuser themselves but have
the ability to become superuser if GRANT'd a superuser role and
therefore we can't have a system where CREATEROLE allows arbitrary
GRANT'ing of roles to each other. I'm a bit confused too as anything
where we are curtailing what CREATEROLE roles are able to do in a manner
that means they're only able to modify some subset of roles should
equally apply to predefined roles too- that is, CREATEROLE shouldn't be
the determining factor in the question of if a role can GRANT a
predefined (or any other role) to some other role- that should be
governed by the admin option on that role, and that should work exactly
the same for predefined roles as it does for any other.
I disagree that ownership is needed that's not what the spec calls for
either. What we need is more flexibility when it comes to the
relationships which are allowed to be created between roles and what
privileges come with them. To that end, I'd argue that we should be
extending pg_auth_members, first by separating out membership itself
into an explicitly tracked attribute (instead of being implicit in the
existance of a row in the table) and then adding on what other
privileges we see fit to add, such as the ability to DROP a role. We
do need to remove the ability for a role who hasn't been explicitly
given the admin right on another role to modify that role's membership
too, as was originally proposed here. This also seems to more closely
follow the spec's expectation, something that role ownership doesn't.
the bot can stand up
accounts, can grant them membership in a defined set of groups, but
cannot exercise the privileges of those accounts (or hack superuser
either).The bot should be provided a security definer procedure that encapsulates
all of this rather than us trying to hack the permission system. This
isn't a user permission concern, it is an unauthorized privilege escalation
concern. Anyone with the bot's credentials can trivially overcome the
third restriction by creating a role with the desired membership and then
logging in as that role - and there is nothing the system can do to prevent
that while also allowing the other two permissions.
Falling back to security definer functions may be one approach but it's
not a great one and it only works if it's possible to end up with the
catalogs having what is actually desired- for example, ADMIN option
without membership isn't something the catalogs today can understand
because existance in pg_auth_members implies membership and you can't
have ADMIN without having that row. The same issue would exist with
ownership if ownership implied the same- that's not improving things.
And that's why I'm not sure it's really the right idea to say that we
don't need syntax for this admin-without-member concept.We already have this syntax in the form of CREATEROLE. But we do need a
fix, just on the group side. We need a way to define a group as having no
ADMINS.
We don't have this syntax today nor do we have a way to store such a
concept in the catalogs either, so I'm pretty baffled by this. Defining
a group without admins is, in fact, what we actually have support for
today in the catalogs- it's just a case where there aren't any rows in
pg_auth_members which have 'admin_option' as true. The opposite is what
we're talking about here- rows which have 'admin_option' as true but
don't have membership, and that can't be the case today because
existance in the table itself implies membership.
ALTER ROLE pg_superuser WITH [NO] ADMIN;
Then adding a role membership including the WITH ADMIN OPTION can be
rejected, as can the non-superuser situation. Setting WITH NO ADMIN should
fail if any existing members have admin. You must be a superuser to
execute WITH ADMIN (maybe WITH NO ADMIN as well...). And possibly even a
new pg_* role that grants this ability (and maybe some others) for use by a
backup/restore user.
I'm not following this in general or how it helps. Surely we don't want
to limit WITH ADMIN to superusers. As for if we should migrate
CREATEROLE to a new predefined role, maybe, but that seems like a
different question.
Or just special-case pg_* roles.
As I hopefully made clear above, this isn't actually a solution, nor do
pg_* roles need to be treated somehow differently in this aspect.
The advantage of exposing this to the DBA is that they can then package
pg_* roles into a custom group and still have the benefit of superuser only
administration. In the special-case implementation the presence of a pg_*
role in a group hierarchy would then preclude a non-superuser from having
admin on the entire tree (the pg_* roles are all roots, or in the case of
pg_monitor, directly emanate from a root role).
We are very much trying to move away from 'superuser only
administration'.
Thanks,
Stephen
On Mar 10, 2022, at 7:56 AM, David G. Johnston <david.g.johnston@gmail.com> wrote:
I want that because I want mini-superusers, where alice can administer
the users that alice creates just as if she were a superuser,
including having their permissions implicitly and dropping them when
she wants them gone, but where alice cannot break out to the operating
system as a true superuser could do.CREATEROLE (once the pg_* with admin rules are fixed) + Ownership and rules restricting interfering with another role's objects (unless superuser) seems to handle this.
What if one of alice's subordinates also owns roles? Can alice interfere with *that* role's objects? I don't see that a simple rule restricting roles from interfering with another role's objects is quite enough. That raises the question of whether role ownership is transitive, and whether we need a concept similar to inherit/noinherit for ownership.
There is also the problem that CREATEROLE currently allows a set of privileges to be granted to created roles, and that set of privileges is hard-coded. You've suggested changing the hard-coded rules to remove pg_* roles from the list of grantable privileges, but that's still an inflexible set of hardcoded privileges. Wouldn't it make more sense for the grantor to need GRANT OPTION on any privilege they give to roles they create?
the bot can stand up
accounts, can grant them membership in a defined set of groups, but
cannot exercise the privileges of those accounts (or hack superuser
either).The bot should be provided a security definer procedure that encapsulates all of this rather than us trying to hack the permission system. This isn't a user permission concern, it is an unauthorized privilege escalation concern. Anyone with the bot's credentials can trivially overcome the third restriction by creating a role with the desired membership and then logging in as that role - and there is nothing the system can do to prevent that while also allowing the other two permissions.
Doesn't this assume password authentication? If the server uses ldap authentication, for example, wouldn't the bot need valid ldap credentials for at least one user for this attack to work? And if CREATEROLE has been made more configurable, wouldn't the bot only be able to grant that ldap user the limited set of privileges that the bot's database user has been granted ADMIN OPTION for?
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Thu, Mar 10, 2022 at 11:19 AM Stephen Frost <sfrost@snowman.net> wrote:
I disagree that ownership is needed that's not what the spec calls for
either. What we need is more flexibility when it comes to the
relationships which are allowed to be created between roles and what
privileges come with them. To that end, I'd argue that we should be
extending pg_auth_members, first by separating out membership itself
into an explicitly tracked attribute (instead of being implicit in the
existance of a row in the table) and then adding on what other
privileges we see fit to add, such as the ability to DROP a role. We
do need to remove the ability for a role who hasn't been explicitly
given the admin right on another role to modify that role's membership
too, as was originally proposed here. This also seems to more closely
follow the spec's expectation, something that role ownership doesn't.
I do not have a problem with more fine-grained kinds of authorization
even though I think there are syntactic issues to work out, but I
strongly disagree with the idea that we can't or shouldn't also have
role ownership. Marc invented it. Now Tom has invented it
independently. All sorts of other objects have it already. Trying to
make it out like this is some kind of kooky idea is not believable.
Yeah, it's not the most sophisticated or elegant model and that's why
it's good for us to also have other things, but for simple cases it is
easy to understand and works great.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Thu, Mar 10, 2022 at 9:19 AM Stephen Frost <sfrost@snowman.net> wrote:
Greetings,
* David G. Johnston (david.g.johnston@gmail.com) wrote:
On Thu, Mar 10, 2022 at 7:46 AM Robert Haas <robertmhaas@gmail.com>
wrote:
On Wed, Mar 9, 2022 at 4:31 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I don't think we need syntax to describe it. As I just said in my
other reply, we have a perfectly good precedent for this already
in ordinary object permissions. That is: an object owner always,
implicitly, has GRANT OPTION for all the object's privileges, even
if she revoked the corresponding plain privilege from herself.Yeah, this does mean that we're effectively deciding that the creator
of a role is its owner. What's the problem with that?I don't think that's entirely the wrong concept, but it doesn't make a
lot of sense in a world where the creator has to be a superuser. If
alice, bob, and charlie are superusers who take turns creating new
users, and then we let charlie go due to budget cuts, forcing alice
and bob to change the owner of all the users he created to some other
superuser as a condition of dropping his account is a waste of
everyone's time. They can do exactly the same things to every account
on the system after we change the role owner as before.Then maybe we should just implement the idea that if a superuser would
become the owner we instead substitute in the bootstrap user. Or givethe
DBA the choice whether they want to retain knowledge of specific roles -
and thus are willing to accept the "waste of time".This doesn't strike me as going in the right direction. Falling back to
the bootstrap superuser is generally a hack and not a great one. I'll
also point out that the SQL spec hasn't got a concept of role ownership
either.But wait, I hear you cry, what about CREATEROLE? Well, CREATEROLE is
generally agreed to be broken right now, and if you don't agree with
that, consider that it can grant pg_execute_server_programs to a
newly-created account and then explain to me how it's functionally
different from superuser.CREATEROLE has long been defined as basically having "with admin option"
on
every role in the system. The failure to special-case the roles that
grant
different aspects of superuser-ness to its members doesn't make
CREATEROLE
itself broken, it makes the implementation of pg_execute_server_programs
broken. Only superusers should be considered to have with admin optionon
these roles. They can delegate through the usual membership+admin
mechanism
to a CREATEROLE role if they desire.
No, CREATEROLE having admin option on every role in the system is broken
and always has been. It's not just an issue for predefined roles like
pg_execute_server_program,
it's an issue for any role that could become
a superuser either directly or indirectly and that extends beyond the
predefined ones.
The only indirect way for a role to become superuser is to have been
granted membership in a superuser group, then SET ROLE. Non-superusers
cannot do this. If a superuser does this I consider the outcome to be no
different than if they go and do:
SET allow_system_table_mods TO true;
DROP pg_catalog.pg_class;
In short, having a CREATEROLE user issuing:
GRANT pg_read_all_stats TO davidj;
should result in the same outcome as them issuing:
GRANT postgres TO davidj;
-- ERROR: must be superuser to alter superusers
Superusers can break their system and we don't go to great effort to stop
them. I see no difference here, so arguments of this nature aren't all
that compelling to me.
CREATEROLE shouldn't be
the determining factor in the question of if a role can GRANT a
predefined (or any other role) to some other role- that should be
governed by the admin option on that role, and that should work exactly
the same for predefined roles as it does for any other.
Never granting the CREATEROLE attribute to anyone will give you this
outcome today.
ADMIN option
without membership isn't something the catalogs today can understand
Today, they don't need to in order for the system to function within its
existing design specs.
ALTER ROLE pg_superuser WITH [NO] ADMIN;
Then adding a role membership including the WITH ADMIN OPTION can be
rejected, as can the non-superuser situation. Setting WITH NO ADMINshould
fail if any existing members have admin. You must be a superuser to
execute WITH ADMIN (maybe WITH NO ADMIN as well...). And possibly even a
new pg_* role that grants this ability (and maybe some others) for useby a
backup/restore user.
I'm not following this in general or how it helps. Surely we don't want
to limit WITH ADMIN to superusers.
Today a non-superuser cannot "grant postgres to someuser;"
The point of this attribute is to allow the superuser to apply that rule to
other roles that aren't superuser. In particular, the predefined pg_*
roles. But it could extend to any other role the superuser would like to
limit. It means, for that for named role, ADMIN privileges cannot be
delegated to other roles - thus all administration of that role's
membership roster must happen by a superuser.
In particular, this means CREATEROLE roles cannot assign membership in the
marked roles; just like they cannot assign membership in superuser roles
today.
For me, because the SUPERUSER cannot have its role become a group without a
superuser making that choice, and by default the default pg_* roles will
all have this property as well, and any newly superuser created roles that
may be members of either superuser or pg_* can have the property defined as
well, gives full control to the superuser as to how superuser abilities are
doled out and so the design itself allows for what many of you are
considering to be "safe usage". That "unsafe configurations" are possible
is due to the policy that superusers are unrestricted in what they can do,
including making unsafe and destructive choices.
In short, removing the self-administration rule solves the "login roles
should not be automatically considered groups administered by themselves"
problem - or at least a feature we really don't need.
And defining a "superuser administration only" attribute to a role solves
the indirect superuser privileges and assignment thereof by non-superusers
problem.
I can see value in adding a feature whereby we allow the DBA to define a
group as a schema-like container and then assign roles to that group with a
fine-grained permissions model. My take is this proposal is a new feature
while the two problems noted above can be solved more readily and with less
risk with the two suggested changes.
David J.
On Thu, Mar 10, 2022 at 12:11 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Mar 10, 2022 at 11:19 AM Stephen Frost <sfrost@snowman.net> wrote:
I disagree that ownership is needed that's not what the spec calls for
either. What we need is more flexibility when it comes to the
relationships which are allowed to be created between roles and what
privileges come with them. To that end, I'd argue that we should be
extending pg_auth_members, first by separating out membership itself
into an explicitly tracked attribute (instead of being implicit in the
existance of a row in the table) and then adding on what other
privileges we see fit to add, such as the ability to DROP a role. We
do need to remove the ability for a role who hasn't been explicitly
given the admin right on another role to modify that role's membership
too, as was originally proposed here. This also seems to more closely
follow the spec's expectation, something that role ownership doesn't.I do not have a problem with more fine-grained kinds of authorization
even though I think there are syntactic issues to work out, but I
strongly disagree with the idea that we can't or shouldn't also have
role ownership. Marc invented it. Now Tom has invented it
independently. All sorts of other objects have it already. Trying to
make it out like this is some kind of kooky idea is not believable.
Yeah, it's not the most sophisticated or elegant model and that's why
it's good for us to also have other things, but for simple cases it is
easy to understand and works great.
Ownership implies DAC, the ability to grant others rights to an
object. It's not "kooky" to see roles as owned objects, but it isn't
required either. For example most objects on a UNIX system are owned
and subject to DAC but users aren't.
Stephen's, and now my, issue with ownership is that, since it implies
DAC, most checks will be bypassed for the owner. We would both prefer
for everyone to be subject to the grants, including whoever created
the role.
Rather, we'd like to see a "creators of roles get this set of grants
against the role by default" and "as a superuser I can revoke grants
from creators against roles they created"
Greetings,
* David G. Johnston (david.g.johnston@gmail.com) wrote:
On Thu, Mar 10, 2022 at 9:19 AM Stephen Frost <sfrost@snowman.net> wrote:
* David G. Johnston (david.g.johnston@gmail.com) wrote:
On Thu, Mar 10, 2022 at 7:46 AM Robert Haas <robertmhaas@gmail.com>
wrote:
On Wed, Mar 9, 2022 at 4:31 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I don't think we need syntax to describe it. As I just said in my
other reply, we have a perfectly good precedent for this already
in ordinary object permissions. That is: an object owner always,
implicitly, has GRANT OPTION for all the object's privileges, even
if she revoked the corresponding plain privilege from herself.Yeah, this does mean that we're effectively deciding that the creator
of a role is its owner. What's the problem with that?I don't think that's entirely the wrong concept, but it doesn't make a
lot of sense in a world where the creator has to be a superuser. If
alice, bob, and charlie are superusers who take turns creating new
users, and then we let charlie go due to budget cuts, forcing alice
and bob to change the owner of all the users he created to some other
superuser as a condition of dropping his account is a waste of
everyone's time. They can do exactly the same things to every account
on the system after we change the role owner as before.Then maybe we should just implement the idea that if a superuser would
become the owner we instead substitute in the bootstrap user. Or givethe
DBA the choice whether they want to retain knowledge of specific roles -
and thus are willing to accept the "waste of time".This doesn't strike me as going in the right direction. Falling back to
the bootstrap superuser is generally a hack and not a great one. I'll
also point out that the SQL spec hasn't got a concept of role ownership
either.But wait, I hear you cry, what about CREATEROLE? Well, CREATEROLE is
generally agreed to be broken right now, and if you don't agree with
that, consider that it can grant pg_execute_server_programs to a
newly-created account and then explain to me how it's functionally
different from superuser.CREATEROLE has long been defined as basically having "with admin option"
on
every role in the system. The failure to special-case the roles that
grant
different aspects of superuser-ness to its members doesn't make
CREATEROLE
itself broken, it makes the implementation of pg_execute_server_programs
broken. Only superusers should be considered to have with admin optionon
these roles. They can delegate through the usual membership+admin
mechanism
to a CREATEROLE role if they desire.
No, CREATEROLE having admin option on every role in the system is broken
and always has been. It's not just an issue for predefined roles like
pg_execute_server_program,it's an issue for any role that could become
a superuser either directly or indirectly and that extends beyond the
predefined ones.The only indirect way for a role to become superuser is to have been
granted membership in a superuser group, then SET ROLE. Non-superusers
cannot do this. If a superuser does this I consider the outcome to be no
different than if they go and do:
A non-superuser absolutely can be GRANT'd membership in a superuser role
and then SET ROLE to that user thus becoming a superuser. Giving users
a regular role to log in as and then membership in a role that can
become a superuser is akin to having a sudoers group in Unix and is good
practice, not something that everyone should have to be super-dooper
careful to not do, lest a CREATEROLE user be able to leverage that.
SET allow_system_table_mods TO true;
DROP pg_catalog.pg_class;
I don't equate these in the least.
In short, having a CREATEROLE user issuing:
GRANT pg_read_all_stats TO davidj;
should result in the same outcome as them issuing:
GRANT postgres TO davidj;
-- ERROR: must be superuser to alter superusers
No, what should matter is if the role doing the GRANT has admin rights
on pg_read_all_stats, or on the postgres role. That also happens to be
what the spec says.
Superusers can break their system and we don't go to great effort to stop
them. I see no difference here, so arguments of this nature aren't all
that compelling to me.
That you don't feel they're compelling don't make them somehow not real,
nor even particularly uncommon, nor do I view ignoring that possibility
as somehow creating a strong authentication system.
CREATEROLE shouldn't be
the determining factor in the question of if a role can GRANT a
predefined (or any other role) to some other role- that should be
governed by the admin option on that role, and that should work exactly
the same for predefined roles as it does for any other.Never granting the CREATEROLE attribute to anyone will give you this
outcome today.
... which is why CREATEROLE is broken.
ADMIN option
without membership isn't something the catalogs today can understandToday, they don't need to in order for the system to function within its
existing design specs.
Eh? Your argument here is "don't use CREATEROLE"? While I agree with
that being a generally good idea today, it hardly makes sense to suggest
it in a thread where we're talking about how to make CREATEROLE, or
something like it, be useful.
ALTER ROLE pg_superuser WITH [NO] ADMIN;
Then adding a role membership including the WITH ADMIN OPTION can be
rejected, as can the non-superuser situation. Setting WITH NO ADMINshould
fail if any existing members have admin. You must be a superuser to
execute WITH ADMIN (maybe WITH NO ADMIN as well...). And possibly even a
new pg_* role that grants this ability (and maybe some others) for useby a
backup/restore user.
I'm not following this in general or how it helps. Surely we don't want
to limit WITH ADMIN to superusers.Today a non-superuser cannot "grant postgres to someuser;"
No, but a role can be created like 'admin', which a superuser GRANT's
'postgres' to and then that role can be GRANT'd to anyone by anyone who
has CREATEROLE rights. That's not sane.
The point of this attribute is to allow the superuser to apply that rule to
other roles that aren't superuser. In particular, the predefined pg_*
roles. But it could extend to any other role the superuser would like to
limit. It means, for that for named role, ADMIN privileges cannot be
delegated to other roles - thus all administration of that role's
membership roster must happen by a superuser.
The whole "X can't modify a superuser role without being a superuser"
concept is just broken and was a poor choice when it was originally
done specifically because it only looks at individual roles and their
specific rolsuper bit, completely ignoring the fact that role membership
exists as a thing that we should handle sanely, including a
non-superuser role being grant'd a superuser role. Predefined roles
haven't got anything to do with any of this, they only make it more
obvious to people who didn't understand how the system worked before
they came along.
I disagree entirely with the idea that we must have some roles who can
only ever be administered by a superuser. If anything, we should be
moving away (as we have, in fact, been doing), from anything being the
exclusive purview of the superuser.
In particular, this means CREATEROLE roles cannot assign membership in the
marked roles; just like they cannot assign membership in superuser roles
today.
I disagree with the idea that we need to mark some roles as only being
able to be modified by the superuser- why invent this? We have the
ADMIN option already and that can be applied to allow any role X to have
the ability to modify the members of role Y. That's a whole lot better
than some explicit flag that says "only superusers can modify this
role". If an admin wants that, they can set things up that way already
today, as long as they don't use the current CREATEROLE attribute.
Ideally, we'd modify CREATEROLE, or remove it and replace it with
something better, which still maintains that same flexibility. What you
seem to be arguing for here is to rip out the ADMIN functionality, which
is defined by spec and not even exclusively by PG, and replace it with a
single per-role flag that says if that role can only be modified by
superusers. That seems entirely backwards to me.
For me, because the SUPERUSER cannot have its role become a group without a
superuser making that choice, and by default the default pg_* roles will
all have this property as well, and any newly superuser created roles that
may be members of either superuser or pg_* can have the property defined as
well, gives full control to the superuser as to how superuser abilities are
doled out and so the design itself allows for what many of you are
considering to be "safe usage". That "unsafe configurations" are possible
is due to the policy that superusers are unrestricted in what they can do,
including making unsafe and destructive choices.
I disagree that it's an 'unsafe configuration' for there to ever exist a
non-superuser role that has been granted a superuser role. The only
thing that makes this unsafe is the existance of CREATEROLE.
Why are we making this all about superusers though? In what you're
proposing, you're suggesting that it's perfectly fine for any role which
has CREATEROLE to be able to take over any other role in the entire
system, excluding predefined roles and superusers. How is that sane, or
truely much less than what the superuser has in terms of ability? The
short answer is that it's not- which is why we have documented
CREATEROLE as being 'superuser light'. The goal here is to get rid of
that.
In short, removing the self-administration rule solves the "login roles
should not be automatically considered groups administered by themselves"
problem - or at least a feature we really don't need.
And defining a "superuser administration only" attribute to a role solves
the indirect superuser privileges and assignment thereof by non-superusers
problem.
But it doesn't *actually* make CREATEROLE something that you can give
out to folks on a general basis because anyone with CREATEROLE would
still be able to take over every single non-superuser and non-predefined
role in the system. We do *not* want that.
I can see value in adding a feature whereby we allow the DBA to define a
group as a schema-like container and then assign roles to that group with a
fine-grained permissions model. My take is this proposal is a new feature
while the two problems noted above can be solved more readily and with less
risk with the two suggested changes.
Yes, we're talking about a new feature- one intended to replace the
broken way that CREATEROLE works, which your proposal doesn't.
Thanks,
Stephen
On Thu, Mar 10, 2022 at 12:26 PM Joshua Brindle
<joshua.brindle@crunchydata.com> wrote:
Ownership implies DAC, the ability to grant others rights to an
object. It's not "kooky" to see roles as owned objects, but it isn't
required either. For example most objects on a UNIX system are owned
and subject to DAC but users aren't.
I have no issue with anything you write in this paragraph.
Stephen's, and now my, issue with ownership is that, since it implies
DAC, most checks will be bypassed for the owner. We would both prefer
for everyone to be subject to the grants, including whoever created
the role.
That sounds like MAC, which is usually something that sits on top of
DAC and is enforced in addition to DAC, not a reason for DAC to not
exist.
Rather, we'd like to see a "creators of roles get this set of grants
against the role by default" and "as a superuser I can revoke grants
from creators against roles they created"
If you create a table, you own it. You get a set of default
permissions on the table which can be revoked either by you or by
someone else, and you also have certain intrinsic rights over the
object as owner which cannot be revoked - including the ability to
re-grant yourself any previously-revoked permissions. I am not against
the idea of trying to clean things up so that everything you can do
with a table is a revocable privilege and you can be the owner without
having any rights at all, including the right to give yourself other
rights back, but I cannot believe that the idea of removing table
ownership as a concept would ever gain consensus on this list.
Therefore, I also do not think it is reasonable to say that we
shouldn't introduce a similar concept for object types that don't have
it yet, such as roles.
But that's not to say that we couldn't decide to do something else
instead, and that other thing might well be better. Do you want to
sketch out a full proposal, even just what the syntax would look like,
and share that here? And if you could explain how I could use it to
create the mini-superusers that I'm trying to get out of this thing,
even better.
Thanks,
--
Robert Haas
EDB: http://www.enterprisedb.com
On 09.03.22 14:02, Robert Haas wrote:
On Wed, Mar 9, 2022 at 7:55 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:Do we have subtractive permissions today?
Not in the GRANT/REVOKE sense, I think, but you can put a user in a
group and then mention that group in pg_hba.conf. And that line might
be "reject" or whatever.
Well, you can always build an external system that looks at roles and
does nonsensical things with it. But the privilege system itself seems
to be additive only. Personally, I agree with the argument that there
should not be any subtractive permissions. The mental model where
permissions are sort of keys to doors or boxes just doesn't work for that.
On Thu, Mar 10, 2022 at 2:05 PM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:
On 09.03.22 14:02, Robert Haas wrote:
On Wed, Mar 9, 2022 at 7:55 AM Peter Eisentraut
<peter.eisentraut@enterprisedb.com> wrote:Do we have subtractive permissions today?
Not in the GRANT/REVOKE sense, I think, but you can put a user in a
group and then mention that group in pg_hba.conf. And that line might
be "reject" or whatever.Well, you can always build an external system that looks at roles and
does nonsensical things with it. But the privilege system itself seems
to be additive only. Personally, I agree with the argument that there
should not be any subtractive permissions. The mental model where
permissions are sort of keys to doors or boxes just doesn't work for that.
I mean, I didn't design pg_hba.conf, but I think it's part of the
database doing a reasonable thing, not an external system doing a
nonsensical thing.
I am not sure that I (or anyone) would endorse a system where you can
say something like GRANT NOT SELECT ON TABLE foo TO bar, essentially
putting a negative ACL into the system dictating that, regardless of
any other grants that may exist, foo should not be able to SELECT from
that table. But I think it's reasonable to use groups as a way of
referencing a defined collection of users for some purpose. The
pg_hba.conf thing is an example of that. You put all the users that
you want to be treated in a certain way for authentication purposes
into a group, and then you mention the group in the file, and it just
works. I don't find that an unreasonable design at all. We could've
created some other kind of grouping mechanism for such purposes that
is separate from the role system, but we didn't choose to do that. I
don't know if that was the absolute best possible decision or not, but
it doesn't seem like an especially bad choice.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Thu, Mar 10, 2022 at 11:05 AM Stephen Frost <sfrost@snowman.net> wrote:
Greetings,
* David G. Johnston (david.g.johnston@gmail.com) wrote:
On Thu, Mar 10, 2022 at 9:19 AM Stephen Frost <sfrost@snowman.net>
wrote:
* David G. Johnston (david.g.johnston@gmail.com) wrote:
On Thu, Mar 10, 2022 at 7:46 AM Robert Haas <robertmhaas@gmail.com>
wrote:
The only indirect way for a role to become superuser is to have been
granted membership in a superuser group, then SET ROLE. Non-superusers
cannot do this. If a superuser does this I consider the outcome to be no
different than if they go and do:A non-superuser absolutely can be GRANT'd membership in a superuser role
and then SET ROLE to that user thus becoming a superuser.
A non-superuser cannot grant a non-superuser membership in a superuser
role. A superuser granting a user membership in a superuser role makes
that user a superuser. This seems sane.
If a superuser grants a non-superuser membership in a superuser role then
today a non-superuser can grant a user membership in that intermediate
role, thus having a non-superuser make another user a superuser. This is
arguably a bug that needs to be fixed.
My desired fix is to just require the superuser to mark (or have it marked
by default ideally) the role inheriting superuser and put the
responsibility on the superuser. I agree this is not ideal, but it is
probably quick and low risk.
I'll let someone else describe the details of the alternative option. I
suspect it will end up being a better option in terms of design. But
depending on time and risk even knowing that we want the better design
eventually doesn't preclude getting the easier fix in now.
No, what should matter is if the role doing the GRANT has admin rights
on pg_read_all_stats, or on the postgres role. That also happens to be
what the spec says.
Yes, and superusers implicitly have that right, while CREATEROLE users
implicitly have that right on the pg_* role but not on superuser roles. I
just want to plug that hole and include the pg_* roles (or any role for
that matter) in being able to be denied implied ADMIN rights for
non-superusers.
Today a non-superuser cannot "grant postgres to someuser;"
No, but a role can be created like 'admin', which a superuser GRANT's
'postgres' to and then that role can be GRANT'd to anyone by anyone who
has CREATEROLE rights. That's not sane.
I agree. And I've suggested a minimal fix, adding an attribute to the role
that prohibits non-superusers from granting it to others, that removes the
insane behavior.
I'm on board for a hard-coded fix as well - if a superuser is in the
membership chain of a role then non-superusers cannot grant membership in
that role to others.
Neither of those really solves the pg_* roles problem. We still need to
indicate that they are somehow special. Whether it is a nice matrix or
roles and permissions or a simple attribute that makes them behave like
they are superuser roles.
I disagree entirely with the idea that we must have some roles who can
only ever be administered by a superuser.
I don't think this is a must have. I think that since we do have it today
that fixes that leverage the status quo in order to be done more easily are
perfectly valid solutions.
If anything, we should be
moving away (as we have, in fact, been doing), from anything being the
exclusive purview of the superuser.
I totally agree.
In particular, this means CREATEROLE roles cannot assign membership in
the
marked roles; just like they cannot assign membership in superuser roles
today.I disagree with the idea that we need to mark some roles as only being
able to be modified by the superuser- why invent this?
Because CREATEUSER is a thing and people want to prevent roles with that
attribute from assigning membership to the predefined superuser-aspect
roles. If I've misunderstood that desire and the scope of delegation given
by the superuser to CREATEUSER roles is acceptable, then no change here is
needed.
What you
seem to be arguing for here is to rip out the ADMIN functionality, which
is defined by spec and not even exclusively by PG, and replace it with a
single per-role flag that says if that role can only be modified by
superusers.
I made the observation that being able to manage the membership of a group
without having the ability to create new users seems like a half a loaf of
a feature. That's it. I would presume that any redesign of the
permissions system here would address this adequately.
The
short answer is that it's not- which is why we have documented
CREATEROLE as being 'superuser light'. The goal here is to get rid of
that.
Now you tell me. Robert should have led with that goal upfront.
Yes, we're talking about a new feature- one intended to replace the
broken way that CREATEROLE works, which your proposal doesn't.
That is correct, I was trying to figure out minimally invasive fixes to
what are arguably being called bugs.
David J.
Greetings,
* David G. Johnston (david.g.johnston@gmail.com) wrote:
On Thu, Mar 10, 2022 at 11:05 AM Stephen Frost <sfrost@snowman.net> wrote:
* David G. Johnston (david.g.johnston@gmail.com) wrote:
On Thu, Mar 10, 2022 at 9:19 AM Stephen Frost <sfrost@snowman.net> wrote:
* David G. Johnston (david.g.johnston@gmail.com) wrote:
On Thu, Mar 10, 2022 at 7:46 AM Robert Haas <robertmhaas@gmail.com> wrote:
The only indirect way for a role to become superuser is to have been
granted membership in a superuser group, then SET ROLE. Non-superusers
cannot do this. If a superuser does this I consider the outcome to be no
different than if they go and do:A non-superuser absolutely can be GRANT'd membership in a superuser role
and then SET ROLE to that user thus becoming a superuser.A non-superuser cannot grant a non-superuser membership in a superuser
role. A superuser granting a user membership in a superuser role makes
that user a superuser. This seems sane.If a superuser grants a non-superuser membership in a superuser role then
today a non-superuser can grant a user membership in that intermediate
role, thus having a non-superuser make another user a superuser. This is
arguably a bug that needs to be fixed.My desired fix is to just require the superuser to mark (or have it marked
by default ideally) the role inheriting superuser and put the
responsibility on the superuser. I agree this is not ideal, but it is
probably quick and low risk.I'll let someone else describe the details of the alternative option. I
suspect it will end up being a better option in terms of design. But
depending on time and risk even knowing that we want the better design
eventually doesn't preclude getting the easier fix in now.No, what should matter is if the role doing the GRANT has admin rights
on pg_read_all_stats, or on the postgres role. That also happens to be
what the spec says.Yes, and superusers implicitly have that right, while CREATEROLE users
implicitly have that right on the pg_* role but not on superuser roles. I
just want to plug that hole and include the pg_* roles (or any role for
that matter) in being able to be denied implied ADMIN rights for
non-superusers.
CREATEROLE users implicitly have that right on *all non-superuser
roles*. Not just the pg_* ones, which is why the pg_* ones aren't any
different in this regard.
Today a non-superuser cannot "grant postgres to someuser;"
No, but a role can be created like 'admin', which a superuser GRANT's
'postgres' to and then that role can be GRANT'd to anyone by anyone who
has CREATEROLE rights. That's not sane.I agree. And I've suggested a minimal fix, adding an attribute to the role
that prohibits non-superusers from granting it to others, that removes the
insane behavior.
I disagree that this is a minimal fix as I don't see it as a fix to the
actual issue, which is the ability for CREATEROLE users to GRANT role
membership to all non-superuser roles on the system. CREATEROLE
shouldn't be allowing that.
I'm on board for a hard-coded fix as well - if a superuser is in the
membership chain of a role then non-superusers cannot grant membership in
that role to others.
Why not just look at the admin_option field of pg_auth_members...? I
don't get why that isn't an even more minimal fix than this idea you
have of adding a column to pg_authid and then propagating around "this
user could become a superuser" or writing code that has to go check "is
there some way for this role to become a superuser, either directly or
through some subset of pg_* roles?"
Neither of those really solves the pg_* roles problem. We still need to
indicate that they are somehow special. Whether it is a nice matrix or
roles and permissions or a simple attribute that makes them behave like
they are superuser roles.
I disagree that they should be considered special when it comes to role
membership and management. They're just roles, like any other.
I disagree entirely with the idea that we must have some roles who can
only ever be administered by a superuser.I don't think this is a must have. I think that since we do have it today
that fixes that leverage the status quo in order to be done more easily are
perfectly valid solutions.
We have a half-way-implemented attempt at this, not something that's
actually effective, and therefore I don't agree that we really have it
today or that we should keep it. I'd much prefer to throw out nearly
everything in the system that's doing an explicit check of "does this
role have a superuser bit set on it?"
If anything, we should be
moving away (as we have, in fact, been doing), from anything being the
exclusive purview of the superuser.I totally agree.
Great.
In particular, this means CREATEROLE roles cannot assign membership in
the
marked roles; just like they cannot assign membership in superuser roles
today.I disagree with the idea that we need to mark some roles as only being
able to be modified by the superuser- why invent this?Because CREATEUSER is a thing and people want to prevent roles with that
attribute from assigning membership to the predefined superuser-aspect
roles. If I've misunderstood that desire and the scope of delegation given
by the superuser to CREATEUSER roles is acceptable, then no change here is
needed.
We can do that by using the admin_option in pg_auth_members instead
though and limiting everyone to using that.
What you
seem to be arguing for here is to rip out the ADMIN functionality, which
is defined by spec and not even exclusively by PG, and replace it with a
single per-role flag that says if that role can only be modified by
superusers.I made the observation that being able to manage the membership of a group
without having the ability to create new users seems like a half a loaf of
a feature. That's it. I would presume that any redesign of the
permissions system here would address this adequately.
If the new design ideas that are being thrown around don't address what
you're thinking they should, it'd be great to point that out.
The
short answer is that it's not- which is why we have documented
CREATEROLE as being 'superuser light'. The goal here is to get rid of
that.Now you tell me. Robert should have led with that goal upfront.
... blink.
Yes, we're talking about a new feature- one intended to replace the
broken way that CREATEROLE works, which your proposal doesn't.That is correct, I was trying to figure out minimally invasive fixes to
what are arguably being called bugs.
What's been proposed here doesn't strike me as minimally invasive,
though I suppose I'm looking at it more from the database system
perspective and less from the end-user side of things for people who
actually use CREATEROLE, but in this particular case, that's the side
I'm on.
Thanks,
Stephen
Greetings,
* Robert Haas (robertmhaas@gmail.com) wrote:
But that's not to say that we couldn't decide to do something else
instead, and that other thing might well be better. Do you want to
sketch out a full proposal, even just what the syntax would look like,
and share that here? And if you could explain how I could use it to
create the mini-superusers that I'm trying to get out of this thing,
even better.
It'd be useful to have a better definition of exactly what a
'mini-superuser' is, but at least for the moment when it comes to roles,
let's look at what the spec says:
CREATE ROLE
- Who is allowed to run CREATE ROLE is implementation-defined
- After creation, this is effictively run:
GRANT new_role TO creator_role WITH ADMIN, GRANTOR "_SYSTEM"
DROP ROLE
- Any user who has been GRANT'd a role with ADMIN option is able to
DROP that role.
GRANT ROLE
- No cycles allowed
- A role must have ADMIN rights on the role to be able to GRANT it to
another role.
ALTER ROLE
- Doesn't exist
This actually looks to me like more-or-less what you're looking for, it
just isn't what we have today because CREATEROLE brings along with it a
bunch of other stuff, some of which we want and some that we don't, and
some things that the SQL spec says ADMIN should be allowed to do (DROP
ROLE) we don't allow today.
It's also not quite what I want because it requires that membership and
ADMIN go together where I'd like to be able to have those be
independently GRANT'able- and then some.
I don't think we're that far from having all of these though. To start
with, we remove from CREATEROLE the random things that it does which go
beyond what folks tend to expect- remove the whole 'grant any role to
any other' stuff, remove the 'drop role' exception, remove the
'alter role' stuff. Do make it so that when you create a role, however,
the above GRANT is effectively done. Now, for the items above where we
removed the checks against have_createrole_privilege() we go back and
add in checks using is_admin_of_role(). Of course, also remove the role
self-administration bug.
That's step #1, but it gets us more-or-less what you're looking for, I
think, and brings us a lot closer to what the spec has.
Step #2 is also in-line with the spec: track GRANTORs and care about
them, for everything. We really should have been doing this all along.
Note that I'm not saying that an owner of a table can't REVOKE some
right that was GRANT'd on that table, but rather that a user who was
GRANT'd ADMIN rights on a table and then GRANT'd that right to some
other user shouldn't have some other user who only has ADMIN rights on
the table be able to remove that GRANT. Same goes for roles, meaning
that you could GRANT rights in a role with ADMIN option and not have to
be afraid that the role you just gave that to will be able to remove
*your* ADMIN rights on that role. In general, I don't think this
would actually have a very large impact on users because most users
don't, today, use the ADMIN option much.
Step #3 starts going in the direction of what I'd like to see, which
would be to break out membership in a role as a separate thing from
admin rights on that role. This is also what would help with the 'bot'
use-case that Joshua (not David Steele, btw) brought up.
Step #4 then breaks the 'admin' option on roles into pieces- a 'drop
role' right, a 'reset password' right, maybe separate rights for
different role attributes, etc. We would likely still keep the
'admin_option' column in pg_auth_members and just check that first
and then check the individual rights (similar to table-level vs.
column-level privileges) so that we stay in line with the spec's
expectation here and with what users are used to.
In some hyptothetical world, there's even a later step #5 which allows
us to define user profiles and then grant the ability for a user to
create a role with a certain profile (but not any arbitrary profile),
thus making things like the 'bot' even more constrained in terms of
what it's able to do (maybe it can then create a role that's a member of
a role without itself being a member of that role or explicitly having
admin rights in that role, as an example).
Thanks,
Stephen
On Thu, Mar 10, 2022 at 12:45 PM Stephen Frost <sfrost@snowman.net> wrote:
* David G. Johnston (david.g.johnston@gmail.com) wrote:
On Thu, Mar 10, 2022 at 11:05 AM Stephen Frost <sfrost@snowman.net>
wrote:
Why not just look at the admin_option field of pg_auth_members...? I
don't get why that isn't an even more minimal fix than this idea you
have of adding a column to pg_authid and then propagating around "this
user could become a superuser" or writing code that has to go check "is
there some way for this role to become a superuser, either directly or
through some subset of pg_* roles?"
Indeed, maybe I am wrong on the scope of the patch. But at least for the
explicit attribute it should be no more difficult than changing:
if (grouprole_is_superuser and current_role_is_not_superuser) then error:
to be
if ((grouoprole_is_superuser OR !groupuser_has_adminattr) AND
current_role_is_not_superuser) then error;
I have to imagine that given how fundamental inheritance is to our
permissions system than doing a similar check up the tree wouldn't be
difficult, but I truly don't know with a strong degree of certainty.
Assuming we don't actually rip out CREATEROLE when this change goes in...do
you propose to prohibit a CREATEROLE user from altering the membership
roster of any group which itself is not a member of and also those which it
is a member of but where admin_option is false?
I don't personally have a problem with the current state where CREATEROLE
is an admin for, but not a member of, every non-superuser(-related) role in
the system. If the consensus is to change that then I suppose this becomes
the minimally invasive fix that accomplishes that goal as well. It seems
incomplete though, since you still need superuser to create a group and add
the initial WITH ADMIN member to it. So this seems to work in the "avoid
using superuser" sense if you've also added something that has what
CREATEROLE provides today - admin without membership - but that would have
the benefit of not carrying around all the baggage that CREATEROLE has.
I made the observation that being able to manage the membership of a
group
without having the ability to create new users seems like a half a loaf
of
a feature. That's it. I would presume that any redesign of the
permissions system here would address this adequately.If the new design ideas that are being thrown around don't address what
you're thinking they should, it'd be great to point that out.
I mean, you need a Create Role permission in some form, even if it's
deprecating the attribute and making it a predefined role. I picked this
thread up because it seemed like a limited scope that I could get my head
around with the time I have, with the main goal to try to understand this
aspect of the system better. I haven't gone and looked into the main
thread yet.
David J.
On Thu, Mar 10, 2022 at 2:58 PM Stephen Frost <sfrost@snowman.net> wrote:
It'd be useful to have a better definition of exactly what a
'mini-superuser' is, but at least for the moment when it comes to roles,
let's look at what the spec says:
Gosh, I feel like I've spelled that out approximately 463,121 times
already. That estimate might be slightly off though; I've been known
to make mistakes from time to time....
CREATE ROLE
- Who is allowed to run CREATE ROLE is implementation-defined
- After creation, this is effictively run:
GRANT new_role TO creator_role WITH ADMIN, GRANTOR "_SYSTEM"DROP ROLE
- Any user who has been GRANT'd a role with ADMIN option is able to
DROP that role.GRANT ROLE
- No cycles allowed
- A role must have ADMIN rights on the role to be able to GRANT it to
another role.ALTER ROLE
- Doesn't existThis actually looks to me like more-or-less what you're looking for, it
just isn't what we have today because CREATEROLE brings along with it a
bunch of other stuff, some of which we want and some that we don't, and
some things that the SQL spec says ADMIN should be allowed to do (DROP
ROLE) we don't allow today.
The above is mostly fine with me, except for the part about ALTER ROLE
not existing. I think it's always good to be able to change your mind
post-CREATE.
Basically, in this sketch, ADMIN OPTION on a role involves the ability
to DROP it, which means we don't need a separate role owner concept.
It also involves membership, meaning that you can freely exercise the
privileges of the role without SET ROLE. While I'm totally down with
having other possible behaviors as options, that particular behavior
seems very useful to me, so, sounds great.
It's also not quite what I want because it requires that membership and
ADMIN go together where I'd like to be able to have those be
independently GRANT'able- and then some.I don't think we're that far from having all of these though. To start
with, we remove from CREATEROLE the random things that it does which go
beyond what folks tend to expect- remove the whole 'grant any role to
any other' stuff, remove the 'drop role' exception, remove the
'alter role' stuff. Do make it so that when you create a role, however,
the above GRANT is effectively done. Now, for the items above where we
removed the checks against have_createrole_privilege() we go back and
add in checks using is_admin_of_role(). Of course, also remove the role
self-administration bug.
What do you mean by the 'drop role' exception?
I don't like removing 'alter role'.
The rest sounds good.
That's step #1, but it gets us more-or-less what you're looking for, I
think, and brings us a lot closer to what the spec has.
Great.
Step #2 is also in-line with the spec: track GRANTORs and care about
them, for everything. We really should have been doing this all along.
Note that I'm not saying that an owner of a table can't REVOKE some
right that was GRANT'd on that table, but rather that a user who was
GRANT'd ADMIN rights on a table and then GRANT'd that right to some
other user shouldn't have some other user who only has ADMIN rights on
the table be able to remove that GRANT. Same goes for roles, meaning
that you could GRANT rights in a role with ADMIN option and not have to
be afraid that the role you just gave that to will be able to remove
*your* ADMIN rights on that role. In general, I don't think this
would actually have a very large impact on users because most users
don't, today, use the ADMIN option much.
There are details to work out here, but in general, I like it.
Step #3 starts going in the direction of what I'd like to see, which
would be to break out membership in a role as a separate thing from
admin rights on that role. This is also what would help with the 'bot'
use-case that Joshua (not David Steele, btw) brought up.
Woops, apologies for getting the name wrong. I also said Marc earlier
when I meant Mark, because I work with people named Mark, Marc, and
Marc, and Mark's spelling got outvoted by some distant corner of my
brain.
I think this is a fine long-term direction, with the caveat that
you've not provided enough specifics here for me to really understand
how it would work. I fear the specifics might be hard to get right,
both in terms of making it understandable to users and in terms of
preserving as much backward-compatibility as we can. However, I am not
opposed to the concept.
Step #4 then breaks the 'admin' option on roles into pieces- a 'drop
role' right, a 'reset password' right, maybe separate rights for
different role attributes, etc. We would likely still keep the
'admin_option' column in pg_auth_members and just check that first
and then check the individual rights (similar to table-level vs.
column-level privileges) so that we stay in line with the spec's
expectation here and with what users are used to.
Same comments as #3, plus I wonder whether it really makes sense to
separate #3 and #4. But we can decide that when there's a fleshed-out
design for this.
In some hyptothetical world, there's even a later step #5 which allows
us to define user profiles and then grant the ability for a user to
create a role with a certain profile (but not any arbitrary profile),
thus making things like the 'bot' even more constrained in terms of
what it's able to do (maybe it can then create a role that's a member of
a role without itself being a member of that role or explicitly having
admin rights in that role, as an example).
Right. I don't object to this either, hypothetically, but I think
we're a long way from understanding how to get there, and I don't want
step #1 to get blocked behind all the rest of this. Particularly the
part where we remove the role self-administration thing.
--
Robert Haas
EDB: http://www.enterprisedb.com
Greetings,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Thu, Mar 10, 2022 at 2:58 PM Stephen Frost <sfrost@snowman.net> wrote:
It'd be useful to have a better definition of exactly what a
'mini-superuser' is, but at least for the moment when it comes to roles,
let's look at what the spec says:Gosh, I feel like I've spelled that out approximately 463,121 times
already. That estimate might be slightly off though; I've been known
to make mistakes from time to time....
If there's a specific message that details it closely on the lists
somewhere, I'm happy to go review it. I admit that I didn't go back and
look for such.
CREATE ROLE
- Who is allowed to run CREATE ROLE is implementation-defined
- After creation, this is effictively run:
GRANT new_role TO creator_role WITH ADMIN, GRANTOR "_SYSTEM"DROP ROLE
- Any user who has been GRANT'd a role with ADMIN option is able to
DROP that role.GRANT ROLE
- No cycles allowed
- A role must have ADMIN rights on the role to be able to GRANT it to
another role.ALTER ROLE
- Doesn't existThis actually looks to me like more-or-less what you're looking for, it
just isn't what we have today because CREATEROLE brings along with it a
bunch of other stuff, some of which we want and some that we don't, and
some things that the SQL spec says ADMIN should be allowed to do (DROP
ROLE) we don't allow today.The above is mostly fine with me, except for the part about ALTER ROLE
not existing. I think it's always good to be able to change your mind
post-CREATE.
Errr, just to be clear, ALTER ROLE doesn't exist *in the spec*. I
wasn't suggesting that we get rid of it, just that it doesn't exist in
the spec and therefore the spec doesn't have anything to say about it.
Basically, in this sketch, ADMIN OPTION on a role involves the ability
to DROP it, which means we don't need a separate role owner concept.
Right. The above doesn't include any specifics about what to do with
ALTER ROLE, but my thought would be to have it also be under ADMIN
OPTION rather than under CREATEROLE, as I tried to outline (though not
very well, I'll admit) below.
It also involves membership, meaning that you can freely exercise the
privileges of the role without SET ROLE. While I'm totally down with
having other possible behaviors as options, that particular behavior
seems very useful to me, so, sounds great.
Well, yes and no- by default you're right, presuming everything is set
as inheirited, but I'd wish for us to keep the option of creating roles
which are noinherit and having that work just as it does today.
It's also not quite what I want because it requires that membership and
ADMIN go together where I'd like to be able to have those be
independently GRANT'able- and then some.I don't think we're that far from having all of these though. To start
with, we remove from CREATEROLE the random things that it does which go
beyond what folks tend to expect- remove the whole 'grant any role to
any other' stuff, remove the 'drop role' exception, remove the
'alter role' stuff. Do make it so that when you create a role, however,
the above GRANT is effectively done. Now, for the items above where we
removed the checks against have_createrole_privilege() we go back and
add in checks using is_admin_of_role(). Of course, also remove the role
self-administration bug.What do you mean by the 'drop role' exception?
'ability' was probably a better word there. What I'm talking about is
changing in DropRole:
if (!have_createrole_privilege())
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to drop role")));
to be, more or less:
if (!is_admin_of_role(role))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to drop role")));
I don't like removing 'alter role'.
Ditto above but for AlterRole. Taking it away from users with
CREATEROLE being able to run those commands on $anyrole and instead
making it so that the role running DROP ROLE or ALTER ROLE needs to have
ADMIN on the role they're messing with. I do think we may also need to
make some adjustments in terms of what a regular user WITH ADMIN on a
given role is able to do when it comes to ALTER ROLE, in particular, I
don't think we'll want to remove the existing is-superuser checks
against a user settings bypassrls or replication or superuser on some
other role. Maybe we can provide a way for a non-superuser to be given
the ability to set those attributes for roles they create, but that
would be a separate thing.
The rest sounds good.
Great.
That's step #1, but it gets us more-or-less what you're looking for, I
think, and brings us a lot closer to what the spec has.Great.
Awesome.
Step #2 is also in-line with the spec: track GRANTORs and care about
them, for everything. We really should have been doing this all along.
Note that I'm not saying that an owner of a table can't REVOKE some
right that was GRANT'd on that table, but rather that a user who was
GRANT'd ADMIN rights on a table and then GRANT'd that right to some
other user shouldn't have some other user who only has ADMIN rights on
the table be able to remove that GRANT. Same goes for roles, meaning
that you could GRANT rights in a role with ADMIN option and not have to
be afraid that the role you just gave that to will be able to remove
*your* ADMIN rights on that role. In general, I don't think this
would actually have a very large impact on users because most users
don't, today, use the ADMIN option much.There are details to work out here, but in general, I like it.
Cool. Note that superusers would still be able to do $anything,
including removing someone's ADMIN rights on a role even if that
superuser didn't GRANT it (at least, that's my thinking on this).
Step #3 starts going in the direction of what I'd like to see, which
would be to break out membership in a role as a separate thing from
admin rights on that role. This is also what would help with the 'bot'
use-case that Joshua (not David Steele, btw) brought up.Woops, apologies for getting the name wrong. I also said Marc earlier
when I meant Mark, because I work with people named Mark, Marc, and
Marc, and Mark's spelling got outvoted by some distant corner of my
brain.
Hah, no worries.
I think this is a fine long-term direction, with the caveat that
you've not provided enough specifics here for me to really understand
how it would work. I fear the specifics might be hard to get right,
both in terms of making it understandable to users and in terms of
preserving as much backward-compatibility as we can. However, I am not
opposed to the concept.
We can perhaps debate the specifics around this later.
Step #4 then breaks the 'admin' option on roles into pieces- a 'drop
role' right, a 'reset password' right, maybe separate rights for
different role attributes, etc. We would likely still keep the
'admin_option' column in pg_auth_members and just check that first
and then check the individual rights (similar to table-level vs.
column-level privileges) so that we stay in line with the spec's
expectation here and with what users are used to.Same comments as #3, plus I wonder whether it really makes sense to
separate #3 and #4. But we can decide that when there's a fleshed-out
design for this.
Ditto. I don't know that they need to be independent either.
In some hyptothetical world, there's even a later step #5 which allows
us to define user profiles and then grant the ability for a user to
create a role with a certain profile (but not any arbitrary profile),
thus making things like the 'bot' even more constrained in terms of
what it's able to do (maybe it can then create a role that's a member of
a role without itself being a member of that role or explicitly having
admin rights in that role, as an example).Right. I don't object to this either, hypothetically, but I think
we're a long way from understanding how to get there, and I don't want
step #1 to get blocked behind all the rest of this. Particularly the
part where we remove the role self-administration thing.
Sure.
Thanks,
Stephen
On Thu, Mar 10, 2022 at 12:58 PM Stephen Frost <sfrost@snowman.net> wrote:
I don't think we're that far from having all of these though. To start
with, we remove from CREATEROLE the random things that it does which go
beyond what folks tend to expect- remove the whole 'grant any role to
any other' stuff, remove the 'drop role' exception, remove the
'alter role' stuff. Do make it so that when you create a role, however,
the above GRANT is effectively done. Now, for the items above where we
removed the checks against have_createrole_privilege() we go back and
add in checks using is_admin_of_role(). Of course, also remove the role
self-administration bug.That's step #1, but it gets us more-or-less what you're looking for, I
think, and brings us a lot closer to what the spec has.
That still leaves attribute specification in place: e.g., REPLICATION,
CREATEROLE, CREATEDB, etc... (I see BYPASSRLS already is SUPERUSER only)
I dislike changing the documented behavior of CREATEROLE to the degree
suggested here. However, there are three choices here, only one of which
can be chosen:
1. Leave CREATEROLE alone entirely
2. Make it so CREATEROLE cannot assign membership to the predefined roles
or superuser (inheritance included), but leave the rest alone. This would
be the hard-coded version, not the role attribute one.
3. Make it so CREATEROLE can only assign membership to roles for which it
has been made an admin; as well as the other things mentioned
Moving forward I'd prefer options 1 or 2, leaving the ability to
create/alter/drop a role to be vested via predefined roles.
The rest seems fine at an initial glance.
David J.
On Thu, Mar 10, 2022 at 3:41 PM Stephen Frost <sfrost@snowman.net> wrote:
Gosh, I feel like I've spelled that out approximately 463,121 times
already. That estimate might be slightly off though; I've been known
to make mistakes from time to time....If there's a specific message that details it closely on the lists
somewhere, I'm happy to go review it. I admit that I didn't go back and
look for such.
Probably easier to just say it again: I want to have users that can
create roles and then have superuser-like powers with respect to those
roles. They can freely exercise the privileges of those roles, and
they can do all the things that a superuser can do but only with
respect to those roles. They cannot break out to the OS. I think it's
pretty similar to what you are describing, with a couple of possible
exceptions. For example, would you imagine that being an admin of a
login role would let you change that user's password? Because that
would be desirable behavior from where I sit.
Errr, just to be clear, ALTER ROLE doesn't exist *in the spec*. I
wasn't suggesting that we get rid of it, just that it doesn't exist in
the spec and therefore the spec doesn't have anything to say about it.
Oh, OK.
Basically, in this sketch, ADMIN OPTION on a role involves the ability
to DROP it, which means we don't need a separate role owner concept.Right. The above doesn't include any specifics about what to do with
ALTER ROLE, but my thought would be to have it also be under ADMIN
OPTION rather than under CREATEROLE, as I tried to outline (though not
very well, I'll admit) below.
This sentence really confused me at first, but I think you're saying
that the right to alter a role would be dependent on having ADMIN
OPTION on the role rather than on having the CREATEROLE attribute.
That seems like a reasonable idea to me.
It also involves membership, meaning that you can freely exercise the
privileges of the role without SET ROLE. While I'm totally down with
having other possible behaviors as options, that particular behavior
seems very useful to me, so, sounds great.Well, yes and no- by default you're right, presuming everything is set
as inheirited, but I'd wish for us to keep the option of creating roles
which are noinherit and having that work just as it does today.
Hmm, so if I have membership WITH ADMIN OPTION in a role, but my role
is marked NOINHERIT, that means I can't exercise the privileges of
that role without SET ROLE. But, can I still do other things to that
role, such as dropping it? Given the current coding of
roles_is_member_of(), it seems like I can't. I don't like that, but
then I don't like much of anything about NOINHERIT. Do you have any
suggestions for how this could be improved?
To make this more concrete, suppose the superuser does "CREATE USER
alice CREATEROLE". Alice will have INHERIT, so she'll have control
over any roles she creates. But if she does "CREATE USER bob
CREATEROLE NOINHERIT" then neither she nor Bob will be able to control
the roles bob creates. I'd like to have a way to make it so that
neither Alice nor any other CREATEROLE users she spins up can create
roles over which they no longer have control. Because otherwise people
will do dumb stuff like that and then have to call the superuser to
sort it out, and the superuser won't like that because s/he is a super
busy person.
What do you mean by the 'drop role' exception?
'ability' was probably a better word there. What I'm talking about is
changing in DropRole:to be, more or less:
if (!is_admin_of_role(role))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied to drop role")));
Sounds good.
I don't like removing 'alter role'.
Ditto above but for AlterRole. Taking it away from users with
CREATEROLE being able to run those commands on $anyrole and instead
making it so that the role running DROP ROLE or ALTER ROLE needs to have
ADMIN on the role they're messing with. I do think we may also need to
make some adjustments in terms of what a regular user WITH ADMIN on a
given role is able to do when it comes to ALTER ROLE, in particular, I
don't think we'll want to remove the existing is-superuser checks
against a user settings bypassrls or replication or superuser on some
other role. Maybe we can provide a way for a non-superuser to be given
the ability to set those attributes for roles they create, but that
would be a separate thing.
This too.
Step #2 is also in-line with the spec: track GRANTORs and care about
them, for everything. We really should have been doing this all along.There are details to work out here, but in general, I like it.
Cool. Note that superusers would still be able to do $anything,
including removing someone's ADMIN rights on a role even if that
superuser didn't GRANT it (at least, that's my thinking on this).
Agree. I also think that it would be a good idea to attribute grants
performed by any superuser to the bootstrap superuser, or leave them
unattributed somehow. Because otherwise dropping superusers becomes a
pain in the tail for no good reason.
We might also need to think carefully about what happens if for
example the table owner is changed. If bob owns the table and we
change the owner to mary, but bob's previous grants are still
attributed to bob, I'm not sure that's going to be very convenient.
Possibly if the table owner changes we also change the owner of all
grants attributed to the old table owner to be attributed to the new
table owner?
--
Robert Haas
EDB: http://www.enterprisedb.com
On Thu, Mar 10, 2022 at 02:22:05PM -0500, Robert Haas wrote:
I mean, I didn't design pg_hba.conf, but I think it's part of the
database doing a reasonable thing, not an external system doing a
nonsensical thing.
FYI, I think pg_hba.conf gets away with having negative/reject
permissions only because it is strictly ordered.
--
Bruce Momjian <bruce@momjian.us> https://momjian.us
EDB https://enterprisedb.com
If only the physical world exists, free will is an illusion.
On Thu, Mar 10, 2022 at 4:00 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:
I dislike changing the documented behavior of CREATEROLE to the degree suggested here. However, there are three choices here, only one of which can be chosen:
1. Leave CREATEROLE alone entirely
2. Make it so CREATEROLE cannot assign membership to the predefined roles or superuser (inheritance included), but leave the rest alone. This would be the hard-coded version, not the role attribute one.
3. Make it so CREATEROLE can only assign membership to roles for which it has been made an admin; as well as the other things mentionedMoving forward I'd prefer options 1 or 2, leaving the ability to create/alter/drop a role to be vested via predefined roles.
It sounds like you prefer a behavior where CREATEROLE gives power over
all non-superusers, but that seems pretty limiting to me. Why can't
someone want to create a user with power over some users but not
others? For example, the superuser might want to give alice the
ability to set up new users in the accounting department, but NOT give
alice the right to tinker with the backup user (who is not a
superuser, but doesn't have the replication privilege). How would they
accomplish that in your view?
--
Robert Haas
EDB: http://www.enterprisedb.com
On Thu, Mar 10, 2022 at 5:00 PM Bruce Momjian <bruce@momjian.us> wrote:
On Thu, Mar 10, 2022 at 02:22:05PM -0500, Robert Haas wrote:
I mean, I didn't design pg_hba.conf, but I think it's part of the
database doing a reasonable thing, not an external system doing a
nonsensical thing.FYI, I think pg_hba.conf gets away with having negative/reject
permissions only because it is strictly ordered.
I agree.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Thu, Mar 10, 2022 at 3:01 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Mar 10, 2022 at 4:00 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:I dislike changing the documented behavior of CREATEROLE to the degree
suggested here. However, there are three choices here, only one of which
can be chosen:1. Leave CREATEROLE alone entirely
2. Make it so CREATEROLE cannot assign membership to the predefinedroles or superuser (inheritance included), but leave the rest alone. This
would be the hard-coded version, not the role attribute one.3. Make it so CREATEROLE can only assign membership to roles for which
it has been made an admin; as well as the other things mentioned
Moving forward I'd prefer options 1 or 2, leaving the ability to
create/alter/drop a role to be vested via predefined roles.
It sounds like you prefer a behavior where CREATEROLE gives power over
all non-superusers, but that seems pretty limiting to me.
Doh! I edited out the part where I made clear I considered options 1 and 2
as basically being done for a limited period of time while deprecating the
CREATEROLE attribute altogether in favor of the fine-grained and predefined
role based permission granting. I don't want to nerf CREATEROLE as part of
adding this new feature, instead leave it as close to status quo as
reasonable so as not to mess up existing setups that make use of it. We
can note in the release notes and documentation that we consider CREATEROLE
to be deprecated and that the new predefined role should be used to give a
user the ability to create/alter/drop roles, etc... DBAs should consider
revoking CREATEROLE from their users and granting them proper memberships
in the predefined roles and the groups those roles should be administering.
David J.
Robert Haas <robertmhaas@gmail.com> writes:
Probably easier to just say it again: I want to have users that can
create roles and then have superuser-like powers with respect to those
roles. They can freely exercise the privileges of those roles, and
they can do all the things that a superuser can do but only with
respect to those roles.
This seems reasonable in isolation, but
(1) it implies a persistent relationship between creating and created
roles. Whether you want to call that ownership or not, it sure walks
and quacks like ownership.
(2) it seems exactly contradictory to your later point that
Agree. I also think that it would be a good idea to attribute grants
performed by any superuser to the bootstrap superuser, or leave them
unattributed somehow. Because otherwise dropping superusers becomes a
pain in the tail for no good reason.
Either there's a persistent relationship or there's not. I don't
think it's sensible to treat superusers differently here.
I think that this argument about the difficulty of dropping superusers
may in fact be the motivation for the existing behavior that object-
permissions GRANTs done by superusers are attributed to the object
owner; something you were unhappy about upthread.
In the end these requirements seem mutually contradictory. Either
we can have a persistent ownership relationship or not, but I don't
think we can have it apply in some cases and not others without
creating worse problems than we solve. I'm inclined to toss overboard
the requirement that superusers need to be an easy thing to drop.
Why is that important, anyway?
We might also need to think carefully about what happens if for
example the table owner is changed. If bob owns the table and we
change the owner to mary, but bob's previous grants are still
attributed to bob, I'm not sure that's going to be very convenient.
That's already handled, is it not?
regression=# create user alice;
CREATE ROLE
regression=# create user bob;
CREATE ROLE
regression=# create user charlie;
CREATE ROLE
regression=# \c - alice
You are now connected to database "regression" as user "alice".
regression=> create table alices_table (f1 int);
CREATE TABLE
regression=> grant select on alices_table to bob;
GRANT
regression=> \c - postgres
You are now connected to database "regression" as user "postgres".
regression=# alter table alices_table owner to charlie;
ALTER TABLE
regression=# \dp alices_table
Access privileges
Schema | Name | Type | Access privileges | Column privileges | Policies
--------+--------------+-------+-------------------------+-------------------+----------
public | alices_table | table | charlie=arwdDxt/charlie+| |
| | | bob=r/charlie | |
(1 row)
I'm a bit disturbed that parts of this discussion seem to be getting
conducted with little understanding of the system's existing behaviors.
We should not be reinventing things we already have perfectly good
solutions for in the object-privileges domain.
regards, tom lane
On Mar 10, 2022, at 2:01 PM, Robert Haas <robertmhaas@gmail.com> wrote:
It sounds like you prefer a behavior where CREATEROLE gives power over
all non-superusers, but that seems pretty limiting to me. Why can't
someone want to create a user with power over some users but not
others?
I agree with Robert on this.
Over at [1]/messages/by-id/53C7DF4C-8463-4647-9DFD-779B5E1861C4@amazon.com, I introduced a patch series to (a) change CREATEROLE and (b) introduce role ownership. Part (a) wasn't that controversial. The patch series failed to make it for postgres 15 on account of (b). The patch didn't go quite far enough, but with it applied, this is an example of a min-superuser "lord" operating within database "fiefdom":
fiefdom=# -- mini-superuser who can create roles and write all data
fiefdom=# CREATE ROLE lord
fiefdom-# WITH CREATEROLE
fiefdom-# IN ROLE pg_write_all_data;
CREATE ROLE
fiefdom=#
fiefdom=# -- group which "lord" belongs to
fiefdom=# CREATE GROUP squire
fiefdom-# ROLE lord;
CREATE ROLE
fiefdom=#
fiefdom=# -- group which "lord" has no connection to
fiefdom=# CREATE GROUP paladin;
CREATE ROLE
fiefdom=#
fiefdom=# SET SESSION AUTHORIZATION lord;
SET
fiefdom=>
fiefdom=> -- fail, merely a member of "squire"
fiefdom=> CREATE ROLE peon IN ROLE squire;
ERROR: must have admin option on role "squire"
fiefdom=>
fiefdom=> -- fail, no privilege to grant CREATEDB
fiefdom=> CREATE ROLE peon CREATEDB;
ERROR: must have createdb privilege to create createdb users
fiefdom=>
fiefdom=> RESET SESSION AUTHORIZATION;
RESET
fiefdom=#
fiefdom=# -- grant admin over "squire" to "lord"
fiefdom=# GRANT squire
fiefdom-# TO lord
fiefdom-# WITH ADMIN OPTION;
GRANT ROLE
fiefdom=#
fiefdom=# SET SESSION AUTHORIZATION lord;
SET
fiefdom=>
fiefdom=> -- ok, have both "CREATEROLE" and admin option for "squire"
fiefdom=> CREATE ROLE peon IN ROLE squire;
CREATE ROLE
fiefdom=>
fiefdom=> -- fail, no privilege to grant CREATEDB
fiefdom=> CREATE ROLE peasant CREATEDB IN ROLE squire;
ERROR: must have createdb privilege to create createdb users
fiefdom=>
fiefdom=> RESET SESSION AUTHORIZATION;
RESET
fiefdom=#
fiefdom=# -- Give lord the missing privilege
fiefdom=# GRANT CREATEDB TO lord;
ERROR: role "createdb" does not exist
fiefdom=#
fiefdom=# RESET SESSION AUTHORIZATION;
RESET
fiefdom=#
fiefdom=# -- ok, have "CREATEROLE", "CREATEDB", and admin option for "squire"
fiefdom=# CREATE ROLE peasant CREATEDB IN ROLE squire;
CREATE ROLE
The problem with this is that "lord" needs CREATEDB to grant CREATEDB, but really it should need something like grant option on "CREATEDB". But that's hard to do with the existing system, given the way these privilege bits are represented. If we added a few more built-in pg_* roles, such as pg_create_db, it would just work. CREATEROLE itself could be reimagined as pg_create_role, and then users could be granted into this role with or without admin option, meaning they could/couldn't further give it away. I think that would be a necessary component to Joshua's "bot" use-case, since the bot must itself have the privilege to create roles, but shouldn't necessarily be trusted with the privilege to create additional roles who have it.
[1]: /messages/by-id/53C7DF4C-8463-4647-9DFD-779B5E1861C4@amazon.com
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Thu, Mar 10, 2022 at 5:14 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
This seems reasonable in isolation, but
(1) it implies a persistent relationship between creating and created
roles. Whether you want to call that ownership or not, it sure walks
and quacks like ownership.
I agree. It's been obvious to me from the beginning that we needed
such a persistent relationship, and also that it needed to be a
relationship from which the created role couldn't simply walk away.
Yet, more than six months after the first discussions of this topic,
we still don't have any kind of agreement on what that thing should be
called. I like my TENANT idea best, but I'm perfectly willing to call
it ownership as you seem to prefer or WITH ADMIN OPTION as Stephen
seems to prefer if one of those ideas gains consensus. But we've
managed to waste all hope of making any significant progress here for
an entire release cycle for lack of ability to agree on spelling. I
think that's unfair to Mark, who put a lot of work into this area and
got nothing out of it, and I think it sucks for users of PostgreSQL,
too.
(2) it seems exactly contradictory to your later point that
Agree. I also think that it would be a good idea to attribute grants
performed by any superuser to the bootstrap superuser, or leave them
unattributed somehow. Because otherwise dropping superusers becomes a
pain in the tail for no good reason.Either there's a persistent relationship or there's not. I don't
think it's sensible to treat superusers differently here.I think that this argument about the difficulty of dropping superusers
may in fact be the motivation for the existing behavior that object-
permissions GRANTs done by superusers are attributed to the object
owner; something you were unhappy about upthread.In the end these requirements seem mutually contradictory. Either
we can have a persistent ownership relationship or not, but I don't
think we can have it apply in some cases and not others without
creating worse problems than we solve. I'm inclined to toss overboard
the requirement that superusers need to be an easy thing to drop.
Why is that important, anyway?
Well, I think you're looking at it the wrong way. Compared to getting
useful functionality, the relative ease of dropping users is
completely unimportant. I'm happy to surrender it in exchange for
something else. I just don't see why we should give it up for nothing.
If Alice creates non-superusers Bob and Charlie, and Charlie creates
Doug, we need the persistent relationship to know that Charlie is
allowed to drop Doug and Bob is not. But if Charlie is a superuser
anyway, then the persistent relationship is of no use. I don't see the
point of cluttering up the system with such dependencies. Will I do it
that way, if that's what it takes to get the patch accepted? Sure. But
I can't imagine any end-user actually liking it.
I'm a bit disturbed that parts of this discussion seem to be getting
conducted with little understanding of the system's existing behaviors.
We should not be reinventing things we already have perfectly good
solutions for in the object-privileges domain.
I did wonder whether that might be the existing behavior, but stopping
to check right at that moment didn't seem that important to me. Maybe
I should have taken the time, but it's not like we're writing the
final patch for commit next Tuesday at this point. It's more important
at this point to get agreement on the principles. That said, I do
agree that there have been times when we haven't thought hard enough
about the existing behavior in proposing new behavior. On the third
hand, though, part of the problem here is that neither Stephen nor I
are entirely happy with the existing behavior, if for somewhat
different reasons. It really isn't "perfectly good." On the one hand,
from a purely technical standpoint, a lot of the behavior around roles
in particular seems well below the standard that anyone would consider
committable today. On the other hand, even the parts of the code that
are in reasonable shape from a code quality point of view don't
actually do the things that we think users want done.
--
Robert Haas
EDB: http://www.enterprisedb.com
Greetings,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Thu, Mar 10, 2022 at 5:14 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
This seems reasonable in isolation, but
(1) it implies a persistent relationship between creating and created
roles. Whether you want to call that ownership or not, it sure walks
and quacks like ownership.
I agree that there would be a recorded relationship (that is, one that
we write into the catalog and keep around until and unless it's removed
by an admin) between creating and created roles and that's probably the
default when CREATE ROLE is run but, unlike tables or such objects in
the system, I don't agree that we should require this to exist at
absolutely all times for every role (what would it be for the bootstrap
superuser..?). At least today, that's distinct from how ownership in
the system works. I also don't believe that this is necessarily an
issue for Robert's use-case, as long as there are appropriate
restrictions around who is allowed to remove or modify these
relationships.
I agree. It's been obvious to me from the beginning that we needed
such a persistent relationship, and also that it needed to be a
relationship from which the created role couldn't simply walk away.
Yet, more than six months after the first discussions of this topic,
we still don't have any kind of agreement on what that thing should be
called. I like my TENANT idea best, but I'm perfectly willing to call
it ownership as you seem to prefer or WITH ADMIN OPTION as Stephen
seems to prefer if one of those ideas gains consensus. But we've
managed to waste all hope of making any significant progress here for
an entire release cycle for lack of ability to agree on spelling. I
think that's unfair to Mark, who put a lot of work into this area and
got nothing out of it, and I think it sucks for users of PostgreSQL,
too.
Well ... one of those actually already exists and also happens to be in
the SQL spec. I don't necessarily agree that we should absolutely
require that the system always enforce that this relationship exist (I'd
like a superuser to be able to get rid of it and to be able to change it
too if they want) and that seems a bit saner than having the bootstrap
superuser be special in some way here as would seem to otherwise be
required. I also feel that it would be generally useful to have more
than one of these relationships, if the user wishes, and that's
something that ownership doesn't (directly) support today. Further,
that's supported and expected by the SQL spec too. Even if we invented
some concept of ownership of roles, it seems like we should make most of
the other changes discussed here to bring us closer to what the spec
says about CREATE ROLE, DROP ROLE, GRANT, REVOKE, etc. At that point
though, what's the point of having ownership?
(2) it seems exactly contradictory to your later point that
Agree. I also think that it would be a good idea to attribute grants
performed by any superuser to the bootstrap superuser, or leave them
unattributed somehow. Because otherwise dropping superusers becomes a
pain in the tail for no good reason.Either there's a persistent relationship or there's not. I don't
think it's sensible to treat superusers differently here.I think that this argument about the difficulty of dropping superusers
may in fact be the motivation for the existing behavior that object-
permissions GRANTs done by superusers are attributed to the object
owner; something you were unhappy about upthread.In the end these requirements seem mutually contradictory. Either
we can have a persistent ownership relationship or not, but I don't
think we can have it apply in some cases and not others without
creating worse problems than we solve. I'm inclined to toss overboard
the requirement that superusers need to be an easy thing to drop.
Why is that important, anyway?Well, I think you're looking at it the wrong way. Compared to getting
useful functionality, the relative ease of dropping users is
completely unimportant. I'm happy to surrender it in exchange for
something else. I just don't see why we should give it up for nothing.
If Alice creates non-superusers Bob and Charlie, and Charlie creates
Doug, we need the persistent relationship to know that Charlie is
allowed to drop Doug and Bob is not. But if Charlie is a superuser
anyway, then the persistent relationship is of no use. I don't see the
point of cluttering up the system with such dependencies. Will I do it
that way, if that's what it takes to get the patch accepted? Sure. But
I can't imagine any end-user actually liking it.
We need to know that Charlie is allowed to drop Doug and Bob isn't but
that doesn't make it absolutely required that this be tracked
permanently or that Alice can't decide later to make it such that Doug
can't be dropped by Charlie for whatever reason she has. Also, I don't
think it would be such an issue to have a CASCADE for DROP ROLE which
would handle this case if we want it (and pg_auth_members is shared, so
there isn't an issue with multi-database concerns). We could also call
it something else if people feel CASCADE would be confusing since it
wouldn't cascade to owned objects. Or we could consider extending GRANT
to make this situation something that could be handled more easily.
Thanks,
Stephen
On Fri, Mar 11, 2022 at 6:55 AM Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Mar 10, 2022 at 5:14 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
This seems reasonable in isolation, but
(1) it implies a persistent relationship between creating and created
roles. Whether you want to call that ownership or not, it sure walks
and quacks like ownership.
I like my TENANT idea best, but I'm perfectly willing to call
it ownership as you seem to prefer or WITH ADMIN OPTION as Stephen
seems to prefer if one of those ideas gains consensus.
If WITH ADMIN OPTION is sufficient to meet our immediate goals I do not see
the benefit of adding an ownership concept where there is not one today.
If added, I'd much rather have it be ownership as to fit in with the rest
of the existing system rather than introduce an entirely new term.
If Alice creates non-superusers Bob and Charlie, and Charlie creates
Doug, we need the persistent relationship to know that Charlie is
allowed to drop Doug and Bob is not
The interesting question seems to be whether Alice can drop Doug, not
whether Bob can.
It's more important
at this point to get agreement on the principles.
What are the principles you want to get agreement on and how do they differ
from what we have in place today? What are the proposed changes you would
make to enforce the new principles. Which principles are now obsolete and
what do you want to do about the features that were built to enforce them
(including backward compatibility concerns)?
David J.
Greetings,
* David G. Johnston (david.g.johnston@gmail.com) wrote:
On Thu, Mar 10, 2022 at 3:01 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Thu, Mar 10, 2022 at 4:00 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:I dislike changing the documented behavior of CREATEROLE to the degree
suggested here. However, there are three choices here, only one of which
can be chosen:1. Leave CREATEROLE alone entirely
2. Make it so CREATEROLE cannot assign membership to the predefinedroles or superuser (inheritance included), but leave the rest alone. This
would be the hard-coded version, not the role attribute one.3. Make it so CREATEROLE can only assign membership to roles for which
it has been made an admin; as well as the other things mentioned
Moving forward I'd prefer options 1 or 2, leaving the ability to
create/alter/drop a role to be vested via predefined roles.
It sounds like you prefer a behavior where CREATEROLE gives power over
all non-superusers, but that seems pretty limiting to me.Doh! I edited out the part where I made clear I considered options 1 and 2
as basically being done for a limited period of time while deprecating the
CREATEROLE attribute altogether in favor of the fine-grained and predefined
role based permission granting. I don't want to nerf CREATEROLE as part of
adding this new feature, instead leave it as close to status quo as
reasonable so as not to mess up existing setups that make use of it. We
can note in the release notes and documentation that we consider CREATEROLE
to be deprecated and that the new predefined role should be used to give a
user the ability to create/alter/drop roles, etc... DBAs should consider
revoking CREATEROLE from their users and granting them proper memberships
in the predefined roles and the groups those roles should be administering.
I disagree entirely with the idea that we should push the out however
many years it'd take to get through some deprecation period. We are
absolutely terrible when it comes to that and what we're talking about
here, at this point anyway, is making changes that get us closer to what
the spec says. I agree that we can't back-patch these changes, but I
don't think we need a deprecation period. If we were just getting rid
of CREATEROLE, I don't think we'd have a deprecation period. If we need
to get rid of CREATEROLE and introduce something new that more-or-less
means the same thing, to make it so that people's scripts break in a
more obvious way, maybe we can consider that, but I don't really think
that's actually the case here. Such scripts as will break will still
break in a pretty clear way with a clear answer as to how to fix them
and I don't think there's some kind of data corruption or something that
would happen.
Thanks,
Stephen
On Fri, Mar 11, 2022 at 8:32 AM Stephen Frost <sfrost@snowman.net> wrote:
Such scripts as will break will still
break in a pretty clear way with a clear answer as to how to fix them
and I don't think there's some kind of data corruption or something that
would happen.
I largely agree and am perfectly fine with going with the majority on this
point. My vote would just fall on the conservative side. But as so far no
one else seems to be overly concerned, nerfing CREATEROLE seems to be the
path forward.
David J.
On Fri, Mar 11, 2022 at 10:27 AM Stephen Frost <sfrost@snowman.net> wrote:
I agree that there would be a recorded relationship (that is, one that
we write into the catalog and keep around until and unless it's removed
by an admin) between creating and created roles and that's probably the
default when CREATE ROLE is run but, unlike tables or such objects in
the system, I don't agree that we should require this to exist at
absolutely all times for every role (what would it be for the bootstrap
superuser..?). At least today, that's distinct from how ownership in
the system works. I also don't believe that this is necessarily an
issue for Robert's use-case, as long as there are appropriate
restrictions around who is allowed to remove or modify these
relationships.
I agree.
I agree. [ but we need to get consensus ]
Well ... [ how about we do it my way? ]
Repeating the same argument over again isn't necessarily going to help
anything here. I read your argument and I can believe there could be a
solution along those lines, although you haven't addressed my concern
about NOINHERIT. Tom is apparently less convinced, and you know, I
think that's OK. Not everybody has to agree with the way you want to
do it.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
On Fri, Mar 11, 2022 at 10:27 AM Stephen Frost <sfrost@snowman.net> wrote:
I agree that there would be a recorded relationship (that is, one that
we write into the catalog and keep around until and unless it's removed
by an admin) between creating and created roles and that's probably the
default when CREATE ROLE is run but, unlike tables or such objects in
the system, I don't agree that we should require this to exist at
absolutely all times for every role (what would it be for the bootstrap
superuser..?). At least today, that's distinct from how ownership in
the system works. I also don't believe that this is necessarily an
issue for Robert's use-case, as long as there are appropriate
restrictions around who is allowed to remove or modify these
relationships.
I agree.
The bootstrap superuser clearly must be a special case in some way.
I'm not convinced that that means there should be other special
cases. Maybe there is a use-case for other "unowned" roles, but in
exactly what way would that be different from deeming such roles
to be owned by the bootstrap superuser?
regards, tom lane
On Fri, Mar 11, 2022 at 10:37 AM David G. Johnston
<david.g.johnston@gmail.com> wrote:
I largely agree and am perfectly fine with going with the majority on this point. My vote would just fall on the conservative side. But as so far no one else seems to be overly concerned, nerfing CREATEROLE seems to be the path forward.
This kind of thing is always a judgement call. If we were talking
about breaking 'SELECT * from table', I'm sure it would be hard to
convince anybody to agree to do that at all, let alone with no
deprecation period. Fortunately, CREATEROLE is less used, so breaking
it will inconvenience fewer people. Moreover, unlike 'SELECT * FROM
table', CREATEROLE is kinda broken, and it's less scary to make
changes to behavior that sucks in the first place than it is to make
changes to the behavior of things that are working well. For all of
that, there's no hard-and-fast rule that we couldn't keep the existing
behavior around, introduce a substitute, and eventually drop the old
thing. I'm just not clear that it's really worth it in this case. It'd
certainly be interesting to hear from anyone who is finding some
utility in the current system. It looks pretty crap to me, but it's
easy to bring too much of one's own bias to such judgements.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mar 11, 2022, at 7:58 AM, Robert Haas <robertmhaas@gmail.com> wrote:
This kind of thing is always a judgement call. If we were talking
about breaking 'SELECT * from table', I'm sure it would be hard to
convince anybody to agree to do that at all, let alone with no
deprecation period. Fortunately, CREATEROLE is less used, so breaking
it will inconvenience fewer people.
This issue of how much backwards compatibility breakage we're willing to tolerate is just as important as questions about how we would want roles to work in a green-field development project. The sense I got a year ago, on this list, was that changing CREATEROLE was acceptable, but changing other parts of the system, such as how ADMIN OPTION works, would go too far.
Role ownership did not yet exist, and that was a big motivation in introducing that concept, because you couldn't credibly say it broke other existing features. It introduces the new notion that when a superuser creates a role, the superuser owns it, which is identical to how things implicitly work today; and when a CREATEROLE non-superuser creates a role, that role owns the new role, which is different from how it works today, arguably breaking CREATEROLE's prior behavior. *But it doesn't break anything else*.
If we're going to change how ADMIN OPTION works, or how role membership works, or how inherit/noinherit works, let's first be clear that we are willing to accept whatever backwards incompatibility that entails. This is not a green-field development project. The constant spinning around with regard to how much compatibility we need to preserve is giving me vertigo.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Fri, Mar 11, 2022 at 10:46 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
The bootstrap superuser clearly must be a special case in some way.
I'm not convinced that that means there should be other special
cases. Maybe there is a use-case for other "unowned" roles, but in
exactly what way would that be different from deeming such roles
to be owned by the bootstrap superuser?
I think that just boils down to how many useless catalog entries you
want to make.
If we implement the link between the creating role and the created
role as role ownership, then we are surely just going to add a
rolowner column to pg_authid, and when the role is owned by nobody, I
think we should always just store a valid OID in it, rather than
sometimes storing 0. It just seems simpler. Any time we would store 0,
store the bootstrap superuser's pg_authid.oid value instead. That way
the OID is always valid, which probably lets us get by with fewer
special cases in the code.
If we implement the link between the creating role and the created
role as an automatically-granted WITH ADMIN OPTION, then we could
choose to put (CREATOR_OID, CREATED_OID, whatever, TRUE) into
pg_auth_members for the creating superuser or, indeed, every superuser
in the system. Or we can leave it out. The result will be exactly the
same. Here, I would favor leaving it out, because extra catalog
entries that don't do anything are usually a thing that we do not
want. See a49d081235997c67e8aab7a523b17e8d1cb93184, for example.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
If we implement the link between the creating role and the created
role as role ownership, then we are surely just going to add a
rolowner column to pg_authid, and when the role is owned by nobody, I
think we should always just store a valid OID in it, rather than
sometimes storing 0. It just seems simpler. Any time we would store 0,
store the bootstrap superuser's pg_authid.oid value instead. That way
the OID is always valid, which probably lets us get by with fewer
special cases in the code.
+1.
Note that either case would also involve making entries in pg_shdepend;
although for the case of roles owned by/granted to the bootstrap
superuser, we could omit those on the usual grounds that we don't need
to record dependencies on pinned objects.
regards, tom lane
On Fri, Mar 11, 2022 at 11:12 AM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:
This issue of how much backwards compatibility breakage we're willing to tolerate is just as important as questions about how we would want roles to work in a green-field development project. The sense I got a year ago, on this list, was that changing CREATEROLE was acceptable, but changing other parts of the system, such as how ADMIN OPTION works, would go too far.
Role ownership did not yet exist, and that was a big motivation in introducing that concept, because you couldn't credibly say it broke other existing features. It introduces the new notion that when a superuser creates a role, the superuser owns it, which is identical to how things implicitly work today; and when a CREATEROLE non-superuser creates a role, that role owns the new role, which is different from how it works today, arguably breaking CREATEROLE's prior behavior. *But it doesn't break anything else*.
If we're going to change how ADMIN OPTION works, or how role membership works, or how inherit/noinherit works, let's first be clear that we are willing to accept whatever backwards incompatibility that entails. This is not a green-field development project. The constant spinning around with regard to how much compatibility we need to preserve is giving me vertigo.
I mean, I agree that the backward compatibility ramifications of every
idea need to be considered, but I agree even more that the amount of
spinning around here is pretty insane. My feeling is that neither role
owners nor tenants introduce any real concerns about
backward-compatibility or, for that matter, SQL standards compliance,
nonwithstanding Stephen's argument to the contrary. Every vendor
extends the standard with their own stuff, and we've done that as
well, as we can do it in more places.
On the other hand, changing ADMIN OPTION does have compatibility and
spec-compliance ramifications. I think Stephen is arguing that we can
solve this problem while coming closer to the spec, and I think we
usually consider getting closer to the spec to be a sufficient reason
for breaking backward compatibility (cf. standard_conforming_strings).
But I don't know whether he is correct when he argues that the spec
makes admin option on a role sufficient to drop the role. I've never
had any luck understanding what the SQL specification is saying about
any topic.
--
Robert Haas
EDB: http://www.enterprisedb.com
Greetings,
* Tom Lane (tgl@sss.pgh.pa.us) wrote:
Robert Haas <robertmhaas@gmail.com> writes:
If we implement the link between the creating role and the created
role as role ownership, then we are surely just going to add a
rolowner column to pg_authid, and when the role is owned by nobody, I
think we should always just store a valid OID in it, rather than
sometimes storing 0. It just seems simpler. Any time we would store 0,
store the bootstrap superuser's pg_authid.oid value instead. That way
the OID is always valid, which probably lets us get by with fewer
special cases in the code.
We haven't got any particularly special cases in the code today for what
happens if we run up the role hierarchy to a point that it ends and so
I'm not sure why adding in a whole new concept around role ownership,
which doesn't exist in the spec, would somehow leave us with fewer such
special cases.
+1.
Note that either case would also involve making entries in pg_shdepend;
although for the case of roles owned by/granted to the bootstrap
superuser, we could omit those on the usual grounds that we don't need
to record dependencies on pinned objects.
That we aren't discussing the issues with the current GRANT ... WITH
ADMIN OPTION and how we deviate from what the spec calls for when it
comes to DROP ROLE, which seems to be the largest thing that's
'solved' with this ownership concept, is concerning to me.
If we go down the route of adding role ownership, are we going to
document that we explicitly go against the SQL standard when it comes to
how DROP ROLE works? Or are we going to fix DROP ROLE? I'd much prefer
the latter, but doing so then largely negates the point of this role
ownership concept. I don't see how it makes sense to do both.
Thanks,
Stephen
Greetings,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Fri, Mar 11, 2022 at 11:12 AM Mark Dilger
<mark.dilger@enterprisedb.com> wrote:This issue of how much backwards compatibility breakage we're willing to tolerate is just as important as questions about how we would want roles to work in a green-field development project. The sense I got a year ago, on this list, was that changing CREATEROLE was acceptable, but changing other parts of the system, such as how ADMIN OPTION works, would go too far.
That we deviate as far as we do when it comes to the SQL spec is
something that I don't feel like I had a good handle on when discussing
this previously (that the spec doesn't talk about 'admin option' really
but rather 'grantable authorization identifiers' or whatever it is
doesn't help... but still, that's on me, sorry about that).
Role ownership did not yet exist, and that was a big motivation in introducing that concept, because you couldn't credibly say it broke other existing features. It introduces the new notion that when a superuser creates a role, the superuser owns it, which is identical to how things implicitly work today; and when a CREATEROLE non-superuser creates a role, that role owns the new role, which is different from how it works today, arguably breaking CREATEROLE's prior behavior. *But it doesn't break anything else*.
If we're going to change how ADMIN OPTION works, or how role membership works, or how inherit/noinherit works, let's first be clear that we are willing to accept whatever backwards incompatibility that entails. This is not a green-field development project. The constant spinning around with regard to how much compatibility we need to preserve is giving me vertigo.
I agree that it would have an impact on backwards compatibility to
change how WITH ADMIN works- but it would also get us more in line with
what the SQL standard says for how WITH ADMIN is supposed to work and
that seems worth the change to me.
On the other hand, changing ADMIN OPTION does have compatibility and
spec-compliance ramifications. I think Stephen is arguing that we can
solve this problem while coming closer to the spec, and I think we
usually consider getting closer to the spec to be a sufficient reason
for breaking backward compatibility (cf. standard_conforming_strings).
Indeed.
But I don't know whether he is correct when he argues that the spec
makes admin option on a role sufficient to drop the role. I've never
had any luck understanding what the SQL specification is saying about
any topic.
I'm happy to point you to what the spec says and to discuss it further
if that would be helpful, or to get other folks to comment on it. I
agree that it's definitely hard to grok at times. In this particular
case what I'm looking at is, under DROP ROLE / Access Rules, there's
only one sentence:
There shall exist at least one grantable role authorization descriptor
whose role name is R and whose grantee is an enabled authorization
identifier.
A bit of decoding: 'grantable role authorization descriptor' is a GRANT
of a role WITH ADMIN OPTION. The role name 'R' is the role specified.
The 'grantee' is who that role R was GRANT'd to, and 'enabled
authorization identifier' is basically "has_privs_of_role()" (note that
you can in the spec hvae roles that you're a member of but which are
*not* currently enabled).
Hopefully that helps.
Thanks,
Stephen
On Fri, Mar 11, 2022 at 11:34 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Note that either case would also involve making entries in pg_shdepend;
although for the case of roles owned by/granted to the bootstrap
superuser, we could omit those on the usual grounds that we don't need
to record dependencies on pinned objects.
That makes sense to me, but it still doesn't solve the problem of
agreeing on role ownership vs. WITH ADMIN OPTION vs. something else.
I find it ironic (and frustrating) that Mark implemented what I think
is basically what you're arguing for, it got stuck because Stephen
didn't like it, we then said OK so let's try to find out what Stephen
would like, only to have you show up and say that it's right the way
he already had it. I'm not saying that you're wrong, or for that
matter that he's wrong. I'm just saying that if both of you are
absolutely bent on having it the way you want it, either one of you is
going to be sad, or we're not going to make any progress.
Never mind the fact that neither of you seem interested in even giving
a hearing to my preferred way of doing it. :-(
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mar 11, 2022, at 8:48 AM, Stephen Frost <sfrost@snowman.net> wrote:
I agree that it would have an impact on backwards compatibility to
change how WITH ADMIN works- but it would also get us more in line with
what the SQL standard says for how WITH ADMIN is supposed to work and
that seems worth the change to me.
I'm fine with giving up some backwards compatibility to get some SQL standard compatibility, as long as we're clear that is what we're doing. What you say about the SQL spec isn't great, though, because too much power is vested in "ADMIN". I see "ADMIN" as at least three separate privileges together. Maybe it would be spec compliant to implement "ADMIN" as a synonym for a set of separate privileges?
On Mar 11, 2022, at 8:41 AM, Stephen Frost <sfrost@snowman.net> wrote:
That we aren't discussing the issues with the current GRANT ... WITH
ADMIN OPTION and how we deviate from what the spec calls for when it
comes to DROP ROLE, which seems to be the largest thing that's
'solved' with this ownership concept, is concerning to me.
Sure, let's discuss that a bit more. Here is my best interpretation of your post about the spec, when applied to postgres with an eye towards not doing any more damage than necessary:
On Mar 10, 2022, at 11:58 AM, Stephen Frost <sfrost@snowman.net> wrote:
let's look at what the spec says:
CREATE ROLE
- Who is allowed to run CREATE ROLE is implementation-defined
This should be anyone with membership in pg_create_role.
- After creation, this is effictively run:
GRANT new_role TO creator_role WITH ADMIN, GRANTOR "_SYSTEM"
This should internally be implemented as three separate privileges, one which means you can grant the role, another which means you can drop the role, and a third that means you're a member of the role. That way, they can be independently granted and revoked. We could make "WITH ADMIN" a short-hand for "WITH G, D, M" where G, D, and M are whatever we name the independent privileges Grant, Drop, and Member-of.
Splitting G and D helps with backwards compatibility, because it gives people who want the traditional postgres "admin" a way to get there, by granting "G+M". Splitting M from G and D makes it simpler to implement the "bot" idea, since the bot shouldn't have M. But it does raise a question about always granting G+D+M to the creator, since the bot is the creator and we don't want the bot to have M. This isn't a problem I've invented from thin air, mind you, as G+D+M is just the definition of ADMIN per the SQL spec, if I've understood you correctly. So we need to think a bit more about the pg_create_role built-in role and whether that needs to be further refined to distinguish those who can get membership in roles they create vs. those who cannot. This line of reasoning takes me in the direction of what I think you were calling #5 upthread, but you'd have to elaborate on that, and how it interacts with the spec, for us to have a useful conversation about it.
DROP ROLE
- Any user who has been GRANT'd a role with ADMIN option is able to
DROP that role.
Change this to "Any role who has D on the role". That's spec compliant, because anyone granted ADMIN necessarily has D.
GRANT ROLE
- No cycles allowed
- A role must have ADMIN rights on the role to be able to GRANT it to
another role.
Change this to "Any role who has G on the role". That's spec compliant, because anyone grant ADMIN necessarily has G.
We should also fix the CREATE ROLE command to require the grantor have G on a role in order to give it to the new role as part of the command. Changing the CREATEROLE, CREATEDB, REPLICATION, and BYPASSRLS attributes into pg_create_role, pg_create_db, pg_replication, and pg_bypassrls, the creator could only give them to the created role if the creator has G on the roles. If we do this, we could keep the historical privilege bits and their syntax support for backward compatibility, or we could rip them out, but the decision between those two options is independent of the rest of the design.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Greetings,
On Fri, Mar 11, 2022 at 12:32 Mark Dilger <mark.dilger@enterprisedb.com>
wrote:
On Mar 11, 2022, at 8:48 AM, Stephen Frost <sfrost@snowman.net> wrote:
I agree that it would have an impact on backwards compatibility to
change how WITH ADMIN works- but it would also get us more in line with
what the SQL standard says for how WITH ADMIN is supposed to work and
that seems worth the change to me.I'm fine with giving up some backwards compatibility to get some SQL
standard compatibility, as long as we're clear that is what we're doing.
What you say about the SQL spec isn't great, though, because too much power
is vested in "ADMIN". I see "ADMIN" as at least three separate privileges
together. Maybe it would be spec compliant to implement "ADMIN" as a
synonym for a set of separate privileges?
I do think that’s reasonable … and believe I suggested it about 3 messages
ago in this thread. ;) (step #3 I think it was? Or maybe 4).
On Mar 11, 2022, at 8:41 AM, Stephen Frost <sfrost@snowman.net> wrote:
That we aren't discussing the issues with the current GRANT ... WITH
ADMIN OPTION and how we deviate from what the spec calls for when it
comes to DROP ROLE, which seems to be the largest thing that's
'solved' with this ownership concept, is concerning to me.Sure, let's discuss that a bit more. Here is my best interpretation of
your post about the spec, when applied to postgres with an eye towards not
doing any more damage than necessary:On Mar 10, 2022, at 11:58 AM, Stephen Frost <sfrost@snowman.net> wrote:
let's look at what the spec says:
CREATE ROLE
- Who is allowed to run CREATE ROLE is implementation-definedThis should be anyone with membership in pg_create_role.
That could be the case if we wished to go that route. I’d think in such
case we’d then also remove CREATEROLE as otherwise the documentation feels
like it’d be quite confusing.
- After creation, this is effictively run:
GRANT new_role TO creator_role WITH ADMIN, GRANTOR "_SYSTEM"
This should internally be implemented as three separate privileges, one
which means you can grant the role, another which means you can drop the
role, and a third that means you're a member of the role. That way, they
can be independently granted and revoked. We could make "WITH ADMIN" a
short-hand for "WITH G, D, M" where G, D, and M are whatever we name the
independent privileges Grant, Drop, and Member-of.
I mean, sure, we can get there, and possibly add more like if you’re
allowed to change or reset that role’s password and other things, but I
don’t see that this piece is required as part of the very first change in
this area. Further, WITH ADMIN already gives grant and member today, so
you’re saying the only thing this change does that makes “WITH ADMIN” too
powerful is adding DROP to it, yet that’s explicitly what the spec calls
for. In short, I disagree that moving the DROP ROLE right from CREATEROLE
roles having that across the entire system (excluding superusers) to WITH
ADMIN where the role who has that right can:
A) already become that role and drop all their objects
B) already GRANT that role to some other role
is a big issue.
Splitting G and D helps with backwards compatibility, because it gives
people who want the traditional postgres "admin" a way to get there, by
granting "G+M". Splitting M from G and D makes it simpler to implement the
"bot" idea, since the bot shouldn't have M. But it does raise a question
about always granting G+D+M to the creator, since the bot is the creator
and we don't want the bot to have M. This isn't a problem I've invented
from thin air, mind you, as G+D+M is just the definition of ADMIN per the
SQL spec, if I've understood you correctly. So we need to think a bit more
about the pg_create_role built-in role and whether that needs to be further
refined to distinguish those who can get membership in roles they create
vs. those who cannot. This line of reasoning takes me in the direction of
what I think you were calling #5 upthread, but you'd have to elaborate on
that, and how it interacts with the spec, for us to have a useful
conversation about it.
All that said, as I said before, I’m in favor of splitting things up and so
if you want to do that as part of this initial work, sure. Idk that it’s
absolutely required as part of this but I’m not going to complain if it’s
included either. I agree that would allow folks to get something similar
to what they could get today if they want.
I agree that the split up helps with the “bot” idea, as we could at least
then create a security definer function that the bot runs and which creates
roles that the bot then has G for but not M or D. Even better would be to
also provide a way for the “bot” to be able to create roles without the
need for a security definer function where it doesn’t automatically get all
three, and that was indeed what I was thinking about with the template
idea. The general thought there being that an admin could define a template
along the lines of:
CREATE TEMPLATE employee_template
CREATOR WITH ADMIN, NOMEMBERSHIP
ROLE IN employee;
And then provide a way for the bot to be given the right to use this
template. Thinking on it a bit further, I’m guessing that we wouldn’t
actually give the bot pg_create_role in this case and instead would leave
that to mean “able to create arbitrary roles and have all privs in that”
similar to what we are talking about where ADMIN implies the full set of
rights.
DROP ROLE
- Any user who has been GRANT'd a role with ADMIN option is able to
DROP that role.Change this to "Any role who has D on the role". That's spec compliant,
because anyone granted ADMIN necessarily has D.
Yeah.
GRANT ROLE
- No cycles allowed
- A role must have ADMIN rights on the role to be able to GRANT it to
another role.Change this to "Any role who has G on the role". That's spec compliant,
because anyone grant ADMIN necessarily has G.
Sure.
We should also fix the CREATE ROLE command to require the grantor have G on
a role in order to give it to the new role as part of the command.
… or just get rid of it, which seems saner to me.
Changing the CREATEROLE, CREATEDB, REPLICATION, and BYPASSRLS attributes
into pg_create_role, pg_create_db, pg_replication, and pg_bypassrls, the
creator could only give them to the created role if the creator has G on
the roles. If we do this, we could keep the historical privilege bits and
their syntax support for backward compatibility, or we could rip them out,
but the decision between those two options is independent of the rest of
the design.
Yeah, turning those into predefined roles which an admin can then decide to
give out (and to allow ADMIN on them to be given to folks who could then
pass that along if they wanted) is another thought I’ve had though one
that’s somewhat independent of the rest of this, but also shows how we
could make those be things that a superuser could choose to give out, or
not, to some set of roles who would then be able to create roles of their
own with those privileges.
On the whole, using predefined roles as the source of certain capabilities,
and the options discussed here which would allow an admin to grant those
capabilities out with or without the ability to grant them further, plus
the splitting out of the individual role-relationship rights (membership,
grantable, drop, etc) strikes me as being quite flexible and extendable and
generally in the direction that we’ve been trending and which seems to be
reasonably successful so far.
Thanks,
Stephen
Show quoted text
On Mar 11, 2022, at 2:46 PM, Stephen Frost <sfrost@snowman.net> wrote:
I do think that’s reasonable … and believe I suggested it about 3 messages ago in this thread. ;) (step #3 I think it was? Or maybe 4).
Yes, and you mentioned it to me off-list.
I'm soliciting a more concrete specification for what you are proposing. To me, that means understanding how the SQL spec behavior that you champion translates into specific changes. You specified some of this in steps #1 through #5, but I'd like a clearer indication of how many of those (#1 alone, both #1 and #2, or what?) constitute a competing idea to the idea of role ownership, and greater detail about how each of those steps translate into specific behavior changes in postgres. Your initial five-step email seems to be claiming that #1 by itself is competitive, but to me it seems at least #1 and #2 would be required.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Greetings,
On Fri, Mar 11, 2022 at 19:03 Mark Dilger <mark.dilger@enterprisedb.com>
wrote:
On Mar 11, 2022, at 2:46 PM, Stephen Frost <sfrost@snowman.net> wrote:
I do think that’s reasonable … and believe I suggested it about 3
messages ago in this thread. ;) (step #3 I think it was? Or maybe 4).
Yes, and you mentioned it to me off-list.
Indeed.
I'm soliciting a more concrete specification for what you are proposing.
To me, that means understanding how the SQL spec behavior that you champion
translates into specific changes. You specified some of this in steps #1
through #5, but I'd like a clearer indication of how many of those (#1
alone, both #1 and #2, or what?) constitute a competing idea to the idea of
role ownership, and greater detail about how each of those steps translate
into specific behavior changes in postgres. Your initial five-step email
seems to be claiming that #1 by itself is competitive, but to me it seems
at least #1 and #2 would be required.
First … I outlined a fair bit of further description in the message you
just responded to but neglected to include in your response, which strikes
me as odd that you’re now asking for further explanation. When it comes to
completing the idea of role ownership- I didn’t come up with that idea nor
champion it and therefore I’m not really sure how many of the steps are
required to fully support that concept..? For my part, I would think that
those steps necessary to satisfy the spec would get us pretty darn close to
what true folks advocating for role ownership are asking for, but that
doesn’t include the superuser-only alter role attributes piece. Is that
included in role ownership? I wouldn’t think so, but some might argue
otherwise, and I don’t know that it is actually useful to divert into a
discussion about what is or isn’t in that.
If we agree that the role attribute bits are independent then I think I
agree that we need 1 and 2 to get the capabilities that the folks asking
for role ownership want, as 2 is where we make sure that one admin of a
role can’t revoke another admin’s rights over that role. Perhaps 2 isn’t
strictly necessary in a carefully managed environment where no one else is
given admin rights over the mini-superuser roles, but I’d rather not have
folks depending on that. I’d still push back though and ask those
advocating for this if it meets what they’re asking for. I got the
impression that it did but maybe I misunderstood.
In terms of exactly how things would work with these changes… I thought I
explained it pretty clearly, so it’s kind of hard to answer that further
without a specific question to answer. Did you have something specific in
mind? Perhaps I could answer a specific question and provide more clarity
that way.
Thanks,
Stephen
Show quoted text
On Mar 11, 2022, at 4:56 PM, Stephen Frost <sfrost@snowman.net> wrote:
First … I outlined a fair bit of further description in the message you just responded to but neglected to include in your response, which strikes me as odd that you’re now asking for further explanation.
When it comes to completing the idea of role ownership- I didn’t come up with that idea nor champion it
Sorry, not "completing", but "competing". It seems we're discussing different ways to fix how roles and CREATEROLE work, and we have several ideas competing against each other. (This differs from *people* competing against each other, as I don't necessarily like the patch I wrote better than I like your idea.)
and therefore I’m not really sure how many of the steps are required to fully support that concept..?
There are problems that the ownership concepts solve. I strongly suspect that your proposal could also solve those same problems, and just trying to identify the specific portions of your proposal necessary to do so.
For my part, I would think that those steps necessary to satisfy the spec would get us pretty darn close to what true folks advocating for role ownership are asking for
I have little idea what "true folks" means in this context. As for "advocating for role ownership", I'm not in that group. Whether role ownership or something else, I just want some solution to a set of problems, mostly to do with needing superuser to do role management tasks.
, but that doesn’t include the superuser-only alter role attributes piece. Is that included in role ownership? I wouldn’t think so, but some might argue otherwise, and I don’t know that it is actually useful to divert into a discussion about what is or isn’t in that.
Introducing the idea of role ownership doesn't fix that. But a patch which introduces role ownership is useless unless CREATEROLE is also fixed. There isn't any point having non-superusers create and own roles if, to do so, they need a privilege which can break into superuser. But that argument is no different with a patch along the lines of what you are proposing. CREATEROLE needs fixing either way.
If we agree that the role attribute bits are independent
Yes.
then I think I agree that we need 1 and 2 to get the capabilities that the folks asking for role ownership want
Yes.
as 2 is where we make sure that one admin of a role can’t revoke another admin’s rights over that role.
Exactly, so #2 is part of the competing proposal. (I get the sense you might not see these as competing proposals, but I find that framing useful for deciding which approach to pursue.)
Perhaps 2 isn’t strictly necessary in a carefully managed environment where no one else is given admin rights over the mini-superuser roles, but I’d rather not have folks depending on that.
I think it is necessary, and for the reason you say.
I’d still push back though and ask those advocating for this if it meets what they’re asking for. I got the impression that it did but maybe I misunderstood.
In terms of exactly how things would work with these changes… I thought I explained it pretty clearly, so it’s kind of hard to answer that further without a specific question to answer. Did you have something specific in mind? Perhaps I could answer a specific question and provide more clarity that way.
Your emails contained a lot of "we could do this or that depending on what people want, and maybe this other thing, but that isn't really necessary, and ...." which left me unclear on the proposal. I don't mean to disparage your communication style; it's just that when trying to distill technical details, high level conversation can be hard to grok.
I have the sense that you aren't going to submit a patch, so I wanted this thread to contain enough detail for somebody else to do so. Thanks.
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
Greetings,
* Mark Dilger (mark.dilger@enterprisedb.com) wrote:
On Mar 11, 2022, at 4:56 PM, Stephen Frost <sfrost@snowman.net> wrote:
First … I outlined a fair bit of further description in the message you just responded to but neglected to include in your response, which strikes me as odd that you’re now asking for further explanation.When it comes to completing the idea of role ownership- I didn’t come up with that idea nor champion it
Sorry, not "completing", but "competing". It seems we're discussing different ways to fix how roles and CREATEROLE work, and we have several ideas competing against each other. (This differs from *people* competing against each other, as I don't necessarily like the patch I wrote better than I like your idea.)
and therefore I’m not really sure how many of the steps are required to fully support that concept..?
There are problems that the ownership concepts solve. I strongly suspect that your proposal could also solve those same problems, and just trying to identify the specific portions of your proposal necessary to do so.
I'm happy to help try to identify those, but it seems we'd need to have
the exact problems that ownership solves defined first. Robert defined
what he's looking for as:
Robert Haas <robertmhaas@gmail.com> wrote:
Probably easier to just say it again: I want to have users that can
create roles and then have superuser-like powers with respect to those
roles. They can freely exercise the privileges of those roles, and
they can do all the things that a superuser can do but only with
respect to those roles. They cannot break out to the OS. I think it's
pretty similar to what you are describing, with a couple of possible
exceptions. For example, would you imagine that being an admin of a
login role would let you change that user's password? Because that
would be desirable behavior from where I sit.
Which sure sounds like it's just about covered in step #1 of what I
outlined before, except that the above description implies that one
can't "get away" from the user who created their role, in which case we
do need step #2 also.
For my part, I would think that those steps necessary to satisfy the spec would get us pretty darn close to what true folks advocating for role ownership are asking for
I have little idea what "true folks" means in this context. As for "advocating for role ownership", I'm not in that group. Whether role ownership or something else, I just want some solution to a set of problems, mostly to do with needing superuser to do role management tasks.
... I'm not entirely sure what I meant there either, my hunch is that
'true' was actually just a leftover word from some other framing of that
sentence and I had meant to remove it. Apologies for that. What I was
trying to get at there is that steps #1 & #2 are the ones that I view as
getting us closer to spec compliance and that doing so would get us to
where Robert's ask above would be answered.
, but that doesn’t include the superuser-only alter role attributes piece. Is that included in role ownership? I wouldn’t think so, but some might argue otherwise, and I don’t know that it is actually useful to divert into a discussion about what is or isn’t in that.
Introducing the idea of role ownership doesn't fix that. But a patch which introduces role ownership is useless unless CREATEROLE is also fixed. There isn't any point having non-superusers create and own roles if, to do so, they need a privilege which can break into superuser. But that argument is no different with a patch along the lines of what you are proposing. CREATEROLE needs fixing either way.
There's a few ways to have the 'CREATEROLE' role attribute be fixed-
- Remove it entirely (replacing with pg_create_role or such)
- Remove its ability to GRANT out rights that the role running it
doesn't have
- Make it superfluous (leave it as-is, but add in pg_create_role which
allows a role to create another role but doesn't include the magic
GRANT whatever-role TO whatever-role that CREATEROLE has)
I agree that we need to do something here to allow roles to create other
roles while not having or being able to trivially get superuser
themselves.
If we agree that the role attribute bits are independent
Yes.
Great.
then I think I agree that we need 1 and 2 to get the capabilities that the folks asking for role ownership want
Yes.
Ok.
as 2 is where we make sure that one admin of a role can’t revoke another admin’s rights over that role.
Exactly, so #2 is part of the competing proposal. (I get the sense you might not see these as competing proposals, but I find that framing useful for deciding which approach to pursue.)
... and is also part of getting us closer to the spec.
Perhaps 2 isn’t strictly necessary in a carefully managed environment where no one else is given admin rights over the mini-superuser roles, but I’d rather not have folks depending on that.
I think it is necessary, and for the reason you say.
Great.
I’d still push back though and ask those advocating for this if it meets what they’re asking for. I got the impression that it did but maybe I misunderstood.
In terms of exactly how things would work with these changes… I thought I explained it pretty clearly, so it’s kind of hard to answer that further without a specific question to answer. Did you have something specific in mind? Perhaps I could answer a specific question and provide more clarity that way.
Your emails contained a lot of "we could do this or that depending on what people want, and maybe this other thing, but that isn't really necessary, and ...." which left me unclear on the proposal. I don't mean to disparage your communication style; it's just that when trying to distill technical details, high level conversation can be hard to grok.
Feel free to quote me explicitly in such places that you're looking for
clarification and I'd be happy to drill down on those.
I have the sense that you aren't going to submit a patch, so I wanted this thread to contain enough detail for somebody else to do so. Thanks.
So ... do you feel like that's now the case? Or were you looking for
more?
Thanks,
Stephen
On Mar 14, 2022, at 7:38 AM, Stephen Frost <sfrost@snowman.net> wrote:
So ... do you feel like that's now the case? Or were you looking for
more?
I don't have any more questions at the moment. Thanks!
—
Mark Dilger
EnterpriseDB: http://www.enterprisedb.com
The Enterprise PostgreSQL Company
On Fri, Mar 11, 2022 at 11:51 AM Robert Haas <robertmhaas@gmail.com> wrote:
On Fri, Mar 11, 2022 at 11:34 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Note that either case would also involve making entries in pg_shdepend;
although for the case of roles owned by/granted to the bootstrap
superuser, we could omit those on the usual grounds that we don't need
to record dependencies on pinned objects.That makes sense to me, but it still doesn't solve the problem of
agreeing on role ownership vs. WITH ADMIN OPTION vs. something else.
Notwithstanding the lack of agreement on that point, I believe that
what we should do for v15 is remove the session user
self-administration exception. We have pretty much established that it
was originally introduced in error. It later was found to be a
security vulnerability, and that resulted in the exception being
narrowed without removing it altogether. While there are differences
of opinion on what the larger plan here ought to be, nobody's proposal
plan involves retaining that exception. Neither has anyone offered a
plausible use case for the current behavior, so there's no reason to
think that removing it would break anything.
However, it might. And if it does, I think it would be best if
removing that exception were the *only* change in this area made by
that release. If for v16 or v17 or v23 we implement Plan Tom or Plan
Stephen or Plan Robert or something else, and along the way we remove
that self-administration exception, we're going to have a real fire
drill if it turns out that the self-administration exception was
important for some reason we're not seeing right now. If, on the other
hand, we remove that exception in v15, then if anything breaks, it'll
be a lot easier to deal with. Worst case scenario we just revert the
removal of that exception, which will be a very localized change if
nothing else has been done that depends heavily on its having been
removed.
So I propose to commit something like what I posted here:
/messages/by-id/CA+TgmobgeK0JraOwQVPqhSXcfBdFitXSomoebHMMMhmJ4gLonw@mail.gmail.com
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
Notwithstanding the lack of agreement on that point, I believe that
what we should do for v15 is remove the session user
self-administration exception. We have pretty much established that it
was originally introduced in error.
Agreed.
However, it might. And if it does, I think it would be best if
removing that exception were the *only* change in this area made by
that release.
Good idea, especially since it's getting to be too late to consider
anything more invasive anyway.
So I propose to commit something like what I posted here:
/messages/by-id/CA+TgmobgeK0JraOwQVPqhSXcfBdFitXSomoebHMMMhmJ4gLonw@mail.gmail.com
+1, although the comments might need some more work. In particular,
I'm not sure that this bit is well stated:
+ * A role cannot have WITH ADMIN OPTION on itself, because that would
+ * imply a membership loop.
We already do consider a role to be a member of itself:
regression=# create role r;
CREATE ROLE
regression=# grant r to r;
ERROR: role "r" is a member of role "r"
regression=# grant r to r with admin option;
ERROR: role "r" is a member of role "r"
It might be better to just say "By policy, a role cannot have WITH ADMIN
OPTION on itself". But if you want to write a defense of that policy,
this isn't a very good one.
regards, tom lane
On Thu, Mar 24, 2022 at 1:10 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
However, it might. And if it does, I think it would be best if
removing that exception were the *only* change in this area made by
that release.Good idea, especially since it's getting to be too late to consider
anything more invasive anyway.
I'd say it's definitely too late at this point.
So I propose to commit something like what I posted here:
/messages/by-id/CA+TgmobgeK0JraOwQVPqhSXcfBdFitXSomoebHMMMhmJ4gLonw@mail.gmail.com+1, although the comments might need some more work. In particular,
I'm not sure that this bit is well stated:+ * A role cannot have WITH ADMIN OPTION on itself, because that would + * imply a membership loop.We already do consider a role to be a member of itself:
regression=# create role r;
CREATE ROLE
regression=# grant r to r;
ERROR: role "r" is a member of role "r"
regression=# grant r to r with admin option;
ERROR: role "r" is a member of role "r"It might be better to just say "By policy, a role cannot have WITH ADMIN
OPTION on itself". But if you want to write a defense of that policy,
this isn't a very good one.
That sentence is present in the current code, along with a bunch of
other sentences, which the patch renders irrelevant. So I just deleted
all of the other stuff and kept the sentence that is still relevant to
the revised code. I think your proposed replacement is an improvement,
but let's be careful not to get sucked into too much of a wordsmithing
exercise in a patch that's here to make a functional change.
--
Robert Haas
EDB: http://www.enterprisedb.com
Greetings,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Thu, Mar 24, 2022 at 1:10 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
However, it might. And if it does, I think it would be best if
removing that exception were the *only* change in this area made by
that release.Good idea, especially since it's getting to be too late to consider
anything more invasive anyway.I'd say it's definitely too late at this point.
Agreed.
So I propose to commit something like what I posted here:
/messages/by-id/CA+TgmobgeK0JraOwQVPqhSXcfBdFitXSomoebHMMMhmJ4gLonw@mail.gmail.com+1, although the comments might need some more work. In particular,
I'm not sure that this bit is well stated:
Also +1 on this.
Thanks,
Stephen
On Mon, Mar 28, 2022 at 10:51 AM Stephen Frost <sfrost@snowman.net> wrote:
So I propose to commit something like what I posted here:
/messages/by-id/CA+TgmobgeK0JraOwQVPqhSXcfBdFitXSomoebHMMMhmJ4gLonw@mail.gmail.com+1, although the comments might need some more work. In particular,
I'm not sure that this bit is well stated:Also +1 on this.
OK, done using Tom's proposed wording.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Mar 4, 2022 at 4:34 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
If we are not tracking the grantors of role authorizations,
then we are doing it wrong and we ought to fix that.
We are definitely doing it wrong. It's not that we aren't doing it at
all, but we are doing it incorrectly. If user foo executes "GRANT foo
TO bar GRANTED BY quux", pg_auth_members.grantor gets the OID of role
"quux". Without the "GRANTED BY" clause, it gets the OID of role
"foo". But no dependency is created; therefore, the OID of that column
can point to a role that no longer exists, or potentially to a role
that did exist at one point, was dropped, and was later replaced by
some other role that happens to get the same OID. pg_dump handles this
by dumping "GRANTED BY whoever" if pg_auth_members.grantor is a
still-extant role and omitting the clause if not. This would be a
security vulnerability if there were any logic in the backend that
actually did anything with pg_auth_members.grantor, because if the
original grantor is removed and replaced by another role with the same
OID, a dump/restore could change the notional grantor. Since there is
no such logic, I don't think it's insecure, but it's still really
lame.
So let's talk about how we could fix this. In a vacuum I'd say this is
just a feature that never got finished and we should rip the whole
thing out. That is, remove pg_auth_members.grantor entirely and at
most keep some do-nothing syntax around for backward compatibility.
However, what Tom is saying in the text quoted above is that we ought
to have something that actually works, which is more challenging.
Apparently, the desired behavior here is for this to work like grants
on non-role objects, where executing "GRANT SELECT ON TABLE s1 TO foo"
under two different user accounts bar and baz that both have
permissions to grant that privilege creates two independent grants
that can be independently revoked. To get there, we'll have to change
a good few things -- not only will we need a dependency to prevent a
grantor from being dropped without revoking the grant, but we need to
change the primary key of pg_auth_members from (roleid, member) to
(roleid, member, grantor). Then we'll also have to change the behavior
of the GRANT and REVOKE commands at the SQL level, and also the
behavior of pg_dump, which will need to dump and restore all grants.
I'm open to other proposals, but my thought is that it might be
simplest to try to clean this up in two steps. In step one, the only
goal would be to make pg_auth_members.grantor reliably sane. In other
words, we'd add a dependency on the grantor when a role is granted to
another role. You could still only have one grant of role A to role B,
but the notional grantor C would always be a user that actually
exists. I suspect it would be a really good idea to also patch pg_dump
to not ever dump the grantor when working from an older release,
because the information is not necessarily reliable and I fear that
propagating it forward could lead to broken stuff or maybe even
security hazards as noted above. Then, in step two, we change things
around to allow multiple grants of the same role to the same other
role, one per grantor. Now you've achieved parity between the behavior
we have for roles and the behavior we have for permissions on other
kinds of SQL objects.
There may be other improvements we want to make in this area -
previous discussions have suggested various ideas - but it seems to me
that making the behavior sane and consistent with other types of
objects would be a good start. That way, if we decide we do want to
change anything else, we will be starting from a firm foundation,
rather than building on sand.
Thoughts?
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
On Fri, Mar 4, 2022 at 4:34 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
If we are not tracking the grantors of role authorizations,
then we are doing it wrong and we ought to fix that.
So let's talk about how we could fix this. In a vacuum I'd say this is
just a feature that never got finished and we should rip the whole
thing out. That is, remove pg_auth_members.grantor entirely and at
most keep some do-nothing syntax around for backward compatibility.
However, what Tom is saying in the text quoted above is that we ought
to have something that actually works, which is more challenging.
Apparently, the desired behavior here is for this to work like grants
on non-role objects, where executing "GRANT SELECT ON TABLE s1 TO foo"
under two different user accounts bar and baz that both have
permissions to grant that privilege creates two independent grants
that can be independently revoked.
Maybe. What I was pointing out is that this is SQL-standard syntax
and there are SQL-standard semantics that it ought to be implementing.
Probably those semantics match what you describe here, but we ought
to dive into the spec and make sure before we spend a lot of effort.
It's not quite clear to me whether the spec defines any particular
unique key (identity) for the set of role authorizations.
regards, tom lane
On Thu, Jun 2, 2022 at 3:15 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Maybe. What I was pointing out is that this is SQL-standard syntax
and there are SQL-standard semantics that it ought to be implementing.
Probably those semantics match what you describe here, but we ought
to dive into the spec and make sure before we spend a lot of effort.
It's not quite clear to me whether the spec defines any particular
unique key (identity) for the set of role authorizations.
I sort of thought /messages/by-id/3981966.1646429663@sss.pgh.pa.us
constituted a completed investigation of this sort. No?
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
On Thu, Jun 2, 2022 at 3:15 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Maybe. What I was pointing out is that this is SQL-standard syntax
and there are SQL-standard semantics that it ought to be implementing.
Probably those semantics match what you describe here, but we ought
to dive into the spec and make sure before we spend a lot of effort.
It's not quite clear to me whether the spec defines any particular
unique key (identity) for the set of role authorizations.
I sort of thought /messages/by-id/3981966.1646429663@sss.pgh.pa.us
constituted a completed investigation of this sort. No?
I didn't think so. It's clear that the spec expects us to track the
grantor, but I didn't chase down what it expects us to *do* with that
information, nor what it thinks the rules are for merging multiple
authorizations.
regards, tom lane
On Thu, Jun 2, 2022 at 3:51 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I sort of thought /messages/by-id/3981966.1646429663@sss.pgh.pa.us
constituted a completed investigation of this sort. No?I didn't think so. It's clear that the spec expects us to track the
grantor, but I didn't chase down what it expects us to *do* with that
information, nor what it thinks the rules are for merging multiple
authorizations.
Hmm, OK. Well, one problem is that I've never had any luck
interpreting what the spec says about anything, and I've sort of given
up. But even if that were not so, I'm a little unclear what other
conclusion is possible here. The spec either wants the same behavior
that we already have for other object types, which is what I am here
proposing that we do, or it wants something different. If it wants
something different, it probably wants that for all object types, not
just roles. Since I doubt we would want the behavior for roles to be
inconsistent with what we do for all other object types, in that case
we would probably either change the behavior for all other object
types to something new, and then clean up the role stuff afterwards,
or else first do what I proposed here and then later change it all at
once. In which case the proposal that I've made is as good a way to
start as any.
Now, if it happens to be the case that the spec proposes a different
behavior for roles than for non-role objects, and if the behavior for
roles is something other than the only we currently have for non-role
objects, then I'd agree that the plan I propose here needs revision. I
suspect that's unlikely but I can't make anything of the spec so ....
maybe?
--
Robert Haas
EDB: http://www.enterprisedb.com
Greetings,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Thu, Jun 2, 2022 at 3:51 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I sort of thought /messages/by-id/3981966.1646429663@sss.pgh.pa.us
constituted a completed investigation of this sort. No?I didn't think so. It's clear that the spec expects us to track the
grantor, but I didn't chase down what it expects us to *do* with that
information, nor what it thinks the rules are for merging multiple
authorizations.Hmm, OK. Well, one problem is that I've never had any luck
interpreting what the spec says about anything, and I've sort of given
up. But even if that were not so, I'm a little unclear what other
conclusion is possible here. The spec either wants the same behavior
that we already have for other object types, which is what I am here
proposing that we do, or it wants something different. If it wants
something different, it probably wants that for all object types, not
just roles. Since I doubt we would want the behavior for roles to be
inconsistent with what we do for all other object types, in that case
we would probably either change the behavior for all other object
types to something new, and then clean up the role stuff afterwards,
or else first do what I proposed here and then later change it all at
once. In which case the proposal that I've made is as good a way to
start as any.Now, if it happens to be the case that the spec proposes a different
behavior for roles than for non-role objects, and if the behavior for
roles is something other than the only we currently have for non-role
objects, then I'd agree that the plan I propose here needs revision. I
suspect that's unlikely but I can't make anything of the spec so ....
maybe?
Thankfully, at least from my reading, the spec isn't all that
complicated on this particular point. The spec talks about "role
authorization descriptor"s and those are "created with role name,
grantee, and grantor" and then further says "redundant duplicate role
authorization descriptors are destroyed", presumably meaning that the
entire thing has to be identical. In other words, yeah, the PK should
include the grantor. There's a further comment that the 'set of
involved grantees' is the union of all the 'grantees', clearly
indicating that you can have multiple GRANT 'foo' to 'bar's with
distinct grantees.
In terms of how that's then used, yeah, it's during REVOKE because a
REVOKE is only able to 'find' role authorization descriptors which match
the triple of role revoked, grantee, grantor (though there's a caveat in
that the 'grantor' role could be the current role, or the current user).
Interestingly, at least in my looking it over today, it doesn't seem
that the 'grantor' could be 'any applicable role' (which is what's
usually used to indicate that it could be any role that the current role
inherits), meaning you have to include the GRANTED BY in the REVOKE
statement or do a SET ROLE first when doing a REVOKE if it's for a role
that you aren't currently running as (but which you are a member of).
Anyhow, in other words, I do think Robert's got it right here. Happy to
discuss further though if there are doubts.
Thanks,
Stephen
On Mon, Jun 6, 2022 at 7:41 PM Stephen Frost <sfrost@snowman.net> wrote:
Thankfully, at least from my reading, the spec isn't all that
complicated on this particular point. The spec talks about "role
authorization descriptor"s and those are "created with role name,
grantee, and grantor" and then further says "redundant duplicate role
authorization descriptors are destroyed", presumably meaning that the
entire thing has to be identical. In other words, yeah, the PK should
include the grantor. There's a further comment that the 'set of
involved grantees' is the union of all the 'grantees', clearly
indicating that you can have multiple GRANT 'foo' to 'bar's with
distinct grantees.In terms of how that's then used, yeah, it's during REVOKE because a
REVOKE is only able to 'find' role authorization descriptors which match
the triple of role revoked, grantee, grantor (though there's a caveat in
that the 'grantor' role could be the current role, or the current user).
What is supposed to happen if someone tries to execute DROP ROLE on a
role that has previously been used as a grantor?
Consider:
create role foo;
create role bar;
create role baz;
grant foo to bar granted by baz;
drop role baz;
Upthread, I proposed that "drop role baz" should fail here, but
there's at least one other option: it could silently remove the grant,
as we would do if either foo or bar were dropped. The situation is not
quite comparable, though: a grant from foo to bar makes no logical
sense if either of those roles cease to exist, but it does make at
least some sense if baz ceases to exist. Therefore I think someone
could argue either for an error or for removing the grant -- or
possibly even for some other behavior, though the other behaviors that
I can think of don't make much sense in a world where the primary key
of pg_auth_members is (roleid, member, grantor).
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Jun 24, 2022 at 1:19 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Mon, Jun 6, 2022 at 7:41 PM Stephen Frost <sfrost@snowman.net> wrote:
In terms of how that's then used, yeah, it's during REVOKE because a
REVOKE is only able to 'find' role authorization descriptors which match
the triple of role revoked, grantee, grantor (though there's a caveat in
that the 'grantor' role could be the current role, or the current user).What is supposed to happen if someone tries to execute DROP ROLE on a
role that has previously been used as a grantor?Upthread, I proposed that "drop role baz" should fail here
I concur with this.
I think that the grantor owns the grant, and that REASSIGNED OWNED should
be able to move those grants to someone else.
By extension, DROP OWNED should remove them.
David J.
On Fri, Jun 24, 2022 at 4:30 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:
Upthread, I proposed that "drop role baz" should fail here
I concur with this.
I think that the grantor owns the grant, and that REASSIGNED OWNED should be able to move those grants to someone else.
By extension, DROP OWNED should remove them.
Interesting. I hadn't thought about changing the behavior of DROP
OWNED BY and REASSIGN OWNED BY. A quick experiment supports your
interpretation:
rhaas=# grant select on table foo to bar;
GRANT
rhaas=# revoke select on table foo from bar;
REVOKE
rhaas=# grant select on table foo to bar with grant option;
GRANT
rhaas=# set role bar;
SET
rhaas=> grant select on table foo to baz;
GRANT
rhaas=> reset role;
RESET
rhaas=# drop role bar;
ERROR: role "bar" cannot be dropped because some objects depend on it
DETAIL: privileges for table foo
rhaas=# drop owned by bar;
DROP OWNED
rhaas=# drop role bar;
DROP ROLE
So, privileges on tables (and presumably all other SQL objects)
already work the way that you propose here. If we choose to make role
memberships work in some other way then the two will be inconsistent.
Probably we shouldn't do that. There is still the question of what the
SQL specification says about this, but I would guess that it mandates
the same behavior for all kinds of privileges rather than treating
role memberships and table permissions in different ways. I could be
wrong, though.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, Jun 24, 2022 at 4:46 PM Robert Haas <robertmhaas@gmail.com> wrote:
Interesting. I hadn't thought about changing the behavior of DROP
OWNED BY and REASSIGN OWNED BY. A quick experiment supports your
interpretation:
Here is a minimal patch fixing exactly $SUBJECT. Granting a role to
another role now creates a dependency on the grantor, so if you try to
drop the grantor you get an ERROR. You can resolve that by revoking
the grant, or by using DROP OWNED BY or REASSIGN OWNED BY. To make
this work, I had to make role memberships participate in the
dependency system, which means pg_auth_members gains an OID column.
The tricky part is that removing either of the two roles directly
involved in a grant currently does, and should still, silently remove
the grant. So, if you do "GRANT foo TO bar GRANTED BY baz", and then
try to "DROP ROLE baz", that should fail, but if you instead try to
"DROP ROLE baz, bar", that should work, because when bar is removed,
the grant is silently removed, and then it's OK to drop baz. If these
were database-local objects I think this could have all been sorted
out quite easily by creating dependencies on all three roles involved
in the GRANT and using the right deptype for each, but shared objects
have their own set of deptypes which seemed to present no easy
solution to this problem. I resolved the issue by having DropRole()
make two loops over the list of roles to be dropped rather than one;
see patch for details.
There are several things that I think ought to be changed which this
patch does not change. Most likely, I'll try to write separate patches
for those things rather than making this one bigger.
First, as discussed upthread, I think we ought to change things so
that you can have multiple simultaneous grants of role A to role B
each with a different grantor. That is what we do for other types of
grants and Stephen at least thinks it's what the SQL standard
specifies.
Second, I think we ought to enforce that the grantor has to be a role
which has the ability to perform the grant, just as we do for other
object types. This is a little thorny, though, because we play some
tricks with other types of objects that don't work for roles. If
superuser alice executes "GRANT SELECT ON bobs_table TO fred" we
record the owner of the grant as being the table owner and update the
ownership of the grant each time the table owner is changed. That way,
even if alice ceases to be a superuser, we maintain the invariant that
the grantor of record must have privileges to perform the grant. But
if superuser alice executes "GRANT accounting TO fred", we can't use
the same trick, because the "accounting" role doesn't have an owner.
If we attribute the grant to alice and she ceases to be a superuser
(and also doesn't have CREATEROLE) then the invariant is violated.
Attributing the grant to the bootstrap superuser doesn't help, as that
user can also be made not a superuser. Attributing the grant to
accounting is no good, as accounting doesn't and can't have ADMIN
OPTION on itself; and fred doesn't have to have ADMIN OPTION on
accounting either.
One way to fix this problem would be to prohibit the removal of
superuser privileges from the booststrap superuser. Then, we could
attribute grants made by users who lack ADMIN OPTION on the granted
role to the bootstrap superuser. Grants made by users who do possess
ADMIN OPTION would be attributed to the actual grantor (unless GRANTED
BY was used) and removing ADMIN OPTION from such a user could be made
to fail if they had outstanding role grants. I think that's probably
the nearest analogue of what we do for other object types, but if
you've got another idea what to do here, I'd love to hear it.
Thoughts on this patch would be great, too.
Thanks,
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v1-0001-Ensure-that-pg_auth_members.grantor-is-always-val.patchapplication/octet-stream; name=v1-0001-Ensure-that-pg_auth_members.grantor-is-always-val.patchDownload
From 0bdf2a0f8fb3298ad1f67e2c1008d4bc156f1c5c Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 24 Jun 2022 15:43:13 -0400
Subject: [PATCH v1] Ensure that pg_auth_members.grantor is always valid.
Previously, "GRANT foo TO bar" or "GRANT foo TO bar GRANTED BY baz"
would record the OID of the grantor in pg_auth_members.grantor, but
that role could later be dropped without modifying or removing the
pg_auth_members record. That's not great, because we typically try
to avoid dangling references in catalog data.
Now, a role grant depends on the grantor, and the grantor can't be
dropped without removing the grant or changing the grantor.
"DROP OWNED BY" will remove the grant, and "REASSIGN OWNED BY"
will change the grantor, similar to what happens with permissions
on other types of SQL objects, where the grantor is regarded as the
"owner" of the grant.
pg_auth_members now has an OID column, because that is needed in order
for dependencies to work. It also now has an index on the grantor
column, because otherwise dropping a role would require a sequential
scan of the entire table to see whether the role's OID is in use as
a grantor. That probably wouldn't be too large a problem in practice,
but it seems better to have an index just in case.
---
doc/src/sgml/catalogs.sgml | 10 ++
src/backend/catalog/catalog.c | 2 +
src/backend/catalog/dependency.c | 14 +-
src/backend/catalog/objectaddress.c | 108 +++++++++++++
src/backend/catalog/pg_shdepend.c | 25 ++-
src/backend/catalog/system_views.sql | 2 +-
src/backend/commands/alter.c | 1 +
src/backend/commands/event_trigger.c | 1 +
src/backend/commands/tablecmds.c | 1 +
src/backend/commands/user.c | 176 ++++++++++++++++------
src/include/catalog/dependency.h | 1 +
src/include/catalog/pg_auth_members.h | 5 +-
src/test/regress/expected/create_role.out | 16 +-
src/test/regress/expected/privileges.out | 31 ++++
src/test/regress/sql/create_role.sql | 7 +-
src/test/regress/sql/privileges.sql | 26 ++++
16 files changed, 363 insertions(+), 63 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index 25b02c4e37..2683fb4cfe 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1669,6 +1669,16 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</thead>
<tbody>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>oid</structfield> <type>oid</type>
+ </para>
+ <para>
+ Row identifier
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>roleid</structfield> <type>oid</type>
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index e784538aae..f5e894c0c6 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -262,6 +262,8 @@ IsSharedRelation(Oid relationId)
relationId == AuthIdRolnameIndexId ||
relationId == AuthMemMemRoleIndexId ||
relationId == AuthMemRoleMemIndexId ||
+ relationId == AuthMemOidIndexId ||
+ relationId == AuthMemGrantorIndexId ||
relationId == DatabaseNameIndexId ||
relationId == DatabaseOidIndexId ||
relationId == DbRoleSettingDatidRolidIndexId ||
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index de10923391..5738c60b9e 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -28,6 +28,7 @@
#include "catalog/pg_amproc.h"
#include "catalog/pg_attrdef.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_cast.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
@@ -171,6 +172,7 @@ static const Oid object_classes[] = {
TSTemplateRelationId, /* OCLASS_TSTEMPLATE */
TSConfigRelationId, /* OCLASS_TSCONFIG */
AuthIdRelationId, /* OCLASS_ROLE */
+ AuthMemRelationId, /* OCLASS_ROLE_MEMBERSHIP */
DatabaseRelationId, /* OCLASS_DATABASE */
TableSpaceRelationId, /* OCLASS_TBLSPACE */
ForeignDataWrapperRelationId, /* OCLASS_FDW */
@@ -1493,6 +1495,7 @@ doDeletion(const ObjectAddress *object, int flags)
case OCLASS_TSPARSER:
case OCLASS_TSDICT:
case OCLASS_TSTEMPLATE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_FDW:
case OCLASS_FOREIGN_SERVER:
case OCLASS_USER_MAPPING:
@@ -1526,9 +1529,8 @@ doDeletion(const ObjectAddress *object, int flags)
* Accepts the same flags as performDeletion (though currently only
* PERFORM_DELETION_CONCURRENTLY does anything).
*
- * We use LockRelation for relations, LockDatabaseObject for everything
- * else. Shared-across-databases objects are not currently supported
- * because no caller cares, but could be modified to use LockSharedObject.
+ * We use LockRelation for relations, and otherwise LockSharedObject or
+ * LockDatabaseObject as appropriate for the object type.
*/
void
AcquireDeletionLock(const ObjectAddress *object, int flags)
@@ -1546,6 +1548,9 @@ AcquireDeletionLock(const ObjectAddress *object, int flags)
else
LockRelationOid(object->objectId, AccessExclusiveLock);
}
+ else if (object->classId == AuthMemRelationId)
+ LockSharedObject(object->classId, object->objectId, 0,
+ AccessExclusiveLock);
else
{
/* assume we should lock the whole object not a sub-object */
@@ -2840,6 +2845,9 @@ getObjectClass(const ObjectAddress *object)
case AuthIdRelationId:
return OCLASS_ROLE;
+ case AuthMemRelationId:
+ return OCLASS_ROLE_MEMBERSHIP;
+
case DatabaseRelationId:
return OCLASS_DATABASE;
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 8377b4f7d4..7c5fd23af7 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -27,6 +27,7 @@
#include "catalog/pg_amproc.h"
#include "catalog/pg_attrdef.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_cast.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
@@ -386,6 +387,20 @@ static const ObjectPropertyType ObjectProperty[] =
-1,
true
},
+ {
+ "role membership",
+ AuthMemRelationId,
+ AuthMemOidIndexId,
+ -1,
+ -1,
+ Anum_pg_auth_members_oid,
+ InvalidAttrNumber,
+ InvalidAttrNumber,
+ Anum_pg_auth_members_grantor,
+ InvalidAttrNumber,
+ -1,
+ true
+ },
{
"rule",
RewriteRelationId,
@@ -787,6 +802,10 @@ static const struct object_type_map
{
"role", OBJECT_ROLE
},
+ /* OCLASS_ROLE_MEMBERSHIP */
+ {
+ "role membership", -1 /* unmapped */
+ },
/* OCLASS_DATABASE */
{
"database", OBJECT_DATABASE
@@ -3648,6 +3667,48 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
break;
}
+ case OCLASS_ROLE_MEMBERSHIP:
+ {
+ Relation amDesc;
+ ScanKeyData skey[1];
+ SysScanDesc rcscan;
+ HeapTuple tup;
+ Form_pg_auth_members amForm;
+
+ amDesc = table_open(AuthMemRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_auth_members_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+
+ rcscan = systable_beginscan(amDesc, AuthMemOidIndexId, true,
+ NULL, 1, skey);
+
+ tup = systable_getnext(rcscan);
+
+ if (!HeapTupleIsValid(tup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for role membership %u",
+ object->objectId);
+
+ systable_endscan(rcscan);
+ table_close(amDesc, AccessShareLock);
+ break;
+ }
+
+ amForm = (Form_pg_auth_members) GETSTRUCT(tup);
+
+ appendStringInfo(&buffer, _("membership of role %s in role %s"),
+ GetUserNameFromId(amForm->member, false),
+ GetUserNameFromId(amForm->roleid, false));
+
+ systable_endscan(rcscan);
+ table_close(amDesc, AccessShareLock);
+ break;
+ }
+
case OCLASS_DATABASE:
{
char *datname;
@@ -4537,6 +4598,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
appendStringInfoString(&buffer, "role");
break;
+ case OCLASS_ROLE_MEMBERSHIP:
+ appendStringInfoString(&buffer, "role membership");
+ break;
+
case OCLASS_DATABASE:
appendStringInfoString(&buffer, "database");
break;
@@ -5480,6 +5545,49 @@ getObjectIdentityParts(const ObjectAddress *object,
break;
}
+ case OCLASS_ROLE_MEMBERSHIP:
+ {
+ Relation authMemDesc;
+ ScanKeyData skey[1];
+ SysScanDesc amscan;
+ HeapTuple tup;
+ Form_pg_auth_members amForm;
+
+ authMemDesc = table_open(AuthMemRelationId,
+ AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_auth_members_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+
+ amscan = systable_beginscan(authMemDesc, AuthMemOidIndexId, true,
+ NULL, 1, skey);
+
+ tup = systable_getnext(amscan);
+
+ if (!HeapTupleIsValid(tup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for pg_auth_members entry %u",
+ object->objectId);
+
+ systable_endscan(amscan);
+ table_close(authMemDesc, AccessShareLock);
+ break;
+ }
+
+ amForm = (Form_pg_auth_members) GETSTRUCT(tup);
+
+ appendStringInfo(&buffer, _("membership of role %s in role %s"),
+ GetUserNameFromId(amForm->member, false),
+ GetUserNameFromId(amForm->roleid, false));
+
+ systable_endscan(amscan);
+ table_close(authMemDesc, AccessShareLock);
+ break;
+ }
+
case OCLASS_DATABASE:
{
char *datname;
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index 3e8fa008b9..c8c1f78567 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -22,6 +22,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_conversion.h"
#include "catalog/pg_database.h"
@@ -1399,21 +1400,18 @@ shdepDropOwned(List *roleids, DropBehavior behavior)
}
break;
case SHARED_DEPENDENCY_OWNER:
- /* If a local object, save it for deletion below */
- if (sdepForm->dbid == MyDatabaseId)
+ /* Save it for deletion below */
+ obj.classId = sdepForm->classid;
+ obj.objectId = sdepForm->objid;
+ obj.objectSubId = sdepForm->objsubid;
+ /* as above */
+ AcquireDeletionLock(&obj, 0);
+ if (!systable_recheck_tuple(scan, tuple))
{
- obj.classId = sdepForm->classid;
- obj.objectId = sdepForm->objid;
- obj.objectSubId = sdepForm->objsubid;
- /* as above */
- AcquireDeletionLock(&obj, 0);
- if (!systable_recheck_tuple(scan, tuple))
- {
- ReleaseDeletionLock(&obj);
- break;
- }
- add_exact_object_address(&obj, deleteobjs);
+ ReleaseDeletionLock(&obj);
+ break;
}
+ add_exact_object_address(&obj, deleteobjs);
break;
}
}
@@ -1593,6 +1591,7 @@ shdepReassignOwned(List *roleids, Oid newrole)
case DatabaseRelationId:
case TSConfigRelationId:
case TSDictionaryRelationId:
+ case AuthMemRelationId:
{
Oid classId = sdepForm->classid;
Relation catalog;
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index fedaed533b..eb95737fcc 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -53,7 +53,7 @@ CREATE VIEW pg_group AS
SELECT
rolname AS groname,
oid AS grosysid,
- ARRAY(SELECT member FROM pg_auth_members WHERE roleid = oid) AS grolist
+ ARRAY(SELECT member FROM pg_auth_members WHERE roleid = pg_authid.oid) AS grolist
FROM pg_authid
WHERE NOT rolcanlogin;
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 5456b8222b..55219bb097 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -650,6 +650,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
case OCLASS_TRIGGER:
case OCLASS_SCHEMA:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_FDW:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index 4642527881..d09cdc6c0a 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -1016,6 +1016,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_PARAMETER_ACL:
/* no support for global objects */
return false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 2de0ebacec..aaf0876381 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12671,6 +12671,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
case OCLASS_TSTEMPLATE:
case OCLASS_TSCONFIG:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_FDW:
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 984305ba31..6b1719193c 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -911,6 +911,7 @@ DropRole(DropRoleStmt *stmt)
Relation pg_authid_rel,
pg_auth_members_rel;
ListCell *item;
+ List *role_addresses = NIL;
if (!have_createrole_privilege())
ereport(ERROR,
@@ -919,7 +920,7 @@ DropRole(DropRoleStmt *stmt)
/*
* Scan the pg_authid relation to find the Oid of the role(s) to be
- * deleted.
+ * deleted and perform prleliminary permissions and sanity checks.
*/
pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
pg_auth_members_rel = table_open(AuthMemRelationId, RowExclusiveLock);
@@ -932,10 +933,9 @@ DropRole(DropRoleStmt *stmt)
tmp_tuple;
Form_pg_authid roleform;
ScanKeyData scankey;
- char *detail;
- char *detail_log;
SysScanDesc sscan;
Oid roleid;
+ ObjectAddress *role_address;
if (rolspec->roletype != ROLESPEC_CSTRING)
ereport(ERROR,
@@ -991,34 +991,32 @@ DropRole(DropRoleStmt *stmt)
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
+ /* Don't leak the syscache tuple */
+ ReleaseSysCache(tuple);
+
/*
* Lock the role, so nobody can add dependencies to her while we drop
* her. We keep the lock until the end of transaction.
*/
LockSharedObject(AuthIdRelationId, roleid, 0, AccessExclusiveLock);
- /* Check for pg_shdepend entries depending on this role */
- if (checkSharedDependencies(AuthIdRelationId, roleid,
- &detail, &detail_log))
- ereport(ERROR,
- (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
- errmsg("role \"%s\" cannot be dropped because some objects depend on it",
- role),
- errdetail_internal("%s", detail),
- errdetail_log("%s", detail_log)));
-
/*
- * Remove the role from the pg_authid table
- */
- CatalogTupleDelete(pg_authid_rel, &tuple->t_self);
-
- ReleaseSysCache(tuple);
-
- /*
- * Remove role from the pg_auth_members table. We have to remove all
- * tuples that show it as either a role or a member.
+ * If there is a pg_auth_members entry that has one of the roles to
+ * be dropped as the roleid or member, it should be silently removed
+ * without complaint, but if there is a pg_auth_members entry that has
+ * one of the roles to be dropped as the grantor, the operation should
+ * fail.
*
- * XXX what about grantor entries? Maybe we should do one heap scan.
+ * It's possible, however, that a single pg_auth_members entry could
+ * fall into multiple categories - e.g. the user could do "GRANT foo
+ * TO bar GRANTED BY baz" and then "DROP ROLE baz, bar". We want
+ * such an operation to succeed regardless of the order in which the
+ * to-be-dropped roles are passed to DROP ROLE.
+ *
+ * To make that work, we remove all pg_auth_members entries that can
+ * be silently removed in this loop, and then below we'll make a second
+ * pass over the list of roles to be removed and check for any
+ * remaining dependencies.
*/
ScanKeyInit(&scankey,
Anum_pg_auth_members_roleid,
@@ -1030,6 +1028,11 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
+ Form_pg_auth_members authmem_form;
+
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
}
@@ -1045,22 +1048,16 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
+ Form_pg_auth_members authmem_form;
+
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
}
systable_endscan(sscan);
- /*
- * Remove any comments or security labels on this role.
- */
- DeleteSharedComments(roleid, AuthIdRelationId);
- DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
-
- /*
- * Remove settings for this role.
- */
- DropSetting(InvalidOid, roleid);
-
/*
* Advance command counter so that later iterations of this loop will
* see the changes already made. This is essential if, for example,
@@ -1071,6 +1068,72 @@ DropRole(DropRoleStmt *stmt)
* itself.)
*/
CommandCounterIncrement();
+
+ /* Looks tentatively OK, add it to the list. */
+ role_address = palloc(sizeof(ObjectAddress));
+ role_address->classId = AuthIdRelationId;
+ role_address->objectId = roleid;
+ role_address->objectSubId = 0;
+ role_addresses = lappend(role_addresses, role_address);
+ }
+
+ /*
+ * Second pass over the roles to be removed.
+ */
+ foreach(item, role_addresses)
+ {
+ ObjectAddress *role_address = lfirst(item);
+ Oid roleid = role_address->objectId;
+ HeapTuple tuple;
+ Form_pg_authid roleform;
+ char *detail;
+ char *detail_log;
+
+ /*
+ * Re-find the pg_authid tuple.
+ *
+ * Since we've taken a lock on the role OID, it shouldn't be possible
+ * for the tuple to have been deleted -- or for that matter updated --
+ * unless the user is manually modifying the system catalogs.
+ */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for role %u", roleid);
+ roleform = (Form_pg_authid) GETSTRUCT(tuple);
+
+ /*
+ * Check for pg_shdepend entries depending on this role.
+ *
+ * This needs to happen after we've completed removing any
+ * pg_auth_members entries that can be removed silently, in order
+ * to avoid spurious failures. See notes above for more details.
+ */
+ if (checkSharedDependencies(AuthIdRelationId, roleid,
+ &detail, &detail_log))
+ ereport(ERROR,
+ (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
+ errmsg("role \"%s\" cannot be dropped because some objects depend on it",
+ NameStr(roleform->rolname)),
+ errdetail_internal("%s", detail),
+ errdetail_log("%s", detail_log)));
+
+ /*
+ * Remove the role from the pg_authid table
+ */
+ CatalogTupleDelete(pg_authid_rel, &tuple->t_self);
+
+ ReleaseSysCache(tuple);
+
+ /*
+ * Remove any comments or security labels on this role.
+ */
+ DeleteSharedComments(roleid, AuthIdRelationId);
+ DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
+
+ /*
+ * Remove settings for this role.
+ */
+ DropSetting(InvalidOid, roleid);
}
/*
@@ -1443,6 +1506,7 @@ AddRoleMems(const char *rolename, Oid roleid,
Datum new_record[Natts_pg_auth_members];
bool new_record_nulls[Natts_pg_auth_members];
bool new_record_repl[Natts_pg_auth_members];
+ Form_pg_auth_members authmem_form;
/*
* pg_database_owner is never a role member. Lifting this restriction
@@ -1488,15 +1552,22 @@ AddRoleMems(const char *rolename, Oid roleid,
authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
ObjectIdGetDatum(roleid),
ObjectIdGetDatum(memberid));
- if (HeapTupleIsValid(authmem_tuple) &&
- (!admin_opt ||
- ((Form_pg_auth_members) GETSTRUCT(authmem_tuple))->admin_option))
+ if (!HeapTupleIsValid(authmem_tuple))
{
- ereport(NOTICE,
- (errmsg("role \"%s\" is already a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
- ReleaseSysCache(authmem_tuple);
- continue;
+ authmem_form = NULL;
+ }
+ else
+ {
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (!admin_opt || authmem_form->admin_option)
+ {
+ ereport(NOTICE,
+ (errmsg("role \"%s\" is already a member of role \"%s\"",
+ get_rolespec_name(memberRole), rolename)));
+ ReleaseSysCache(authmem_tuple);
+ continue;
+ }
}
/* Build a tuple to insert or update */
@@ -1517,13 +1588,24 @@ AddRoleMems(const char *rolename, Oid roleid,
new_record,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
+
+ if (authmem_form->grantor != grantorId)
+ changeDependencyOnOwner(AuthMemRelationId, authmem_form->oid,
+ grantorId);
+
ReleaseSysCache(authmem_tuple);
}
else
{
+ Oid objectId;
+
+ objectId = GetNewObjectId();
+ new_record[Anum_pg_auth_members_oid - 1] = objectId;
tuple = heap_form_tuple(pg_authmem_dsc,
new_record, new_record_nulls);
CatalogTupleInsert(pg_authmem_rel, tuple);
+
+ recordDependencyOnOwner(AuthMemRelationId, objectId, grantorId);
}
/* CCI after each change, in case there are duplicates in list */
@@ -1590,6 +1672,7 @@ DelRoleMems(const char *rolename, Oid roleid,
RoleSpec *memberRole = lfirst(specitem);
Oid memberid = lfirst_oid(iditem);
HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
/*
* Find entry for this role/member
@@ -1605,9 +1688,16 @@ DelRoleMems(const char *rolename, Oid roleid,
continue;
}
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
if (!admin_opt)
{
- /* Remove the entry altogether */
+ /*
+ * Remove the entry altogether, after first removing its
+ * dependenceis
+ */
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_authmem_rel, &authmem_tuple->t_self);
}
else
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index d027075a4c..0a1e072bef 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -112,6 +112,7 @@ typedef enum ObjectClass
OCLASS_TSTEMPLATE, /* pg_ts_template */
OCLASS_TSCONFIG, /* pg_ts_config */
OCLASS_ROLE, /* pg_authid */
+ OCLASS_ROLE_MEMBERSHIP, /* pg_auth_members */
OCLASS_DATABASE, /* pg_database */
OCLASS_TBLSPACE, /* pg_tablespace */
OCLASS_FDW, /* pg_foreign_data_wrapper */
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index 1bc027f133..c9d7697730 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -29,6 +29,7 @@
*/
CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(2843,AuthMemRelation_Rowtype_Id) BKI_SCHEMA_MACRO
{
+ Oid oid; /* oid */
Oid roleid BKI_LOOKUP(pg_authid); /* ID of a role */
Oid member BKI_LOOKUP(pg_authid); /* ID of a member of that role */
Oid grantor BKI_LOOKUP(pg_authid); /* who granted the membership */
@@ -42,7 +43,9 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
*/
typedef FormData_pg_auth_members *Form_pg_auth_members;
-DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_oid_index, 9385, AuthMemOidIndexId, on pg_auth_members using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops));
+DECLARE_INDEX(pg_auth_members_grantor_index, 9384, AuthMemGrantorIndexId, on pg_auth_members using btree(grantor oid_ops));
#endif /* PG_AUTH_MEMBERS_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..b6d1de26c9 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -103,9 +103,21 @@ ERROR: role "regress_nosuch_recursive" does not exist
DROP ROLE regress_nosuch_admin_recursive;
ERROR: role "regress_nosuch_admin_recursive" does not exist
DROP ROLE regress_plainrole;
+-- fail, can't drop regress_createrole yet, due to outstanding grants
+DROP ROLE regress_createrole;
+ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
+DETAIL: owner of membership of role regress_read_all_data in role pg_read_all_data
+owner of membership of role regress_write_all_data in role pg_write_all_data
+owner of membership of role regress_monitor in role pg_monitor
+owner of membership of role regress_read_all_settings in role pg_read_all_settings
+owner of membership of role regress_read_all_stats in role pg_read_all_stats
+owner of membership of role regress_stat_scan_tables in role pg_stat_scan_tables
+owner of membership of role regress_read_server_files in role pg_read_server_files
+owner of membership of role regress_write_server_files in role pg_write_server_files
+owner of membership of role regress_execute_server_program in role pg_execute_server_program
+owner of membership of role regress_signal_backend in role pg_signal_backend
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
-DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -125,6 +137,8 @@ DROP ROLE regress_read_server_files;
DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
+-- ok, dropped the other roles first so this is ok now
+DROP ROLE regress_createrole;
-- fail, role still owns database objects
DROP ROLE regress_tenant;
ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 03df567d50..c91783bc14 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -33,6 +33,37 @@ CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
+GRANT regress_priv_user1 TO regress_priv_user2 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- fail, dependency
+ERROR: role "regress_priv_user3" cannot be dropped because some objects depend on it
+DETAIL: owner of membership of role regress_priv_user2 in role regress_priv_user1
+REASSIGN OWNED BY regress_priv_user3 TO regress_priv_user4;
+DROP ROLE regress_priv_user3; -- ok now
+DROP ROLE regress_priv_user4; -- fail, dependency
+ERROR: role "regress_priv_user4" cannot be dropped because some objects depend on it
+DETAIL: owner of membership of role regress_priv_user2 in role regress_priv_user1
+DROP OWNED BY regress_priv_user4;
+DROP ROLE regress_priv_user4; -- ok now
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user5 TO regress_priv_user6 GRANTED BY regress_priv_user1;
+DROP ROLE regress_priv_user1; -- should fail, dependency
+ERROR: role "regress_priv_user1" cannot be dropped because some objects depend on it
+DETAIL: owner of membership of role regress_priv_user6 in role regress_priv_user5
+DROP ROLE regress_priv_user5; -- ok
+DROP ROLE regress_priv_user1; -- ok now
+GRANT regress_priv_user7 TO regress_priv_user6 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- should fail, dependency
+ERROR: role "regress_priv_user2" cannot be dropped because some objects depend on it
+DETAIL: owner of membership of role regress_priv_user6 in role regress_priv_user7
+DROP ROLE regress_priv_user2, regress_priv_user6; -- ok, despite order
+-- recreate the roles we just dropped
+CREATE USER regress_priv_user1;
+CREATE USER regress_priv_user2;
+CREATE USER regress_priv_user3;
+CREATE USER regress_priv_user4;
+CREATE USER regress_priv_user5;
+CREATE USER regress_priv_user6;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..b696628238 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -98,9 +98,11 @@ DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
+-- fail, can't drop regress_createrole yet, due to outstanding grants
+DROP ROLE regress_createrole;
+
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
-DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -121,6 +123,9 @@ DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
+-- ok, dropped the other roles first so this is ok now
+DROP ROLE regress_createrole;
+
-- fail, role still owns database objects
DROP ROLE regress_tenant;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 2a6ba38e52..e9a415d3d8 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -37,6 +37,32 @@ CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
+GRANT regress_priv_user1 TO regress_priv_user2 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- fail, dependency
+REASSIGN OWNED BY regress_priv_user3 TO regress_priv_user4;
+DROP ROLE regress_priv_user3; -- ok now
+DROP ROLE regress_priv_user4; -- fail, dependency
+DROP OWNED BY regress_priv_user4;
+DROP ROLE regress_priv_user4; -- ok now
+
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user5 TO regress_priv_user6 GRANTED BY regress_priv_user1;
+DROP ROLE regress_priv_user1; -- should fail, dependency
+DROP ROLE regress_priv_user5; -- ok
+DROP ROLE regress_priv_user1; -- ok now
+GRANT regress_priv_user7 TO regress_priv_user6 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- should fail, dependency
+DROP ROLE regress_priv_user2, regress_priv_user6; -- ok, despite order
+
+-- recreate the roles we just dropped
+CREATE USER regress_priv_user1;
+CREATE USER regress_priv_user2;
+CREATE USER regress_priv_user3;
+CREATE USER regress_priv_user4;
+CREATE USER regress_priv_user5;
+CREATE USER regress_priv_user6;
+
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
--
2.24.3 (Apple Git-128)
On Fri, Jun 24, 2022 at 4:46 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Fri, Jun 24, 2022 at 4:30 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:Upthread, I proposed that "drop role baz" should fail here
I concur with this.
I think that the grantor owns the grant, and that REASSIGNED OWNED should be able to move those grants to someone else.
By extension, DROP OWNED should remove them.
Interesting. I hadn't thought about changing the behavior of DROP
OWNED BY and REASSIGN OWNED BY. A quick experiment supports your
interpretation:
This experiment was insufficiently thorough. I see now that, for other
object types, DROP OWNED BY does work in the way that you propose, but
REASSIGN OWNED BY does not. Here's a better test:
rhaas=# create table foo();
CREATE TABLE
rhaas=# create role bar;
CREATE ROLE
rhaas=# create role baz;
CREATE ROLE
rhaas=# grant select on table foo to bar with grant option;
GRANT
rhaas=# set role bar;
SET
rhaas=> grant select on table foo to baz;
GRANT
rhaas=> reset role;
RESET
rhaas=# drop role bar;
ERROR: role "bar" cannot be dropped because some objects depend on it
DETAIL: privileges for table foo
rhaas=# create role quux;
CREATE ROLE
rhaas=# reassign owned by bar to quux;
REASSIGN OWNED
rhaas=# drop role bar;
ERROR: role "bar" cannot be dropped because some objects depend on it
DETAIL: privileges for table foo
rhaas=# drop owned by bar;
DROP OWNED
rhaas=# drop role bar;
DROP ROLE
This behavior might look somewhat bizarre, but there's actually a good
reason for it: the system guarantees that whoever is listed as the
grantor of a privilege has the *current* right to grant that
privilege. It can't categorically change the grantor of every
privilege given by bar to quux because quux might not and in fact does
not have the right to grant select on table foo to baz. Now, you might
be thinking, ah, but what if the superuser performed the grant? They
could cease to be the superuser later, and then the rule would be
violated! But actually not, because a grant by the superuser is
imputed to the table owner, who always has the right to grant all
rights on the table, and if the table owner is ever changed, all the
grants imputed to the old table owner are changed to have their
grantor as the new table owner. Similarly, trying to revoke select, or
the grant option on it, from bar would fail. So it looks pretty
intentional, and pretty tightly-enforced, that every role listed as a
grantor must be one which is currently able to grant that privilege.
And that means that REASSIGN OWNED can't just do a blanket change to
the recorded grantor. It could try to do so, I suppose, and just throw
an error if it doesn't work out, but that might make REASSIGN OWNED
fail a lot more often, which could suck. In any event, the implemented
behavior is that REASSIGN OWNED does nothing about permissions, but
DROP OWNED cascades to grantors. This is SORT OF documented, although
the documentation only mentions that DROP OWNED cascades to privileges
granted *to* the target role, and does not mention that it also
cascades to privileges granted *by* the target role.
The previous version of the patch makes both DROP OWNED and REASSIGN
OWNED cascade to grantors, but I now think that, for consistency, I'd
better look into changing it so that only DROP OWNED cascades. I think
perhaps I should be using SHARED_DEPENDENCY_ACL instead of
SHARED_DEPENDENCY_OWNER.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Wed, Jul 20, 2022 at 3:11 PM Robert Haas <robertmhaas@gmail.com> wrote:
The previous version of the patch makes both DROP OWNED and REASSIGN
OWNED cascade to grantors, but I now think that, for consistency, I'd
better look into changing it so that only DROP OWNED cascades. I think
perhaps I should be using SHARED_DEPENDENCY_ACL instead of
SHARED_DEPENDENCY_OWNER.
All right, here's a new patch set, now with a second patch added to the series.
0001, as before, is a minimal fix for $SUBJECT, but it now uses
SHARED_DEPENDENCY_ACL instead of SHARED_DEPENDENCY_OWNER, because that
gives behavior which is more like what we do for other object types.
However, it confines itself to making sure that
pg_auth_members.grantor is a valid user, and that's it.
0002 then revises the behavior substantially further to make role
grants work like other grants. The grantor of record is required to be
a user with ADMIN OPTION on the grant, or the bootstrap superuser,
just as for other object types the grantor of record must have GRANT
OPTION or be the object owner (but roles don't have owners). Dependent
grants are tracked and must be revoked before the grants upon which
they depend, but REVOKE .. CASCADE now works. Dependent grants must be
acyclic: you can't have alice getting ADMIN OPTION from bob and bob
getting it from alice; somebody's got to get it from the bootstrap
superuser. This is all just by analogy with what we do for grants on
object types, and making role grants do something similar instead of
the completely random treatment we have at present.
I believe that these patches are mostly complete, but I think that
dumpRoleMembership() probably needs some more work. I don't know what
exactly, but there's nothing to cause it to dump the role grants in an
order that will create dependent grants after the things that they
depend on, which seems essential.
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v2-0001-Ensure-that-pg_auth_members.grantor-is-always-val.patchapplication/octet-stream; name=v2-0001-Ensure-that-pg_auth_members.grantor-is-always-val.patchDownload
From 922d8049cd599a17133e22324d9060cf54049f40 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 24 Jun 2022 15:43:13 -0400
Subject: [PATCH v2 1/2] Ensure that pg_auth_members.grantor is always valid.
Previously, "GRANT foo TO bar" or "GRANT foo TO bar GRANTED BY baz"
would record the OID of the grantor in pg_auth_members.grantor, but
that role could later be dropped without modifying or removing the
pg_auth_members record. That's not great, because we typically try
to avoid dangling references in catalog data.
Now, a role grant depends on the grantor, and the grantor can't be
dropped without removing the grant or changing the grantor. "DROP
OWNED BY" will remove the grant, just as it does for other kinds of
privileges. "REASSIGN OWNED BY" will not, again just like what we do
in other cases involving privileges.
pg_auth_members now has an OID column, because that is needed in order
for dependencies to work. It also now has an index on the grantor
column, because otherwise dropping a role would require a sequential
scan of the entire table to see whether the role's OID is in use as
a grantor. That probably wouldn't be too large a problem in practice,
but it seems better to have an index just in case.
---
doc/src/sgml/catalogs.sgml | 10 ++
src/backend/catalog/catalog.c | 2 +
src/backend/catalog/dependency.c | 14 +-
src/backend/catalog/objectaddress.c | 108 ++++++++++++
src/backend/catalog/pg_shdepend.c | 47 ++++--
src/backend/catalog/system_views.sql | 2 +-
src/backend/commands/alter.c | 1 +
src/backend/commands/event_trigger.c | 1 +
src/backend/commands/tablecmds.c | 1 +
src/backend/commands/user.c | 192 +++++++++++++++++-----
src/include/catalog/dependency.h | 1 +
src/include/catalog/pg_auth_members.h | 5 +-
src/test/regress/expected/create_role.out | 16 +-
src/test/regress/expected/privileges.out | 32 ++++
src/test/regress/sql/create_role.sql | 7 +-
src/test/regress/sql/privileges.sql | 27 +++
16 files changed, 398 insertions(+), 68 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a186e35f00..607baa277e 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1669,6 +1669,16 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</thead>
<tbody>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>oid</structfield> <type>oid</type>
+ </para>
+ <para>
+ Row identifier
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>roleid</structfield> <type>oid</type>
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 6f43870779..2abd6b007a 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -262,6 +262,8 @@ IsSharedRelation(Oid relationId)
relationId == AuthIdRolnameIndexId ||
relationId == AuthMemMemRoleIndexId ||
relationId == AuthMemRoleMemIndexId ||
+ relationId == AuthMemOidIndexId ||
+ relationId == AuthMemGrantorIndexId ||
relationId == DatabaseNameIndexId ||
relationId == DatabaseOidIndexId ||
relationId == DbRoleSettingDatidRolidIndexId ||
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index e119674b1f..39768fa22b 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -28,6 +28,7 @@
#include "catalog/pg_amproc.h"
#include "catalog/pg_attrdef.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_cast.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
@@ -172,6 +173,7 @@ static const Oid object_classes[] = {
TSTemplateRelationId, /* OCLASS_TSTEMPLATE */
TSConfigRelationId, /* OCLASS_TSCONFIG */
AuthIdRelationId, /* OCLASS_ROLE */
+ AuthMemRelationId, /* OCLASS_ROLE_MEMBERSHIP */
DatabaseRelationId, /* OCLASS_DATABASE */
TableSpaceRelationId, /* OCLASS_TBLSPACE */
ForeignDataWrapperRelationId, /* OCLASS_FDW */
@@ -1502,6 +1504,7 @@ doDeletion(const ObjectAddress *object, int flags)
case OCLASS_DEFACL:
case OCLASS_EVENT_TRIGGER:
case OCLASS_TRANSFORM:
+ case OCLASS_ROLE_MEMBERSHIP:
DropObjectById(object);
break;
@@ -1529,9 +1532,8 @@ doDeletion(const ObjectAddress *object, int flags)
* Accepts the same flags as performDeletion (though currently only
* PERFORM_DELETION_CONCURRENTLY does anything).
*
- * We use LockRelation for relations, LockDatabaseObject for everything
- * else. Shared-across-databases objects are not currently supported
- * because no caller cares, but could be modified to use LockSharedObject.
+ * We use LockRelation for relations, and otherwise LockSharedObject or
+ * LockDatabaseObject as appropriate for the object type.
*/
void
AcquireDeletionLock(const ObjectAddress *object, int flags)
@@ -1549,6 +1551,9 @@ AcquireDeletionLock(const ObjectAddress *object, int flags)
else
LockRelationOid(object->objectId, AccessExclusiveLock);
}
+ else if (object->classId == AuthMemRelationId)
+ LockSharedObject(object->classId, object->objectId, 0,
+ AccessExclusiveLock);
else
{
/* assume we should lock the whole object not a sub-object */
@@ -2914,6 +2919,9 @@ getObjectClass(const ObjectAddress *object)
case AuthIdRelationId:
return OCLASS_ROLE;
+ case AuthMemRelationId:
+ return OCLASS_ROLE_MEMBERSHIP;
+
case DatabaseRelationId:
return OCLASS_DATABASE;
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 6080ff8f5f..cc80172113 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -27,6 +27,7 @@
#include "catalog/pg_amproc.h"
#include "catalog/pg_attrdef.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_cast.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
@@ -386,6 +387,20 @@ static const ObjectPropertyType ObjectProperty[] =
-1,
true
},
+ {
+ "role membership",
+ AuthMemRelationId,
+ AuthMemOidIndexId,
+ -1,
+ -1,
+ Anum_pg_auth_members_oid,
+ InvalidAttrNumber,
+ InvalidAttrNumber,
+ Anum_pg_auth_members_grantor,
+ InvalidAttrNumber,
+ -1,
+ true
+ },
{
"rule",
RewriteRelationId,
@@ -787,6 +802,10 @@ static const struct object_type_map
{
"role", OBJECT_ROLE
},
+ /* OCLASS_ROLE_MEMBERSHIP */
+ {
+ "role membership", -1 /* unmapped */
+ },
/* OCLASS_DATABASE */
{
"database", OBJECT_DATABASE
@@ -3644,6 +3663,48 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
break;
}
+ case OCLASS_ROLE_MEMBERSHIP:
+ {
+ Relation amDesc;
+ ScanKeyData skey[1];
+ SysScanDesc rcscan;
+ HeapTuple tup;
+ Form_pg_auth_members amForm;
+
+ amDesc = table_open(AuthMemRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_auth_members_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+
+ rcscan = systable_beginscan(amDesc, AuthMemOidIndexId, true,
+ NULL, 1, skey);
+
+ tup = systable_getnext(rcscan);
+
+ if (!HeapTupleIsValid(tup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for role membership %u",
+ object->objectId);
+
+ systable_endscan(rcscan);
+ table_close(amDesc, AccessShareLock);
+ break;
+ }
+
+ amForm = (Form_pg_auth_members) GETSTRUCT(tup);
+
+ appendStringInfo(&buffer, _("membership of role %s in role %s"),
+ GetUserNameFromId(amForm->member, false),
+ GetUserNameFromId(amForm->roleid, false));
+
+ systable_endscan(rcscan);
+ table_close(amDesc, AccessShareLock);
+ break;
+ }
+
case OCLASS_DATABASE:
{
char *datname;
@@ -4533,6 +4594,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
appendStringInfoString(&buffer, "role");
break;
+ case OCLASS_ROLE_MEMBERSHIP:
+ appendStringInfoString(&buffer, "role membership");
+ break;
+
case OCLASS_DATABASE:
appendStringInfoString(&buffer, "database");
break;
@@ -5476,6 +5541,49 @@ getObjectIdentityParts(const ObjectAddress *object,
break;
}
+ case OCLASS_ROLE_MEMBERSHIP:
+ {
+ Relation authMemDesc;
+ ScanKeyData skey[1];
+ SysScanDesc amscan;
+ HeapTuple tup;
+ Form_pg_auth_members amForm;
+
+ authMemDesc = table_open(AuthMemRelationId,
+ AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_auth_members_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+
+ amscan = systable_beginscan(authMemDesc, AuthMemOidIndexId, true,
+ NULL, 1, skey);
+
+ tup = systable_getnext(amscan);
+
+ if (!HeapTupleIsValid(tup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for pg_auth_members entry %u",
+ object->objectId);
+
+ systable_endscan(amscan);
+ table_close(authMemDesc, AccessShareLock);
+ break;
+ }
+
+ amForm = (Form_pg_auth_members) GETSTRUCT(tup);
+
+ appendStringInfo(&buffer, _("membership of role %s in role %s"),
+ GetUserNameFromId(amForm->member, false),
+ GetUserNameFromId(amForm->roleid, false));
+
+ systable_endscan(amscan);
+ table_close(authMemDesc, AccessShareLock);
+ break;
+ }
+
case OCLASS_DATABASE:
{
char *datname;
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index 3e8fa008b9..f2f227f887 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -22,6 +22,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_conversion.h"
#include "catalog/pg_database.h"
@@ -1364,11 +1365,6 @@ shdepDropOwned(List *roleids, DropBehavior behavior)
case SHARED_DEPENDENCY_INVALID:
elog(ERROR, "unexpected dependency type");
break;
- case SHARED_DEPENDENCY_ACL:
- RemoveRoleFromObjectACL(roleid,
- sdepForm->classid,
- sdepForm->objid);
- break;
case SHARED_DEPENDENCY_POLICY:
/*
@@ -1398,22 +1394,37 @@ shdepDropOwned(List *roleids, DropBehavior behavior)
add_exact_object_address(&obj, deleteobjs);
}
break;
+ case SHARED_DEPENDENCY_ACL:
+
+ /*
+ * Dependencies on role grants are recorded using
+ * SHARED_DEPENDENCY_ACL, but unlike a regular ACL list
+ * which stores all permissions for a particular object in
+ * a single ACL array, there's a separate catalog row for
+ * each grant - so removing the grant just means removing
+ * the entire row.
+ */
+ if (sdepForm->classid != AuthMemRelationId)
+ {
+ RemoveRoleFromObjectACL(roleid,
+ sdepForm->classid,
+ sdepForm->objid);
+ break;
+ }
+ /* FALLTHROUGH */
case SHARED_DEPENDENCY_OWNER:
- /* If a local object, save it for deletion below */
- if (sdepForm->dbid == MyDatabaseId)
+ /* Save it for deletion below */
+ obj.classId = sdepForm->classid;
+ obj.objectId = sdepForm->objid;
+ obj.objectSubId = sdepForm->objsubid;
+ /* as above */
+ AcquireDeletionLock(&obj, 0);
+ if (!systable_recheck_tuple(scan, tuple))
{
- obj.classId = sdepForm->classid;
- obj.objectId = sdepForm->objid;
- obj.objectSubId = sdepForm->objsubid;
- /* as above */
- AcquireDeletionLock(&obj, 0);
- if (!systable_recheck_tuple(scan, tuple))
- {
- ReleaseDeletionLock(&obj);
- break;
- }
- add_exact_object_address(&obj, deleteobjs);
+ ReleaseDeletionLock(&obj);
+ break;
}
+ add_exact_object_address(&obj, deleteobjs);
break;
}
}
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index f369b1fc14..5a844b63a1 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -53,7 +53,7 @@ CREATE VIEW pg_group AS
SELECT
rolname AS groname,
oid AS grosysid,
- ARRAY(SELECT member FROM pg_auth_members WHERE roleid = oid) AS grolist
+ ARRAY(SELECT member FROM pg_auth_members WHERE roleid = pg_authid.oid) AS grolist
FROM pg_authid
WHERE NOT rolcanlogin;
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 5456b8222b..55219bb097 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -650,6 +650,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
case OCLASS_TRIGGER:
case OCLASS_SCHEMA:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_FDW:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index eef3e5d56e..549e30da34 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -1015,6 +1015,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_PARAMETER_ACL:
/* no support for global objects */
return false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7fbee0c1f7..ce323a5987 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12636,6 +12636,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
case OCLASS_TSTEMPLATE:
case OCLASS_TSCONFIG:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_FDW:
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 5b24b6dcad..b7046981ac 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -904,6 +904,7 @@ DropRole(DropRoleStmt *stmt)
Relation pg_authid_rel,
pg_auth_members_rel;
ListCell *item;
+ List *role_addresses = NIL;
if (!have_createrole_privilege())
ereport(ERROR,
@@ -912,7 +913,7 @@ DropRole(DropRoleStmt *stmt)
/*
* Scan the pg_authid relation to find the Oid of the role(s) to be
- * deleted.
+ * deleted and perform prleliminary permissions and sanity checks.
*/
pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
pg_auth_members_rel = table_open(AuthMemRelationId, RowExclusiveLock);
@@ -925,10 +926,9 @@ DropRole(DropRoleStmt *stmt)
tmp_tuple;
Form_pg_authid roleform;
ScanKeyData scankey;
- char *detail;
- char *detail_log;
SysScanDesc sscan;
Oid roleid;
+ ObjectAddress *role_address;
if (rolspec->roletype != ROLESPEC_CSTRING)
ereport(ERROR,
@@ -984,34 +984,31 @@ DropRole(DropRoleStmt *stmt)
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
+ /* Don't leak the syscache tuple */
+ ReleaseSysCache(tuple);
+
/*
* Lock the role, so nobody can add dependencies to her while we drop
* her. We keep the lock until the end of transaction.
*/
LockSharedObject(AuthIdRelationId, roleid, 0, AccessExclusiveLock);
- /* Check for pg_shdepend entries depending on this role */
- if (checkSharedDependencies(AuthIdRelationId, roleid,
- &detail, &detail_log))
- ereport(ERROR,
- (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
- errmsg("role \"%s\" cannot be dropped because some objects depend on it",
- role),
- errdetail_internal("%s", detail),
- errdetail_log("%s", detail_log)));
-
- /*
- * Remove the role from the pg_authid table
- */
- CatalogTupleDelete(pg_authid_rel, &tuple->t_self);
-
- ReleaseSysCache(tuple);
-
/*
- * Remove role from the pg_auth_members table. We have to remove all
- * tuples that show it as either a role or a member.
+ * If there is a pg_auth_members entry that has one of the roles to be
+ * dropped as the roleid or member, it should be silently removed, but
+ * if there is a pg_auth_members entry that has one of the roles to be
+ * dropped as the grantor, the operation should fail.
+ *
+ * It's possible, however, that a single pg_auth_members entry could
+ * fall into multiple categories - e.g. the user could do "GRANT foo
+ * TO bar GRANTED BY baz" and then "DROP ROLE baz, bar". We want such
+ * an operation to succeed regardless of the order in which the
+ * to-be-dropped roles are passed to DROP ROLE.
*
- * XXX what about grantor entries? Maybe we should do one heap scan.
+ * To make that work, we remove all pg_auth_members entries that can
+ * be silently removed in this loop, and then below we'll make a
+ * second pass over the list of roles to be removed and check for any
+ * remaining dependencies.
*/
ScanKeyInit(&scankey,
Anum_pg_auth_members_roleid,
@@ -1023,6 +1020,11 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
+ Form_pg_auth_members authmem_form;
+
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
}
@@ -1038,22 +1040,16 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
+ Form_pg_auth_members authmem_form;
+
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
}
systable_endscan(sscan);
- /*
- * Remove any comments or security labels on this role.
- */
- DeleteSharedComments(roleid, AuthIdRelationId);
- DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
-
- /*
- * Remove settings for this role.
- */
- DropSetting(InvalidOid, roleid);
-
/*
* Advance command counter so that later iterations of this loop will
* see the changes already made. This is essential if, for example,
@@ -1064,6 +1060,72 @@ DropRole(DropRoleStmt *stmt)
* itself.)
*/
CommandCounterIncrement();
+
+ /* Looks tentatively OK, add it to the list. */
+ role_address = palloc(sizeof(ObjectAddress));
+ role_address->classId = AuthIdRelationId;
+ role_address->objectId = roleid;
+ role_address->objectSubId = 0;
+ role_addresses = lappend(role_addresses, role_address);
+ }
+
+ /*
+ * Second pass over the roles to be removed.
+ */
+ foreach(item, role_addresses)
+ {
+ ObjectAddress *role_address = lfirst(item);
+ Oid roleid = role_address->objectId;
+ HeapTuple tuple;
+ Form_pg_authid roleform;
+ char *detail;
+ char *detail_log;
+
+ /*
+ * Re-find the pg_authid tuple.
+ *
+ * Since we've taken a lock on the role OID, it shouldn't be possible
+ * for the tuple to have been deleted -- or for that matter updated --
+ * unless the user is manually modifying the system catalogs.
+ */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for role %u", roleid);
+ roleform = (Form_pg_authid) GETSTRUCT(tuple);
+
+ /*
+ * Check for pg_shdepend entries depending on this role.
+ *
+ * This needs to happen after we've completed removing any
+ * pg_auth_members entries that can be removed silently, in order to
+ * avoid spurious failures. See notes above for more details.
+ */
+ if (checkSharedDependencies(AuthIdRelationId, roleid,
+ &detail, &detail_log))
+ ereport(ERROR,
+ (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
+ errmsg("role \"%s\" cannot be dropped because some objects depend on it",
+ NameStr(roleform->rolname)),
+ errdetail_internal("%s", detail),
+ errdetail_log("%s", detail_log)));
+
+ /*
+ * Remove the role from the pg_authid table
+ */
+ CatalogTupleDelete(pg_authid_rel, &tuple->t_self);
+
+ ReleaseSysCache(tuple);
+
+ /*
+ * Remove any comments or security labels on this role.
+ */
+ DeleteSharedComments(roleid, AuthIdRelationId);
+ DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
+
+ /*
+ * Remove settings for this role.
+ */
+ DropSetting(InvalidOid, roleid);
}
/*
@@ -1436,6 +1498,7 @@ AddRoleMems(const char *rolename, Oid roleid,
Datum new_record[Natts_pg_auth_members] = {0};
bool new_record_nulls[Natts_pg_auth_members] = {0};
bool new_record_repl[Natts_pg_auth_members] = {0};
+ Form_pg_auth_members authmem_form;
/*
* pg_database_owner is never a role member. Lifting this restriction
@@ -1481,15 +1544,22 @@ AddRoleMems(const char *rolename, Oid roleid,
authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
ObjectIdGetDatum(roleid),
ObjectIdGetDatum(memberid));
- if (HeapTupleIsValid(authmem_tuple) &&
- (!admin_opt ||
- ((Form_pg_auth_members) GETSTRUCT(authmem_tuple))->admin_option))
+ if (!HeapTupleIsValid(authmem_tuple))
{
- ereport(NOTICE,
- (errmsg("role \"%s\" is already a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
- ReleaseSysCache(authmem_tuple);
- continue;
+ authmem_form = NULL;
+ }
+ else
+ {
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (!admin_opt || authmem_form->admin_option)
+ {
+ ereport(NOTICE,
+ (errmsg("role \"%s\" is already a member of role \"%s\"",
+ get_rolespec_name(memberRole), rolename)));
+ ReleaseSysCache(authmem_tuple);
+ continue;
+ }
}
/* Build a tuple to insert or update */
@@ -1506,13 +1576,41 @@ AddRoleMems(const char *rolename, Oid roleid,
new_record,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
+
+ if (authmem_form->grantor != grantorId)
+ {
+ Oid *oldmembers = palloc(sizeof(Oid));
+ Oid *newmembers = palloc(sizeof(Oid));
+
+ /* updateAclDependencies wants to pfree array inputs */
+ oldmembers[0] = authmem_form->grantor;
+ newmembers[0] = grantorId;
+
+ updateAclDependencies(AuthMemRelationId, authmem_form->oid,
+ 0, InvalidOid,
+ 1, oldmembers,
+ 1, newmembers);
+ }
+
ReleaseSysCache(authmem_tuple);
}
else
{
+ Oid objectId;
+ Oid *newmembers = palloc(sizeof(Oid));
+
+ objectId = GetNewObjectId();
+ new_record[Anum_pg_auth_members_oid - 1] = objectId;
tuple = heap_form_tuple(pg_authmem_dsc,
new_record, new_record_nulls);
CatalogTupleInsert(pg_authmem_rel, tuple);
+
+ /* updateAclDependencies wants to pfree array inputs */
+ newmembers[0] = grantorId;
+ updateAclDependencies(AuthMemRelationId, objectId,
+ 0, InvalidOid,
+ 0, NULL,
+ 1, newmembers);
}
/* CCI after each change, in case there are duplicates in list */
@@ -1579,6 +1677,7 @@ DelRoleMems(const char *rolename, Oid roleid,
RoleSpec *memberRole = lfirst(specitem);
Oid memberid = lfirst_oid(iditem);
HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
/*
* Find entry for this role/member
@@ -1594,9 +1693,16 @@ DelRoleMems(const char *rolename, Oid roleid,
continue;
}
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
if (!admin_opt)
{
- /* Remove the entry altogether */
+ /*
+ * Remove the entry altogether, after first removing its
+ * dependencies
+ */
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_authmem_rel, &authmem_tuple->t_self);
}
else
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index d027075a4c..0a1e072bef 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -112,6 +112,7 @@ typedef enum ObjectClass
OCLASS_TSTEMPLATE, /* pg_ts_template */
OCLASS_TSCONFIG, /* pg_ts_config */
OCLASS_ROLE, /* pg_authid */
+ OCLASS_ROLE_MEMBERSHIP, /* pg_auth_members */
OCLASS_DATABASE, /* pg_database */
OCLASS_TBLSPACE, /* pg_tablespace */
OCLASS_FDW, /* pg_foreign_data_wrapper */
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index 1bc027f133..c9d7697730 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -29,6 +29,7 @@
*/
CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(2843,AuthMemRelation_Rowtype_Id) BKI_SCHEMA_MACRO
{
+ Oid oid; /* oid */
Oid roleid BKI_LOOKUP(pg_authid); /* ID of a role */
Oid member BKI_LOOKUP(pg_authid); /* ID of a member of that role */
Oid grantor BKI_LOOKUP(pg_authid); /* who granted the membership */
@@ -42,7 +43,9 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
*/
typedef FormData_pg_auth_members *Form_pg_auth_members;
-DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_oid_index, 9385, AuthMemOidIndexId, on pg_auth_members using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops));
+DECLARE_INDEX(pg_auth_members_grantor_index, 9384, AuthMemGrantorIndexId, on pg_auth_members using btree(grantor oid_ops));
#endif /* PG_AUTH_MEMBERS_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..c2465d0f49 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -103,9 +103,21 @@ ERROR: role "regress_nosuch_recursive" does not exist
DROP ROLE regress_nosuch_admin_recursive;
ERROR: role "regress_nosuch_admin_recursive" does not exist
DROP ROLE regress_plainrole;
+-- fail, can't drop regress_createrole yet, due to outstanding grants
+DROP ROLE regress_createrole;
+ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_read_all_data in role pg_read_all_data
+privileges for membership of role regress_write_all_data in role pg_write_all_data
+privileges for membership of role regress_monitor in role pg_monitor
+privileges for membership of role regress_read_all_settings in role pg_read_all_settings
+privileges for membership of role regress_read_all_stats in role pg_read_all_stats
+privileges for membership of role regress_stat_scan_tables in role pg_stat_scan_tables
+privileges for membership of role regress_read_server_files in role pg_read_server_files
+privileges for membership of role regress_write_server_files in role pg_write_server_files
+privileges for membership of role regress_execute_server_program in role pg_execute_server_program
+privileges for membership of role regress_signal_backend in role pg_signal_backend
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
-DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -125,6 +137,8 @@ DROP ROLE regress_read_server_files;
DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
+-- ok, dropped the other roles first so this is ok now
+DROP ROLE regress_createrole;
-- fail, role still owns database objects
DROP ROLE regress_tenant;
ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index e10dd6f9ae..65b4a22ebc 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -33,6 +33,38 @@ CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- fail, dependency
+ERROR: role "regress_priv_user2" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user3 in role regress_priv_user1
+REASSIGN OWNED BY regress_priv_user2 TO regress_priv_user4;
+DROP ROLE regress_priv_user2; -- still fail, REASSIGN OWNED doesn't help
+ERROR: role "regress_priv_user2" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user3 in role regress_priv_user1
+DROP OWNED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- ok now, DROP OWNED does the job
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user4 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- should fail, dependency
+ERROR: role "regress_priv_user3" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user4 in role regress_priv_user1
+DROP ROLE regress_priv_user4; -- ok
+DROP ROLE regress_priv_user3; -- ok now
+GRANT regress_priv_user1 TO regress_priv_user5 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user6 GRANTED BY regress_priv_user5;
+DROP ROLE regress_priv_user5; -- should fail, dependency
+ERROR: role "regress_priv_user5" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user6 in role regress_priv_user1
+DROP ROLE regress_priv_user1, regress_priv_user5; -- ok, despite order
+-- recreate the roles we just dropped
+CREATE USER regress_priv_user1;
+CREATE USER regress_priv_user2;
+CREATE USER regress_priv_user3;
+CREATE USER regress_priv_user4;
+CREATE USER regress_priv_user5;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..b696628238 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -98,9 +98,11 @@ DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
+-- fail, can't drop regress_createrole yet, due to outstanding grants
+DROP ROLE regress_createrole;
+
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
-DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -121,6 +123,9 @@ DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
+-- ok, dropped the other roles first so this is ok now
+DROP ROLE regress_createrole;
+
-- fail, role still owns database objects
DROP ROLE regress_tenant;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 6d1fd3391a..66834e32a7 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -37,6 +37,33 @@ CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- fail, dependency
+REASSIGN OWNED BY regress_priv_user2 TO regress_priv_user4;
+DROP ROLE regress_priv_user2; -- still fail, REASSIGN OWNED doesn't help
+DROP OWNED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- ok now, DROP OWNED does the job
+
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user4 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- should fail, dependency
+DROP ROLE regress_priv_user4; -- ok
+DROP ROLE regress_priv_user3; -- ok now
+GRANT regress_priv_user1 TO regress_priv_user5 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user6 GRANTED BY regress_priv_user5;
+DROP ROLE regress_priv_user5; -- should fail, dependency
+DROP ROLE regress_priv_user1, regress_priv_user5; -- ok, despite order
+
+-- recreate the roles we just dropped
+CREATE USER regress_priv_user1;
+CREATE USER regress_priv_user2;
+CREATE USER regress_priv_user3;
+CREATE USER regress_priv_user4;
+CREATE USER regress_priv_user5;
+
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
--
2.24.3 (Apple Git-128)
v2-0002-Make-role-grant-system-more-consistent-with-other.patchapplication/octet-stream; name=v2-0002-Make-role-grant-system-more-consistent-with-other.patchDownload
From 16b7e3777425a02286e8fededeb3bb6ff08ed250 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 26 Jul 2022 12:02:25 -0400
Subject: [PATCH v2 2/2] Make role grant system more consistent with other
privileges.
Previously, membership of role A in role B could be recorded in the
catalog tables only once. This meant that a new grant of role A to
role B would overwrite the previous grant. For other object types, a
new grant of permission on an object - in this case role A - exists
along side the existing grant provided that the grantor is different.
Either grant can be revoked independently of the other, and
permissions remain so long as at least one grant remains. Make role
grants work similarly.
Previously, when granting membership in a role, the superuser could
specify any role whatsoever as the grantor, but for other object types,
the grantor of record must be either the owner of the object, or a
role that currently has privileges to perform a similar GRANT.
Implement the same scheme for role grants, treating the bootstrap
superuser as the role owner since roles do not have owners. This means
that attempting to revoke a grant, or admin option on a grant, can now
fail if there are dependent privileges, and that CASCADE can be used
to revoke these. It also means that you can't grant ADMIN OPTION on
a role back to a user who granted it directly or indirectly to you,
similar to how you can't give WITH GRANT OPTION on a privilege back
to a role which granted it directly or indirectly to you.
Previously, only the superuser could specify GRANTED BY with a user
other than the current user. Relax that rule to allow the grantor
to be any role whose privileges the current user posseses. This
doesn't improve compatibility with what we do for other object types,
where support for GRANTED BY is entirely vestigial, but it makes this
feature more usable and seems to make sense to change at the same time
we're changing related behaviors.
XXX: pg_dump might need to do something with dependencies.
---
doc/src/sgml/ref/grant.sgml | 6 +-
doc/src/sgml/ref/revoke.sgml | 8 +-
src/backend/commands/user.c | 533 +++++++++++++++++++---
src/backend/parser/gram.y | 2 +
src/backend/utils/adt/acl.c | 47 +-
src/backend/utils/cache/syscache.c | 8 +-
src/bin/pg_dump/pg_dumpall.c | 9 +-
src/include/catalog/pg_auth_members.h | 4 +-
src/include/utils/acl.h | 1 +
src/test/regress/expected/create_role.out | 16 +-
src/test/regress/expected/privileges.out | 62 ++-
src/test/regress/sql/create_role.sql | 7 +-
src/test/regress/sql/privileges.sql | 33 +-
13 files changed, 617 insertions(+), 119 deletions(-)
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index f744b05b55..4761826ab3 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -267,8 +267,8 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
<para>
If <literal>GRANTED BY</literal> is specified, the grant is recorded as
- having been done by the specified role. Only database superusers may
- use this option, except when it names the same role executing the command.
+ having been done by the specified role. A user can only attribute a grant
+ to another role if they possess the privileges of that role.
</para>
<para>
@@ -333,7 +333,7 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
owner of the affected object. In particular, privileges granted via
such a command will appear to have been granted by the object owner.
(For role membership, the membership appears to have been granted
- by the containing role itself.)
+ by the bootstrap superuser.)
</para>
<para>
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 62f1971036..ac40ab0b19 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -198,9 +198,6 @@ REVOKE [ ADMIN OPTION FOR ]
<para>
When revoking membership in a role, <literal>GRANT OPTION</literal> is instead
called <literal>ADMIN OPTION</literal>, but the behavior is similar.
- This form of the command also allows a <literal>GRANTED BY</literal>
- option, but that option is currently ignored (except for checking
- the existence of the named role).
Note also that this form of the command does not
allow the noise word <literal>GROUP</literal>
in <replaceable class="parameter">role_specification</replaceable>.
@@ -239,7 +236,10 @@ REVOKE [ ADMIN OPTION FOR ]
<para>
If a superuser chooses to issue a <command>GRANT</command> or <command>REVOKE</command>
command, the command is performed as though it were issued by the
- owner of the affected object. Since all privileges ultimately come
+ owner of the affected object. (Since roles do not have owners, in the
+ case of a <command>GRANT</command> of role membership, the command is
+ performed as though it were issued by the bootstrap superuser.)
+ Since all privileges ultimately come
from the object owner (possibly indirectly via chains of grant options),
it is possible for a superuser to revoke all privileges, but this might
require use of <literal>CASCADE</literal> as stated above.
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index b7046981ac..d7d6c0d4f3 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -35,10 +35,32 @@
#include "storage/lmgr.h"
#include "utils/acl.h"
#include "utils/builtins.h"
+#include "utils/catcache.h"
#include "utils/fmgroids.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
+/*
+ * Removing a role grant - or the admin option on it - might recurse to
+ * dependent grants. We use these values to reason about what would need to
+ * be done in such cases.
+ *
+ * RRG_NOOP indicates a grant that would not need to be altered by the
+ * operation.
+ *
+ * RRG_REMOVE_ADMIN_OPTION indicates a grant that would need to have
+ * admin_option set to false by the operation.
+ *
+ * RRG_DELETE_GRANT indicates a grant that would need to be removed entirely
+ * by the operation.
+ */
+typedef enum
+{
+ RRG_NOOP,
+ RRG_REMOVE_ADMIN_OPTION,
+ RRG_DELETE_GRANT
+} RevokeRoleGrantAction;
+
/* Potentially set by pg_upgrade_support functions */
Oid binary_upgrade_next_pg_authid_oid = InvalidOid;
@@ -54,7 +76,22 @@ static void AddRoleMems(const char *rolename, Oid roleid,
Oid grantorId, bool admin_opt);
static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
- bool admin_opt);
+ Oid grantorId, bool admin_opt, DropBehavior behavior);
+static Oid check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId,
+ bool is_grant);
+static RevokeRoleGrantAction *initialize_revoke_actions(CatCList *memlist);
+static bool plan_single_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions,
+ Oid member, Oid grantor,
+ bool revoke_admin_option_only,
+ DropBehavior behavior);
+static void plan_member_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions, Oid member);
+static void plan_recursive_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions,
+ int index,
+ bool revoke_admin_option_only,
+ DropBehavior behavior);
/* Check if current user has createrole privileges */
@@ -449,7 +486,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
AddRoleMems(oldrolename, oldroleid,
thisrole_list,
thisrole_oidlist,
- GetUserId(), false);
+ InvalidOid, false);
ReleaseSysCache(oldroletup);
}
@@ -461,10 +498,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
AddRoleMems(stmt->role, roleid,
adminmembers, roleSpecsToIds(adminmembers),
- GetUserId(), true);
+ InvalidOid, true);
AddRoleMems(stmt->role, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- GetUserId(), false);
+ InvalidOid, false);
/* Post creation hook for new role */
InvokeObjectPostCreateHook(AuthIdRelationId, roleid, 0);
@@ -798,11 +835,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
if (stmt->action == +1) /* add members to role */
AddRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- GetUserId(), false);
+ InvalidOid, false);
else if (stmt->action == -1) /* drop members from role */
DelRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- false);
+ InvalidOid, false, DROP_RESTRICT); /* XXX sketchy - hint
+ * may mislead */
}
/*
@@ -1020,7 +1058,7 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
- Form_pg_auth_members authmem_form;
+ Form_pg_auth_members authmem_form;
authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
deleteSharedDependencyRecordsFor(AuthMemRelationId,
@@ -1040,7 +1078,7 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
- Form_pg_auth_members authmem_form;
+ Form_pg_auth_members authmem_form;
authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
deleteSharedDependencyRecordsFor(AuthMemRelationId,
@@ -1289,7 +1327,7 @@ GrantRole(GrantRoleStmt *stmt)
if (stmt->grantor)
grantor = get_rolespec_oid(stmt->grantor, false);
else
- grantor = GetUserId();
+ grantor = InvalidOid;
grantee_ids = roleSpecsToIds(stmt->grantee_roles);
@@ -1323,7 +1361,7 @@ GrantRole(GrantRoleStmt *stmt)
else
DelRoleMems(rolename, roleid,
stmt->grantee_roles, grantee_ids,
- stmt->admin_opt);
+ grantor, stmt->admin_opt, stmt->behavior);
}
/*
@@ -1424,7 +1462,7 @@ roleSpecsToIds(List *memberNames)
* roleid: OID of role to add to
* memberSpecs: list of RoleSpec of roles to add (used only for error messages)
* memberIds: OIDs of roles to add
- * grantorId: who is granting the membership
+ * grantorId: who is granting the membership (InvalidOid if not set explicitly)
* admin_opt: granting admin option?
*/
static void
@@ -1436,6 +1474,7 @@ AddRoleMems(const char *rolename, Oid roleid,
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
+ Oid currentUserId = GetUserId();
Assert(list_length(memberSpecs) == list_length(memberIds));
@@ -1457,7 +1496,7 @@ AddRoleMems(const char *rolename, Oid roleid,
else
{
if (!have_createrole_privilege() &&
- !is_admin_of_role(grantorId, roleid))
+ !is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
@@ -1476,29 +1515,25 @@ AddRoleMems(const char *rolename, Oid roleid,
ereport(ERROR,
errmsg("role \"%s\" cannot have explicit members", rolename));
- /*
- * The role membership grantor of record has little significance at
- * present. Nonetheless, inasmuch as users might look to it for a crude
- * audit trail, let only superusers impute the grant to a third party.
- */
- if (grantorId != GetUserId() && !superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to set grantor")));
+ /* Validate grantor (and resolve implicit grantor if not specified). */
+ grantorId = check_role_grantor(currentUserId, roleid, grantorId, true);
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
+ /*
+ * Only allow changes to this role by one backend at a time, so that we
+ * can check integrity constraints like the lack of circular ADMIN OPTION
+ * grants without fear of race conditions.
+ */
+ LockSharedObject(AuthIdRelationId, roleid, 0,
+ ShareUpdateExclusiveLock);
+
+ /* Preliminary sanity checks. */
forboth(specitem, memberSpecs, iditem, memberIds)
{
RoleSpec *memberRole = lfirst_node(RoleSpec, specitem);
Oid memberid = lfirst_oid(iditem);
- HeapTuple authmem_tuple;
- HeapTuple tuple;
- Datum new_record[Natts_pg_auth_members] = {0};
- bool new_record_nulls[Natts_pg_auth_members] = {0};
- bool new_record_repl[Natts_pg_auth_members] = {0};
- Form_pg_auth_members authmem_form;
/*
* pg_database_owner is never a role member. Lifting this restriction
@@ -1536,14 +1571,94 @@ AddRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INVALID_GRANT_OPERATION),
errmsg("role \"%s\" is a member of role \"%s\"",
rolename, get_rolespec_name(memberRole))));
+ }
+
+ /*
+ * Disallow attempts to grant ADMIN OPTION back to a user who granted it
+ * to you, similar to what check_circularity does for ACLs. We want the
+ * chains of grants to remain acyclic, so that it's always possible to use
+ * REVOKE .. CASCADE to clean up all grants that depend on the one being
+ * revoked.
+ *
+ * NB: This check might look redundant with the check for membership
+ * loops above, but it isn't. That's checking for role-member loop (e.g.
+ * A is a member of B and B is a member of A) while this is checking for
+ * a member-grantor loop (e.g. A gave ADMIN OPTION to X to B and now B, who
+ * has no other source of ADMIN OPTION on X, tries to give ADMIN OPTION
+ * on X back to A).
+ */
+ if (admin_opt && grantorId != BOOTSTRAP_SUPERUSERID)
+ {
+ CatCList *memlist;
+ RevokeRoleGrantAction *actions;
+ int i;
+
+ /* Get the list of members for this role. */
+ memlist = SearchSysCacheList1(AUTHMEMROLEMEM,
+ ObjectIdGetDatum(roleid));
+
+ /*
+ * Figure out what would happen if we removed all existing grants to
+ * every role to which we've been asked to make a new grant.
+ */
+ actions = initialize_revoke_actions(memlist);
+ foreach(iditem, memberIds)
+ {
+ Oid memberid = lfirst_oid(iditem);
+
+ if (memberid == BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_GRANT_OPERATION),
+ errmsg("grants with admin options cannot be circular")));
+ plan_member_revoke(memlist, actions, memberid);
+ }
+
+ /*
+ * If the result would be that the grantor role would no longer have
+ * the ability to perform the grant, then the proposed grant would
+ * create a circularity.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (actions[i] == RRG_NOOP &&
+ authmem_form->member == grantorId &&
+ authmem_form->admin_option)
+ break;
+ }
+ if (i >= memlist->n_members)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_GRANT_OPERATION),
+ errmsg("admin options cannot be granted back to your own grantor")));
+
+ ReleaseSysCacheList(memlist);
+ }
+
+ /* Now perform the catalog updates. */
+ forboth(specitem, memberSpecs, iditem, memberIds)
+ {
+ RoleSpec *memberRole = lfirst_node(RoleSpec, specitem);
+ Oid memberid = lfirst_oid(iditem);
+ HeapTuple authmem_tuple;
+ HeapTuple tuple;
+ Datum new_record[Natts_pg_auth_members] = {0};
+ bool new_record_nulls[Natts_pg_auth_members] = {0};
+ bool new_record_repl[Natts_pg_auth_members] = {0};
+ Form_pg_auth_members authmem_form;
/*
* Check if entry for this role/member already exists; if so, give
* warning unless we are adding admin option.
*/
- authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
+ authmem_tuple = SearchSysCache3(AUTHMEMROLEMEM,
ObjectIdGetDatum(roleid),
- ObjectIdGetDatum(memberid));
+ ObjectIdGetDatum(memberid),
+ ObjectIdGetDatum(grantorId));
if (!HeapTupleIsValid(authmem_tuple))
{
authmem_form = NULL;
@@ -1555,8 +1670,9 @@ AddRoleMems(const char *rolename, Oid roleid,
if (!admin_opt || authmem_form->admin_option)
{
ereport(NOTICE,
- (errmsg("role \"%s\" is already a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
+ (errmsg("role \"%s\" has already been granted membership in role \"%s\" by role \"%s\"",
+ get_rolespec_name(memberRole), rolename,
+ GetUserNameFromId(grantorId, false))));
ReleaseSysCache(authmem_tuple);
continue;
}
@@ -1570,28 +1686,12 @@ AddRoleMems(const char *rolename, Oid roleid,
if (HeapTupleIsValid(authmem_tuple))
{
- new_record_repl[Anum_pg_auth_members_grantor - 1] = true;
new_record_repl[Anum_pg_auth_members_admin_option - 1] = true;
tuple = heap_modify_tuple(authmem_tuple, pg_authmem_dsc,
new_record,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
- if (authmem_form->grantor != grantorId)
- {
- Oid *oldmembers = palloc(sizeof(Oid));
- Oid *newmembers = palloc(sizeof(Oid));
-
- /* updateAclDependencies wants to pfree array inputs */
- oldmembers[0] = authmem_form->grantor;
- newmembers[0] = grantorId;
-
- updateAclDependencies(AuthMemRelationId, authmem_form->oid,
- 0, InvalidOid,
- 1, oldmembers,
- 1, newmembers);
- }
-
ReleaseSysCache(authmem_tuple);
}
else
@@ -1630,17 +1730,22 @@ AddRoleMems(const char *rolename, Oid roleid,
* roleid: OID of role to del from
* memberSpecs: list of RoleSpec of roles to del (used only for error messages)
* memberIds: OIDs of roles to del
+ * grantorId: who is revoking the membership
* admin_opt: remove admin option only?
*/
static void
DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
- bool admin_opt)
+ Oid grantorId, bool admin_opt, DropBehavior behavior)
{
Relation pg_authmem_rel;
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
+ Oid currentUserId = GetUserId();
+ CatCList *memlist;
+ RevokeRoleGrantAction *actions;
+ int i;
Assert(list_length(memberSpecs) == list_length(memberIds));
@@ -1662,40 +1767,69 @@ DelRoleMems(const char *rolename, Oid roleid,
else
{
if (!have_createrole_privilege() &&
- !is_admin_of_role(GetUserId(), roleid))
+ !is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
rolename)));
}
+ /* Validate grantor (and resolve implicit grantor if not specified). */
+ grantorId = check_role_grantor(currentUserId, roleid, grantorId, false);
+
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
+ /*
+ * Only allow changes to this role by one backend at a time, so that we
+ * can check for things like dependent privileges without fear of race
+ * conditions.
+ */
+ LockSharedObject(AuthIdRelationId, roleid, 0,
+ ShareUpdateExclusiveLock);
+
+ memlist = SearchSysCacheList1(AUTHMEMROLEMEM, ObjectIdGetDatum(roleid));
+ actions = initialize_revoke_actions(memlist);
+
+ /*
+ * We may need to recurse to dependent privileges if DROP_CASCADE was
+ * specified, or refuse to perform the operation if dependent privileges
+ * exist and DROP_RECURSE was specified. plan_single_revoke() will
+ * figure out what to do with each catalog tuple.
+ */
forboth(specitem, memberSpecs, iditem, memberIds)
{
RoleSpec *memberRole = lfirst(specitem);
Oid memberid = lfirst_oid(iditem);
- HeapTuple authmem_tuple;
- Form_pg_auth_members authmem_form;
- /*
- * Find entry for this role/member
- */
- authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
- ObjectIdGetDatum(roleid),
- ObjectIdGetDatum(memberid));
- if (!HeapTupleIsValid(authmem_tuple))
+ if (!plan_single_revoke(memlist, actions, memberid, grantorId,
+ admin_opt, behavior))
{
ereport(WARNING,
- (errmsg("role \"%s\" is not a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
+ (errmsg("role \"%s\" has not been granted membership in role \"%s\" by role \"%s\"",
+ get_rolespec_name(memberRole), rolename,
+ GetUserNameFromId(grantorId, false))));
continue;
}
+ }
+ /*
+ * We now know what to do with each catalog tuple: it should either be
+ * left alone, deleted, or just have the admin_option flag cleared.
+ * Perform the appropriate action in each case.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ if (actions[i] == RRG_NOOP)
+ continue;
+
+ authmem_tuple = &memlist->members[i]->tuple;
authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
- if (!admin_opt)
+ if (actions[i] == RRG_DELETE_GRANT)
{
/*
* Remove the entry altogether, after first removing its
@@ -1722,15 +1856,282 @@ DelRoleMems(const char *rolename, Oid roleid,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
}
-
- ReleaseSysCache(authmem_tuple);
-
- /* CCI after each change, in case there are duplicates in list */
- CommandCounterIncrement();
}
+ ReleaseSysCacheList(memlist);
+
/*
* Close pg_authmem, but keep lock till commit.
*/
table_close(pg_authmem_rel, NoLock);
}
+
+/*
+ * Sanity-check, or infer, the grantor for a GRANT or REVOKE statement
+ * targeting a role.
+ *
+ * The grantor must always be either a role with ADMIN OPTION on the role in
+ * which membership is being granted, or the bootstrap superuser. This is
+ * similar to the restriction enforced by select_best_grantor, except that
+ * roles don't have owners, so we regard the bootstrap superuser as the
+ * implicit owner.
+ *
+ * The return value is the OID to be regarded as the grantor when executing
+ * the operation.
+ */
+static Oid
+check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
+{
+ /* If the grantor ID was not specified, pick one to use. */
+ if (!OidIsValid(grantorId))
+ {
+ /*
+ * Grants where the grantor is recorded as the bootstrap superuser
+ * do not depend on any other existing grants, so always default to
+ * this interpretation when possible.
+ */
+ if (has_createrole_privilege(currentUserId))
+ return BOOTSTRAP_SUPERUSERID;
+
+ /*
+ * Otherwise, the grantor must either have ADMIN OPTION on the role
+ * or inherit the privileges of a role which does. In the former case,
+ * record the grantor as the current user; in the latter, pick one
+ * of the roles that is "most directly" inherited by the current role
+ * (i.e. fewest "hops").
+ *
+ * (We shouldn't fail to find a best grantor, because we've already
+ * established that the current user has permission to perform the
+ * operation.)
+ */
+ grantorId = select_best_admin(currentUserId, roleid);
+ if (!OidIsValid(grantorId))
+ elog(ERROR, "no possible grantors");
+ return grantorId;
+ }
+
+ /*
+ * If an explicit grantor is specified, it must be a role whose privileges
+ * the current user possesses.
+ *
+ * It should also be a role that has ADMIN OPTION on the target role, but
+ * we check this condition only in case of GRANT. For REVOKE, no matching
+ * grant should exist anyway, but if it somehow does, let the user get rid
+ * of it.
+ */
+ if (is_grant)
+ {
+ if (!has_privs_of_role(currentUserId, grantorId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to grant privileges as role \"%s\"",
+ GetUserNameFromId(grantorId, false))));
+
+ if (grantorId != BOOTSTRAP_SUPERUSERID &&
+ select_best_admin(grantorId, roleid) != grantorId)
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("grantor must have ADMIN OPTION on \"%s\"",
+ GetUserNameFromId(roleid, false))));
+ }
+ else
+ {
+ if (!has_privs_of_role(currentUserId, grantorId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to revoke privileges granted by role \"%s\"",
+ GetUserNameFromId(grantorId, false))));
+ }
+
+ /*
+ * If a grantor was specified explicitly, always attribute the grant to
+ * that role (unless we error out above).
+ */
+ return grantorId;
+}
+
+/*
+ * Initialize an array of RevokeRoleGrantAction objects.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * We here construct an array indicating that no actions are to be performed;
+ * that is, every element is intiially RRG_NOOP.
+ */
+static RevokeRoleGrantAction *
+initialize_revoke_actions(CatCList *memlist)
+{
+ RevokeRoleGrantAction *result;
+ int i;
+
+ if (memlist->n_members == 0)
+ return NULL;
+
+ result = palloc(sizeof(RevokeRoleGrantAction) * memlist->n_members);
+ for (i = 0; i < memlist->n_members; i++)
+ result[i] = RRG_NOOP;
+ return result;
+}
+
+/*
+ * Figure out what we would need to do in order to revoke a grant, or just the
+ * admin option on a grant, given that there might be dependent privileges.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * Whatever actions prove to be necessary will be signalled by updating
+ * 'actions'.
+ *
+ * If behavior is DROP_RESTRICT, an error will occur if there are dependent
+ * role membership grants; if DROP_CASCADE, those grants will be scheduled
+ * for deletion.
+ *
+ * The return value is true if the matching grant was found in the list,
+ * and false if not.
+ */
+static bool
+plan_single_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ Oid member, Oid grantor, bool revoke_admin_option_only,
+ DropBehavior behavior)
+{
+ int i;
+
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (authmem_form->member == member &&
+ authmem_form->grantor == grantor)
+ {
+ plan_recursive_revoke(memlist, actions, i,
+ revoke_admin_option_only, behavior);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Figure out what we would need to do in order to revoke all grants to
+ * a given member, given that there might be dependent privileges.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * Whatever actions prove to be necessary will be signalled by updating
+ * 'actions'.
+ */
+static void
+plan_member_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ Oid member)
+{
+ int i;
+
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (authmem_form->member == member)
+ plan_recursive_revoke(memlist, actions, i, false, DROP_CASCADE);
+ }
+}
+
+/*
+ * Workhorse for figuring out recursive revocation of role grants.
+ *
+ * This is similar to what recursive_revoke() does for ACLs.
+ */
+static void
+plan_recursive_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ int index,
+ bool revoke_admin_option_only, DropBehavior behavior)
+{
+ bool would_still_have_admin_option = false;
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+ int i;
+
+ /* If it's already been done, we can just return. */
+ if (actions[index] == RRG_DELETE_GRANT)
+ return;
+ if (actions[index] == RRG_REMOVE_ADMIN_OPTION &&
+ revoke_admin_option_only)
+ return;
+
+ /* Locate tuple data. */
+ authmem_tuple = &memlist->members[index]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ /*
+ * If the existing tuple does not have admin_option set, then we do not
+ * need to recurse. If we're just supposed to clear that bit we don't
+ * need to do anything at all; if we're supposed to remove the grant,
+ * we need to do something, but only to the tuple, and not any others.
+ */
+ if (!revoke_admin_option_only)
+ {
+ actions[index] = RRG_DELETE_GRANT;
+ if (!authmem_form->admin_option)
+ return;
+ }
+ else
+ {
+ if (!authmem_form->admin_option)
+ return;
+ actions[index] = RRG_REMOVE_ADMIN_OPTION;
+ }
+
+ /* Determine whether the member would still have ADMIN OPTION. */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple am_cascade_tuple;
+ Form_pg_auth_members am_cascade_form;
+
+ am_cascade_tuple = &memlist->members[i]->tuple;
+ am_cascade_form = (Form_pg_auth_members) GETSTRUCT(am_cascade_tuple);
+
+ if (am_cascade_form->member == authmem_form->member &&
+ am_cascade_form->admin_option && actions[i] == RRG_NOOP)
+ {
+ would_still_have_admin_option = true;
+ break;
+ }
+ }
+
+ /* If the member would still have ADMIN OPTION, we need not recurse. */
+ if (would_still_have_admin_option)
+ return;
+
+ /*
+ * Recurse to grants that are not yet slated for deletion which have this
+ * member as the grantor.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple am_cascade_tuple;
+ Form_pg_auth_members am_cascade_form;
+
+ am_cascade_tuple = &memlist->members[i]->tuple;
+ am_cascade_form = (Form_pg_auth_members) GETSTRUCT(am_cascade_tuple);
+
+ if (am_cascade_form->grantor == authmem_form->member &&
+ actions[i] != RRG_DELETE_GRANT)
+ {
+ if (behavior == DROP_RESTRICT)
+ ereport(ERROR,
+ (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
+ errmsg("dependent privileges exist"),
+ errhint("Use CASCADE to revoke them too.")));
+
+ plan_recursive_revoke(memlist, actions, i, false, behavior);
+ }
+ }
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f9037761f9..c8bd66dd54 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -7870,6 +7870,7 @@ RevokeRoleStmt:
n->admin_opt = false;
n->granted_roles = $2;
n->grantee_roles = $4;
+ n->grantor = $5;
n->behavior = $6;
$$ = (Node *) n;
}
@@ -7881,6 +7882,7 @@ RevokeRoleStmt:
n->admin_opt = true;
n->granted_roles = $5;
n->grantee_roles = $7;
+ n->grantor = $8;
n->behavior = $9;
$$ = (Node *) n;
}
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 6fa58dd8eb..3e045da31f 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4791,9 +4791,7 @@ has_rolinherit(Oid roleid)
* Get a list of roles that the specified roleid is a member of
*
* Type ROLERECURSE_PRIVS recurses only through roles that have rolinherit
- * set, while ROLERECURSE_MEMBERS recurses through all roles. This sets
- * *is_admin==true if and only if role "roleid" has an ADMIN OPTION membership
- * in role "admin_of".
+ * set, while ROLERECURSE_MEMBERS recurses through all roles.
*
* Since indirect membership testing is relatively expensive, we cache
* a list of memberships. Hence, the result is only guaranteed good until
@@ -4801,10 +4799,15 @@ has_rolinherit(Oid roleid)
*
* For the benefit of select_best_grantor, the result is defined to be
* in breadth-first order, ie, closer relationships earlier.
+ *
+ * If admin_of is not InvalidOid, this function sets *admin_role, either
+ * to the OID of the first role in the result list that directly possesses
+ * ADMIN OPTION on the role corresponding to admin_of, or to InvalidOid if
+ * there is no such role.
*/
static List *
roles_is_member_of(Oid roleid, enum RoleRecurseType type,
- Oid admin_of, bool *is_admin)
+ Oid admin_of, Oid *admin_role)
{
Oid dba;
List *roles_list;
@@ -4812,7 +4815,9 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
List *new_cached_roles;
MemoryContext oldctx;
- Assert(OidIsValid(admin_of) == PointerIsValid(is_admin));
+ Assert(OidIsValid(admin_of) == PointerIsValid(admin_role));
+ if (admin_role != NULL)
+ *admin_role = InvalidOid;
/* If cache is valid and ADMIN OPTION not sought, just return the list */
if (cached_role[type] == roleid && !OidIsValid(admin_of) &&
@@ -4873,8 +4878,8 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
*/
if (otherid == admin_of &&
((Form_pg_auth_members) GETSTRUCT(tup))->admin_option &&
- OidIsValid(admin_of))
- *is_admin = true;
+ OidIsValid(admin_of) && !OidIsValid(*admin_role))
+ *admin_role = memberid;
/*
* Even though there shouldn't be any loops in the membership
@@ -5014,7 +5019,7 @@ is_member_of_role_nosuper(Oid member, Oid role)
bool
is_admin_of_role(Oid member, Oid role)
{
- bool result = false;
+ Oid admin_role;
if (superuser_arg(member))
return true;
@@ -5023,8 +5028,30 @@ is_admin_of_role(Oid member, Oid role)
if (member == role)
return false;
- (void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &result);
- return result;
+ (void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &admin_role);
+ return OidIsValid(admin_role);
+}
+
+/*
+ * Find a role whose privileges "member" inherits which has ADMIN OPTION
+ * on "role", ignoring super-userness.
+ *
+ * There might be more than one such role; prefer one which involves fewer
+ * hops. That is, if member has ADMIN OPTION, prefer that over all other
+ * options; if not, prefer a role from which member inherits more directly
+ * over more indirect inheritance.
+ */
+Oid
+select_best_admin(Oid member, Oid role)
+{
+ Oid admin_role;
+
+ /* By policy, a role cannot have WITH ADMIN OPTION on itself. */
+ if (member == role)
+ return InvalidOid;
+
+ (void) roles_is_member_of(member, ROLERECURSE_PRIVS, role, &admin_role);
+ return admin_role;
}
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 1912b12146..eec644ec84 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -213,22 +213,22 @@ static const struct cachedesc cacheinfo[] = {
},
{AuthMemRelationId, /* AUTHMEMMEMROLE */
AuthMemMemRoleIndexId,
- 2,
+ 3,
{
Anum_pg_auth_members_member,
Anum_pg_auth_members_roleid,
- 0,
+ Anum_pg_auth_members_grantor,
0
},
8
},
{AuthMemRelationId, /* AUTHMEMROLEMEM */
AuthMemRoleMemIndexId,
- 2,
+ 3,
{
Anum_pg_auth_members_roleid,
Anum_pg_auth_members_member,
- 0,
+ Anum_pg_auth_members_grantor,
0
},
8
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 26d3d53809..b6c589c462 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -954,10 +954,13 @@ dumpRoleMembership(PGconn *conn)
fprintf(OPF, " WITH ADMIN OPTION");
/*
- * We don't track the grantor very carefully in the backend, so cope
- * with the possibility that it has been dropped.
+ * Previous versions of PostgreSQL didn't used to track the grantor
+ * very carefully in the backend, and the grantor could be any user
+ * even if they didn't have ADMIN OPTION on the role, or a user that
+ * no longer existed. To avoid dump and restore failures, don't dump
+ * the grantor when talking to an old server version.
*/
- if (!PQgetisnull(res, i, 3))
+ if (PQserverVersion(conn) >= 160000)
{
char *grantor = PQgetvalue(res, i, 3);
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index c9d7697730..e57ec4f810 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -44,8 +44,8 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
typedef FormData_pg_auth_members *Form_pg_auth_members;
DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_oid_index, 9385, AuthMemOidIndexId, on pg_auth_members using btree(oid oid_ops));
-DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
-DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops, grantor oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops, grantor oid_ops));
DECLARE_INDEX(pg_auth_members_grantor_index, 9384, AuthMemGrantorIndexId, on pg_auth_members using btree(grantor oid_ops));
#endif /* PG_AUTH_MEMBERS_H */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 48f7d72add..3d6411197c 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -212,6 +212,7 @@ extern bool has_privs_of_role(Oid member, Oid role);
extern bool is_member_of_role(Oid member, Oid role);
extern bool is_member_of_role_nosuper(Oid member, Oid role);
extern bool is_admin_of_role(Oid member, Oid role);
+extern Oid select_best_admin(Oid member, Oid role);
extern void check_is_member_of_role(Oid member, Oid role);
extern Oid get_role_oid(const char *rolename, bool missing_ok);
extern Oid get_role_oid_or_public(const char *rolename);
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index c2465d0f49..4e67d72760 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -103,21 +103,9 @@ ERROR: role "regress_nosuch_recursive" does not exist
DROP ROLE regress_nosuch_admin_recursive;
ERROR: role "regress_nosuch_admin_recursive" does not exist
DROP ROLE regress_plainrole;
--- fail, can't drop regress_createrole yet, due to outstanding grants
-DROP ROLE regress_createrole;
-ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
-DETAIL: privileges for membership of role regress_read_all_data in role pg_read_all_data
-privileges for membership of role regress_write_all_data in role pg_write_all_data
-privileges for membership of role regress_monitor in role pg_monitor
-privileges for membership of role regress_read_all_settings in role pg_read_all_settings
-privileges for membership of role regress_read_all_stats in role pg_read_all_stats
-privileges for membership of role regress_stat_scan_tables in role pg_stat_scan_tables
-privileges for membership of role regress_read_server_files in role pg_read_server_files
-privileges for membership of role regress_write_server_files in role pg_write_server_files
-privileges for membership of role regress_execute_server_program in role pg_execute_server_program
-privileges for membership of role regress_signal_backend in role pg_signal_backend
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
+DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -137,8 +125,6 @@ DROP ROLE regress_read_server_files;
DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
--- ok, dropped the other roles first so this is ok now
-DROP ROLE regress_createrole;
-- fail, role still owns database objects
DROP ROLE regress_tenant;
ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 65b4a22ebc..8f5072bbda 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -33,6 +33,54 @@ CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- circular ADMIN OPTION grants should be disallowed
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION GRANTED BY regress_priv_user2;
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION GRANTED BY regress_priv_user3;
+ERROR: admin options cannot be granted back to your own grantor
+-- need CASCADE to revoke grant or admin option if dependent grants exist
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2; -- fail
+ERROR: dependent privileges exist
+HINT: Use CASCADE to revoke them too.
+REVOKE regress_priv_user1 FROM regress_priv_user2; -- fail
+ERROR: dependent privileges exist
+HINT: Use CASCADE to revoke them too.
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ member | admin_option
+--------------------+--------------
+ regress_priv_user2 | t
+ regress_priv_user3 | t
+(2 rows)
+
+BEGIN;
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ member | admin_option
+--------------------+--------------
+ regress_priv_user2 | f
+(1 row)
+
+ROLLBACK;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ member | admin_option
+--------+--------------
+(0 rows)
+
+-- inferred grantor must be a role with ADMIN OPTION
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user2 TO regress_priv_user3;
+SET ROLE regress_priv_user3;
+GRANT regress_priv_user1 TO regress_priv_user4;
+SELECT grantor::regrole FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole and member = 'regress_priv_user4'::regrole;
+ grantor
+--------------------
+ regress_priv_user2
+(1 row)
+
+RESET ROLE;
+REVOKE regress_priv_user2 FROM regress_priv_user3;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
@@ -68,15 +116,17 @@ CREATE USER regress_priv_user5;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
+GRANT regress_priv_user9 TO regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
GRANT pg_read_all_settings TO regress_priv_user9 WITH ADMIN OPTION;
SET SESSION AUTHORIZATION regress_priv_user9;
GRANT pg_read_all_settings TO regress_priv_user10;
SET SESSION AUTHORIZATION regress_priv_user8;
-REVOKE pg_read_all_settings FROM regress_priv_user10;
+REVOKE pg_read_all_settings FROM regress_priv_user10 GRANTED BY regress_priv_user9;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user9;
REVOKE pg_read_all_settings FROM regress_priv_user9;
RESET SESSION AUTHORIZATION;
+REVOKE regress_priv_user9 FROM regress_priv_user8;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
SET ROLE pg_read_all_settings;
@@ -90,7 +140,7 @@ CREATE GROUP regress_priv_group1;
CREATE GROUP regress_priv_group2 WITH USER regress_priv_user1, regress_priv_user2;
ALTER GROUP regress_priv_group1 ADD USER regress_priv_user4;
ALTER GROUP regress_priv_group2 ADD USER regress_priv_user2; -- duplicate
-NOTICE: role "regress_priv_user2" is already a member of role "regress_priv_group2"
+NOTICE: role "regress_priv_user2" has already been granted membership in role "regress_priv_group2" by role "rhaas"
ALTER GROUP regress_priv_group2 DROP USER regress_priv_user2;
GRANT regress_priv_group2 TO regress_priv_user4 WITH ADMIN OPTION;
-- prepare non-leakproof function for later
@@ -99,9 +149,13 @@ CREATE FUNCTION leak(integer,integer) RETURNS boolean
LANGUAGE internal IMMUTABLE STRICT; -- but deliberately not LEAKPROOF
ALTER FUNCTION leak(integer,integer) OWNER TO regress_priv_user1;
-- test owner privileges
+GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY regress_priv_role; -- error, doesn't have ADMIN OPTION
+ERROR: grantor must have ADMIN OPTION on "regress_priv_role"
GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY CURRENT_ROLE;
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY foo; -- error
-REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- error
+ERROR: role "foo" does not exist
+REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- warning, noop
+WARNING: role "regress_priv_user1" has not been granted membership in role "regress_priv_role" by role "regress_priv_user2"
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_USER;
REVOKE regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_ROLE;
DROP ROLE regress_priv_role;
@@ -1746,7 +1800,7 @@ SET SESSION AUTHORIZATION regress_priv_user1;
GRANT regress_priv_group2 TO regress_priv_user5; -- fails: no ADMIN OPTION
ERROR: must have admin option on role "regress_priv_group2"
SELECT dogrant_ok(); -- ok: SECURITY DEFINER conveys ADMIN
-NOTICE: role "regress_priv_user5" is already a member of role "regress_priv_group2"
+NOTICE: role "regress_priv_user5" has already been granted membership in role "regress_priv_group2" by role "regress_priv_user4"
dogrant_ok
------------
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index b696628238..292dc08797 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -98,11 +98,9 @@ DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
--- fail, can't drop regress_createrole yet, due to outstanding grants
-DROP ROLE regress_createrole;
-
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
+DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -123,9 +121,6 @@ DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
--- ok, dropped the other roles first so this is ok now
-DROP ROLE regress_createrole;
-
-- fail, role still owns database objects
DROP ROLE regress_tenant;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 66834e32a7..034ebbbf94 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -37,6 +37,32 @@ CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- circular ADMIN OPTION grants should be disallowed
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION GRANTED BY regress_priv_user2;
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION GRANTED BY regress_priv_user3;
+
+-- need CASCADE to revoke grant or admin option if dependent grants exist
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2; -- fail
+REVOKE regress_priv_user1 FROM regress_priv_user2; -- fail
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+BEGIN;
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ROLLBACK;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+
+-- inferred grantor must be a role with ADMIN OPTION
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user2 TO regress_priv_user3;
+SET ROLE regress_priv_user3;
+GRANT regress_priv_user1 TO regress_priv_user4;
+SELECT grantor::regrole FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole and member = 'regress_priv_user4'::regrole;
+RESET ROLE;
+REVOKE regress_priv_user2 FROM regress_priv_user3;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
+
-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
@@ -67,6 +93,7 @@ CREATE USER regress_priv_user5;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
+GRANT regress_priv_user9 TO regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
GRANT pg_read_all_settings TO regress_priv_user9 WITH ADMIN OPTION;
@@ -75,11 +102,12 @@ SET SESSION AUTHORIZATION regress_priv_user9;
GRANT pg_read_all_settings TO regress_priv_user10;
SET SESSION AUTHORIZATION regress_priv_user8;
-REVOKE pg_read_all_settings FROM regress_priv_user10;
+REVOKE pg_read_all_settings FROM regress_priv_user10 GRANTED BY regress_priv_user9;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user9;
REVOKE pg_read_all_settings FROM regress_priv_user9;
RESET SESSION AUTHORIZATION;
+REVOKE regress_priv_user9 FROM regress_priv_user8;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
@@ -110,9 +138,10 @@ ALTER FUNCTION leak(integer,integer) OWNER TO regress_priv_user1;
-- test owner privileges
+GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY regress_priv_role; -- error, doesn't have ADMIN OPTION
GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY CURRENT_ROLE;
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY foo; -- error
-REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- error
+REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- warning, noop
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_USER;
REVOKE regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_ROLE;
DROP ROLE regress_priv_role;
--
2.24.3 (Apple Git-128)
On Tue, Jul 26, 2022 at 12:46 PM Robert Haas <robertmhaas@gmail.com> wrote:
I believe that these patches are mostly complete, but I think that
dumpRoleMembership() probably needs some more work. I don't know what
exactly, but there's nothing to cause it to dump the role grants in an
order that will create dependent grants after the things that they
depend on, which seems essential.
OK, so I fixed that, and also updated the documentation a bit more. I
think these patches are basically done, and I'd like to get them
committed before too much more time goes by, because I have other
things that depend on this which I also want to get done for this
release. Anybody object?
I'm hoping not, because, while this is a behavior change, the current
state of play in this area is just terrible. To my knowledge, this is
the only place in the system where we allow a dangling OID reference
in a catalog table to persist after the object to which it refers has
been dropped. I believe it's also the object type where multiple
grants by different grantors aren't tracked separately, and where the
grantor need not themselves have the permission being granted. It
doesn't really look like any of these things were intentional behavior
so much as just ... nobody ever bothered to write the code to make it
work properly. I'm hoping the fact that I have now done that will be
viewed as a good thing, but maybe that won't turn out to be the case.
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v3-0001-Ensure-that-pg_auth_members.grantor-is-always-val.patchapplication/octet-stream; name=v3-0001-Ensure-that-pg_auth_members.grantor-is-always-val.patchDownload
From 217525c6d2eba2f7eb46ed50d37ee12f4809d3eb Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Fri, 24 Jun 2022 15:43:13 -0400
Subject: [PATCH v3 1/2] Ensure that pg_auth_members.grantor is always valid.
Previously, "GRANT foo TO bar" or "GRANT foo TO bar GRANTED BY baz"
would record the OID of the grantor in pg_auth_members.grantor, but
that role could later be dropped without modifying or removing the
pg_auth_members record. That's not great, because we typically try
to avoid dangling references in catalog data.
Now, a role grant depends on the grantor, and the grantor can't be
dropped without removing the grant or changing the grantor. "DROP
OWNED BY" will remove the grant, just as it does for other kinds of
privileges. "REASSIGN OWNED BY" will not, again just like what we do
in other cases involving privileges.
pg_auth_members now has an OID column, because that is needed in order
for dependencies to work. It also now has an index on the grantor
column, because otherwise dropping a role would require a sequential
scan of the entire table to see whether the role's OID is in use as
a grantor. That probably wouldn't be too large a problem in practice,
but it seems better to have an index just in case.
---
doc/src/sgml/catalogs.sgml | 9 +
src/backend/catalog/catalog.c | 2 +
src/backend/catalog/dependency.c | 14 +-
src/backend/catalog/objectaddress.c | 108 ++++++++++++
src/backend/catalog/pg_shdepend.c | 47 ++++--
src/backend/catalog/system_views.sql | 2 +-
src/backend/commands/alter.c | 1 +
src/backend/commands/event_trigger.c | 1 +
src/backend/commands/tablecmds.c | 1 +
src/backend/commands/user.c | 192 +++++++++++++++++-----
src/include/catalog/dependency.h | 1 +
src/include/catalog/pg_auth_members.h | 5 +-
src/test/regress/expected/create_role.out | 16 +-
src/test/regress/expected/privileges.out | 32 ++++
src/test/regress/sql/create_role.sql | 7 +-
src/test/regress/sql/privileges.sql | 27 +++
16 files changed, 397 insertions(+), 68 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index a186e35f00..adb150f685 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1669,6 +1669,15 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</thead>
<tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>oid</structfield> <type>oid</type>
+ </para>
+ <para>
+ Row identifier
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>roleid</structfield> <type>oid</type>
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 6f43870779..2abd6b007a 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -262,6 +262,8 @@ IsSharedRelation(Oid relationId)
relationId == AuthIdRolnameIndexId ||
relationId == AuthMemMemRoleIndexId ||
relationId == AuthMemRoleMemIndexId ||
+ relationId == AuthMemOidIndexId ||
+ relationId == AuthMemGrantorIndexId ||
relationId == DatabaseNameIndexId ||
relationId == DatabaseOidIndexId ||
relationId == DbRoleSettingDatidRolidIndexId ||
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index e119674b1f..39768fa22b 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -28,6 +28,7 @@
#include "catalog/pg_amproc.h"
#include "catalog/pg_attrdef.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_cast.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
@@ -172,6 +173,7 @@ static const Oid object_classes[] = {
TSTemplateRelationId, /* OCLASS_TSTEMPLATE */
TSConfigRelationId, /* OCLASS_TSCONFIG */
AuthIdRelationId, /* OCLASS_ROLE */
+ AuthMemRelationId, /* OCLASS_ROLE_MEMBERSHIP */
DatabaseRelationId, /* OCLASS_DATABASE */
TableSpaceRelationId, /* OCLASS_TBLSPACE */
ForeignDataWrapperRelationId, /* OCLASS_FDW */
@@ -1502,6 +1504,7 @@ doDeletion(const ObjectAddress *object, int flags)
case OCLASS_DEFACL:
case OCLASS_EVENT_TRIGGER:
case OCLASS_TRANSFORM:
+ case OCLASS_ROLE_MEMBERSHIP:
DropObjectById(object);
break;
@@ -1529,9 +1532,8 @@ doDeletion(const ObjectAddress *object, int flags)
* Accepts the same flags as performDeletion (though currently only
* PERFORM_DELETION_CONCURRENTLY does anything).
*
- * We use LockRelation for relations, LockDatabaseObject for everything
- * else. Shared-across-databases objects are not currently supported
- * because no caller cares, but could be modified to use LockSharedObject.
+ * We use LockRelation for relations, and otherwise LockSharedObject or
+ * LockDatabaseObject as appropriate for the object type.
*/
void
AcquireDeletionLock(const ObjectAddress *object, int flags)
@@ -1549,6 +1551,9 @@ AcquireDeletionLock(const ObjectAddress *object, int flags)
else
LockRelationOid(object->objectId, AccessExclusiveLock);
}
+ else if (object->classId == AuthMemRelationId)
+ LockSharedObject(object->classId, object->objectId, 0,
+ AccessExclusiveLock);
else
{
/* assume we should lock the whole object not a sub-object */
@@ -2914,6 +2919,9 @@ getObjectClass(const ObjectAddress *object)
case AuthIdRelationId:
return OCLASS_ROLE;
+ case AuthMemRelationId:
+ return OCLASS_ROLE_MEMBERSHIP;
+
case DatabaseRelationId:
return OCLASS_DATABASE;
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 6080ff8f5f..cc80172113 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -27,6 +27,7 @@
#include "catalog/pg_amproc.h"
#include "catalog/pg_attrdef.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_cast.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
@@ -386,6 +387,20 @@ static const ObjectPropertyType ObjectProperty[] =
-1,
true
},
+ {
+ "role membership",
+ AuthMemRelationId,
+ AuthMemOidIndexId,
+ -1,
+ -1,
+ Anum_pg_auth_members_oid,
+ InvalidAttrNumber,
+ InvalidAttrNumber,
+ Anum_pg_auth_members_grantor,
+ InvalidAttrNumber,
+ -1,
+ true
+ },
{
"rule",
RewriteRelationId,
@@ -787,6 +802,10 @@ static const struct object_type_map
{
"role", OBJECT_ROLE
},
+ /* OCLASS_ROLE_MEMBERSHIP */
+ {
+ "role membership", -1 /* unmapped */
+ },
/* OCLASS_DATABASE */
{
"database", OBJECT_DATABASE
@@ -3644,6 +3663,48 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
break;
}
+ case OCLASS_ROLE_MEMBERSHIP:
+ {
+ Relation amDesc;
+ ScanKeyData skey[1];
+ SysScanDesc rcscan;
+ HeapTuple tup;
+ Form_pg_auth_members amForm;
+
+ amDesc = table_open(AuthMemRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_auth_members_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+
+ rcscan = systable_beginscan(amDesc, AuthMemOidIndexId, true,
+ NULL, 1, skey);
+
+ tup = systable_getnext(rcscan);
+
+ if (!HeapTupleIsValid(tup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for role membership %u",
+ object->objectId);
+
+ systable_endscan(rcscan);
+ table_close(amDesc, AccessShareLock);
+ break;
+ }
+
+ amForm = (Form_pg_auth_members) GETSTRUCT(tup);
+
+ appendStringInfo(&buffer, _("membership of role %s in role %s"),
+ GetUserNameFromId(amForm->member, false),
+ GetUserNameFromId(amForm->roleid, false));
+
+ systable_endscan(rcscan);
+ table_close(amDesc, AccessShareLock);
+ break;
+ }
+
case OCLASS_DATABASE:
{
char *datname;
@@ -4533,6 +4594,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
appendStringInfoString(&buffer, "role");
break;
+ case OCLASS_ROLE_MEMBERSHIP:
+ appendStringInfoString(&buffer, "role membership");
+ break;
+
case OCLASS_DATABASE:
appendStringInfoString(&buffer, "database");
break;
@@ -5476,6 +5541,49 @@ getObjectIdentityParts(const ObjectAddress *object,
break;
}
+ case OCLASS_ROLE_MEMBERSHIP:
+ {
+ Relation authMemDesc;
+ ScanKeyData skey[1];
+ SysScanDesc amscan;
+ HeapTuple tup;
+ Form_pg_auth_members amForm;
+
+ authMemDesc = table_open(AuthMemRelationId,
+ AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_auth_members_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+
+ amscan = systable_beginscan(authMemDesc, AuthMemOidIndexId, true,
+ NULL, 1, skey);
+
+ tup = systable_getnext(amscan);
+
+ if (!HeapTupleIsValid(tup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for pg_auth_members entry %u",
+ object->objectId);
+
+ systable_endscan(amscan);
+ table_close(authMemDesc, AccessShareLock);
+ break;
+ }
+
+ amForm = (Form_pg_auth_members) GETSTRUCT(tup);
+
+ appendStringInfo(&buffer, _("membership of role %s in role %s"),
+ GetUserNameFromId(amForm->member, false),
+ GetUserNameFromId(amForm->roleid, false));
+
+ systable_endscan(amscan);
+ table_close(authMemDesc, AccessShareLock);
+ break;
+ }
+
case OCLASS_DATABASE:
{
char *datname;
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index 3e8fa008b9..f2f227f887 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -22,6 +22,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_conversion.h"
#include "catalog/pg_database.h"
@@ -1364,11 +1365,6 @@ shdepDropOwned(List *roleids, DropBehavior behavior)
case SHARED_DEPENDENCY_INVALID:
elog(ERROR, "unexpected dependency type");
break;
- case SHARED_DEPENDENCY_ACL:
- RemoveRoleFromObjectACL(roleid,
- sdepForm->classid,
- sdepForm->objid);
- break;
case SHARED_DEPENDENCY_POLICY:
/*
@@ -1398,22 +1394,37 @@ shdepDropOwned(List *roleids, DropBehavior behavior)
add_exact_object_address(&obj, deleteobjs);
}
break;
+ case SHARED_DEPENDENCY_ACL:
+
+ /*
+ * Dependencies on role grants are recorded using
+ * SHARED_DEPENDENCY_ACL, but unlike a regular ACL list
+ * which stores all permissions for a particular object in
+ * a single ACL array, there's a separate catalog row for
+ * each grant - so removing the grant just means removing
+ * the entire row.
+ */
+ if (sdepForm->classid != AuthMemRelationId)
+ {
+ RemoveRoleFromObjectACL(roleid,
+ sdepForm->classid,
+ sdepForm->objid);
+ break;
+ }
+ /* FALLTHROUGH */
case SHARED_DEPENDENCY_OWNER:
- /* If a local object, save it for deletion below */
- if (sdepForm->dbid == MyDatabaseId)
+ /* Save it for deletion below */
+ obj.classId = sdepForm->classid;
+ obj.objectId = sdepForm->objid;
+ obj.objectSubId = sdepForm->objsubid;
+ /* as above */
+ AcquireDeletionLock(&obj, 0);
+ if (!systable_recheck_tuple(scan, tuple))
{
- obj.classId = sdepForm->classid;
- obj.objectId = sdepForm->objid;
- obj.objectSubId = sdepForm->objsubid;
- /* as above */
- AcquireDeletionLock(&obj, 0);
- if (!systable_recheck_tuple(scan, tuple))
- {
- ReleaseDeletionLock(&obj);
- break;
- }
- add_exact_object_address(&obj, deleteobjs);
+ ReleaseDeletionLock(&obj);
+ break;
}
+ add_exact_object_address(&obj, deleteobjs);
break;
}
}
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index f369b1fc14..5a844b63a1 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -53,7 +53,7 @@ CREATE VIEW pg_group AS
SELECT
rolname AS groname,
oid AS grosysid,
- ARRAY(SELECT member FROM pg_auth_members WHERE roleid = oid) AS grolist
+ ARRAY(SELECT member FROM pg_auth_members WHERE roleid = pg_authid.oid) AS grolist
FROM pg_authid
WHERE NOT rolcanlogin;
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 5456b8222b..55219bb097 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -650,6 +650,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
case OCLASS_TRIGGER:
case OCLASS_SCHEMA:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_FDW:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index eef3e5d56e..549e30da34 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -1015,6 +1015,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_PARAMETER_ACL:
/* no support for global objects */
return false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 7fbee0c1f7..ce323a5987 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12636,6 +12636,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
case OCLASS_TSTEMPLATE:
case OCLASS_TSCONFIG:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_FDW:
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 94135fdd6b..258943094a 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -911,6 +911,7 @@ DropRole(DropRoleStmt *stmt)
Relation pg_authid_rel,
pg_auth_members_rel;
ListCell *item;
+ List *role_addresses = NIL;
if (!have_createrole_privilege())
ereport(ERROR,
@@ -919,7 +920,7 @@ DropRole(DropRoleStmt *stmt)
/*
* Scan the pg_authid relation to find the Oid of the role(s) to be
- * deleted.
+ * deleted and perform prleliminary permissions and sanity checks.
*/
pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
pg_auth_members_rel = table_open(AuthMemRelationId, RowExclusiveLock);
@@ -932,10 +933,9 @@ DropRole(DropRoleStmt *stmt)
tmp_tuple;
Form_pg_authid roleform;
ScanKeyData scankey;
- char *detail;
- char *detail_log;
SysScanDesc sscan;
Oid roleid;
+ ObjectAddress *role_address;
if (rolspec->roletype != ROLESPEC_CSTRING)
ereport(ERROR,
@@ -991,34 +991,31 @@ DropRole(DropRoleStmt *stmt)
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
+ /* Don't leak the syscache tuple */
+ ReleaseSysCache(tuple);
+
/*
* Lock the role, so nobody can add dependencies to her while we drop
* her. We keep the lock until the end of transaction.
*/
LockSharedObject(AuthIdRelationId, roleid, 0, AccessExclusiveLock);
- /* Check for pg_shdepend entries depending on this role */
- if (checkSharedDependencies(AuthIdRelationId, roleid,
- &detail, &detail_log))
- ereport(ERROR,
- (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
- errmsg("role \"%s\" cannot be dropped because some objects depend on it",
- role),
- errdetail_internal("%s", detail),
- errdetail_log("%s", detail_log)));
-
- /*
- * Remove the role from the pg_authid table
- */
- CatalogTupleDelete(pg_authid_rel, &tuple->t_self);
-
- ReleaseSysCache(tuple);
-
/*
- * Remove role from the pg_auth_members table. We have to remove all
- * tuples that show it as either a role or a member.
+ * If there is a pg_auth_members entry that has one of the roles to be
+ * dropped as the roleid or member, it should be silently removed, but
+ * if there is a pg_auth_members entry that has one of the roles to be
+ * dropped as the grantor, the operation should fail.
+ *
+ * It's possible, however, that a single pg_auth_members entry could
+ * fall into multiple categories - e.g. the user could do "GRANT foo
+ * TO bar GRANTED BY baz" and then "DROP ROLE baz, bar". We want such
+ * an operation to succeed regardless of the order in which the
+ * to-be-dropped roles are passed to DROP ROLE.
*
- * XXX what about grantor entries? Maybe we should do one heap scan.
+ * To make that work, we remove all pg_auth_members entries that can
+ * be silently removed in this loop, and then below we'll make a
+ * second pass over the list of roles to be removed and check for any
+ * remaining dependencies.
*/
ScanKeyInit(&scankey,
Anum_pg_auth_members_roleid,
@@ -1030,6 +1027,11 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
+ Form_pg_auth_members authmem_form;
+
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
}
@@ -1045,22 +1047,16 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
+ Form_pg_auth_members authmem_form;
+
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
}
systable_endscan(sscan);
- /*
- * Remove any comments or security labels on this role.
- */
- DeleteSharedComments(roleid, AuthIdRelationId);
- DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
-
- /*
- * Remove settings for this role.
- */
- DropSetting(InvalidOid, roleid);
-
/*
* Advance command counter so that later iterations of this loop will
* see the changes already made. This is essential if, for example,
@@ -1071,6 +1067,72 @@ DropRole(DropRoleStmt *stmt)
* itself.)
*/
CommandCounterIncrement();
+
+ /* Looks tentatively OK, add it to the list. */
+ role_address = palloc(sizeof(ObjectAddress));
+ role_address->classId = AuthIdRelationId;
+ role_address->objectId = roleid;
+ role_address->objectSubId = 0;
+ role_addresses = lappend(role_addresses, role_address);
+ }
+
+ /*
+ * Second pass over the roles to be removed.
+ */
+ foreach(item, role_addresses)
+ {
+ ObjectAddress *role_address = lfirst(item);
+ Oid roleid = role_address->objectId;
+ HeapTuple tuple;
+ Form_pg_authid roleform;
+ char *detail;
+ char *detail_log;
+
+ /*
+ * Re-find the pg_authid tuple.
+ *
+ * Since we've taken a lock on the role OID, it shouldn't be possible
+ * for the tuple to have been deleted -- or for that matter updated --
+ * unless the user is manually modifying the system catalogs.
+ */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for role %u", roleid);
+ roleform = (Form_pg_authid) GETSTRUCT(tuple);
+
+ /*
+ * Check for pg_shdepend entries depending on this role.
+ *
+ * This needs to happen after we've completed removing any
+ * pg_auth_members entries that can be removed silently, in order to
+ * avoid spurious failures. See notes above for more details.
+ */
+ if (checkSharedDependencies(AuthIdRelationId, roleid,
+ &detail, &detail_log))
+ ereport(ERROR,
+ (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
+ errmsg("role \"%s\" cannot be dropped because some objects depend on it",
+ NameStr(roleform->rolname)),
+ errdetail_internal("%s", detail),
+ errdetail_log("%s", detail_log)));
+
+ /*
+ * Remove the role from the pg_authid table
+ */
+ CatalogTupleDelete(pg_authid_rel, &tuple->t_self);
+
+ ReleaseSysCache(tuple);
+
+ /*
+ * Remove any comments or security labels on this role.
+ */
+ DeleteSharedComments(roleid, AuthIdRelationId);
+ DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
+
+ /*
+ * Remove settings for this role.
+ */
+ DropSetting(InvalidOid, roleid);
}
/*
@@ -1443,6 +1505,7 @@ AddRoleMems(const char *rolename, Oid roleid,
Datum new_record[Natts_pg_auth_members] = {0};
bool new_record_nulls[Natts_pg_auth_members] = {0};
bool new_record_repl[Natts_pg_auth_members] = {0};
+ Form_pg_auth_members authmem_form;
/*
* pg_database_owner is never a role member. Lifting this restriction
@@ -1488,15 +1551,22 @@ AddRoleMems(const char *rolename, Oid roleid,
authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
ObjectIdGetDatum(roleid),
ObjectIdGetDatum(memberid));
- if (HeapTupleIsValid(authmem_tuple) &&
- (!admin_opt ||
- ((Form_pg_auth_members) GETSTRUCT(authmem_tuple))->admin_option))
+ if (!HeapTupleIsValid(authmem_tuple))
{
- ereport(NOTICE,
- (errmsg("role \"%s\" is already a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
- ReleaseSysCache(authmem_tuple);
- continue;
+ authmem_form = NULL;
+ }
+ else
+ {
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (!admin_opt || authmem_form->admin_option)
+ {
+ ereport(NOTICE,
+ (errmsg("role \"%s\" is already a member of role \"%s\"",
+ get_rolespec_name(memberRole), rolename)));
+ ReleaseSysCache(authmem_tuple);
+ continue;
+ }
}
/* Build a tuple to insert or update */
@@ -1513,13 +1583,41 @@ AddRoleMems(const char *rolename, Oid roleid,
new_record,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
+
+ if (authmem_form->grantor != grantorId)
+ {
+ Oid *oldmembers = palloc(sizeof(Oid));
+ Oid *newmembers = palloc(sizeof(Oid));
+
+ /* updateAclDependencies wants to pfree array inputs */
+ oldmembers[0] = authmem_form->grantor;
+ newmembers[0] = grantorId;
+
+ updateAclDependencies(AuthMemRelationId, authmem_form->oid,
+ 0, InvalidOid,
+ 1, oldmembers,
+ 1, newmembers);
+ }
+
ReleaseSysCache(authmem_tuple);
}
else
{
+ Oid objectId;
+ Oid *newmembers = palloc(sizeof(Oid));
+
+ objectId = GetNewObjectId();
+ new_record[Anum_pg_auth_members_oid - 1] = objectId;
tuple = heap_form_tuple(pg_authmem_dsc,
new_record, new_record_nulls);
CatalogTupleInsert(pg_authmem_rel, tuple);
+
+ /* updateAclDependencies wants to pfree array inputs */
+ newmembers[0] = grantorId;
+ updateAclDependencies(AuthMemRelationId, objectId,
+ 0, InvalidOid,
+ 0, NULL,
+ 1, newmembers);
}
/* CCI after each change, in case there are duplicates in list */
@@ -1586,6 +1684,7 @@ DelRoleMems(const char *rolename, Oid roleid,
RoleSpec *memberRole = lfirst(specitem);
Oid memberid = lfirst_oid(iditem);
HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
/*
* Find entry for this role/member
@@ -1601,9 +1700,16 @@ DelRoleMems(const char *rolename, Oid roleid,
continue;
}
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
if (!admin_opt)
{
- /* Remove the entry altogether */
+ /*
+ * Remove the entry altogether, after first removing its
+ * dependencies
+ */
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_authmem_rel, &authmem_tuple->t_self);
}
else
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index d027075a4c..0a1e072bef 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -112,6 +112,7 @@ typedef enum ObjectClass
OCLASS_TSTEMPLATE, /* pg_ts_template */
OCLASS_TSCONFIG, /* pg_ts_config */
OCLASS_ROLE, /* pg_authid */
+ OCLASS_ROLE_MEMBERSHIP, /* pg_auth_members */
OCLASS_DATABASE, /* pg_database */
OCLASS_TBLSPACE, /* pg_tablespace */
OCLASS_FDW, /* pg_foreign_data_wrapper */
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index 1bc027f133..c9d7697730 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -29,6 +29,7 @@
*/
CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(2843,AuthMemRelation_Rowtype_Id) BKI_SCHEMA_MACRO
{
+ Oid oid; /* oid */
Oid roleid BKI_LOOKUP(pg_authid); /* ID of a role */
Oid member BKI_LOOKUP(pg_authid); /* ID of a member of that role */
Oid grantor BKI_LOOKUP(pg_authid); /* who granted the membership */
@@ -42,7 +43,9 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
*/
typedef FormData_pg_auth_members *Form_pg_auth_members;
-DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_oid_index, 9385, AuthMemOidIndexId, on pg_auth_members using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops));
+DECLARE_INDEX(pg_auth_members_grantor_index, 9384, AuthMemGrantorIndexId, on pg_auth_members using btree(grantor oid_ops));
#endif /* PG_AUTH_MEMBERS_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..c2465d0f49 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -103,9 +103,21 @@ ERROR: role "regress_nosuch_recursive" does not exist
DROP ROLE regress_nosuch_admin_recursive;
ERROR: role "regress_nosuch_admin_recursive" does not exist
DROP ROLE regress_plainrole;
+-- fail, can't drop regress_createrole yet, due to outstanding grants
+DROP ROLE regress_createrole;
+ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_read_all_data in role pg_read_all_data
+privileges for membership of role regress_write_all_data in role pg_write_all_data
+privileges for membership of role regress_monitor in role pg_monitor
+privileges for membership of role regress_read_all_settings in role pg_read_all_settings
+privileges for membership of role regress_read_all_stats in role pg_read_all_stats
+privileges for membership of role regress_stat_scan_tables in role pg_stat_scan_tables
+privileges for membership of role regress_read_server_files in role pg_read_server_files
+privileges for membership of role regress_write_server_files in role pg_write_server_files
+privileges for membership of role regress_execute_server_program in role pg_execute_server_program
+privileges for membership of role regress_signal_backend in role pg_signal_backend
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
-DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -125,6 +137,8 @@ DROP ROLE regress_read_server_files;
DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
+-- ok, dropped the other roles first so this is ok now
+DROP ROLE regress_createrole;
-- fail, role still owns database objects
DROP ROLE regress_tenant;
ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index e10dd6f9ae..65b4a22ebc 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -33,6 +33,38 @@ CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- fail, dependency
+ERROR: role "regress_priv_user2" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user3 in role regress_priv_user1
+REASSIGN OWNED BY regress_priv_user2 TO regress_priv_user4;
+DROP ROLE regress_priv_user2; -- still fail, REASSIGN OWNED doesn't help
+ERROR: role "regress_priv_user2" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user3 in role regress_priv_user1
+DROP OWNED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- ok now, DROP OWNED does the job
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user4 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- should fail, dependency
+ERROR: role "regress_priv_user3" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user4 in role regress_priv_user1
+DROP ROLE regress_priv_user4; -- ok
+DROP ROLE regress_priv_user3; -- ok now
+GRANT regress_priv_user1 TO regress_priv_user5 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user6 GRANTED BY regress_priv_user5;
+DROP ROLE regress_priv_user5; -- should fail, dependency
+ERROR: role "regress_priv_user5" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user6 in role regress_priv_user1
+DROP ROLE regress_priv_user1, regress_priv_user5; -- ok, despite order
+-- recreate the roles we just dropped
+CREATE USER regress_priv_user1;
+CREATE USER regress_priv_user2;
+CREATE USER regress_priv_user3;
+CREATE USER regress_priv_user4;
+CREATE USER regress_priv_user5;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..b696628238 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -98,9 +98,11 @@ DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
+-- fail, can't drop regress_createrole yet, due to outstanding grants
+DROP ROLE regress_createrole;
+
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
-DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -121,6 +123,9 @@ DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
+-- ok, dropped the other roles first so this is ok now
+DROP ROLE regress_createrole;
+
-- fail, role still owns database objects
DROP ROLE regress_tenant;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 6d1fd3391a..66834e32a7 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -37,6 +37,33 @@ CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- fail, dependency
+REASSIGN OWNED BY regress_priv_user2 TO regress_priv_user4;
+DROP ROLE regress_priv_user2; -- still fail, REASSIGN OWNED doesn't help
+DROP OWNED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- ok now, DROP OWNED does the job
+
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user4 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- should fail, dependency
+DROP ROLE regress_priv_user4; -- ok
+DROP ROLE regress_priv_user3; -- ok now
+GRANT regress_priv_user1 TO regress_priv_user5 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user6 GRANTED BY regress_priv_user5;
+DROP ROLE regress_priv_user5; -- should fail, dependency
+DROP ROLE regress_priv_user1, regress_priv_user5; -- ok, despite order
+
+-- recreate the roles we just dropped
+CREATE USER regress_priv_user1;
+CREATE USER regress_priv_user2;
+CREATE USER regress_priv_user3;
+CREATE USER regress_priv_user4;
+CREATE USER regress_priv_user5;
+
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
--
2.24.3 (Apple Git-128)
v3-0002-Make-role-grant-system-more-consistent-with-other.patchapplication/octet-stream; name=v3-0002-Make-role-grant-system-more-consistent-with-other.patchDownload
From e4653d80ae6ffeca07f890fb5b178bc0d209a002 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Tue, 26 Jul 2022 12:02:25 -0400
Subject: [PATCH v3 2/2] Make role grant system more consistent with other
privileges.
Previously, membership of role A in role B could be recorded in the
catalog tables only once. This meant that a new grant of role A to
role B would overwrite the previous grant. For other object types, a
new grant of permission on an object - in this case role A - exists
along side the existing grant provided that the grantor is different.
Either grant can be revoked independently of the other, and
permissions remain so long as at least one grant remains. Make role
grants work similarly.
Previously, when granting membership in a role, the superuser could
specify any role whatsoever as the grantor, but for other object types,
the grantor of record must be either the owner of the object, or a
role that currently has privileges to perform a similar GRANT.
Implement the same scheme for role grants, treating the bootstrap
superuser as the role owner since roles do not have owners. This means
that attempting to revoke a grant, or admin option on a grant, can now
fail if there are dependent privileges, and that CASCADE can be used
to revoke these. It also means that you can't grant ADMIN OPTION on
a role back to a user who granted it directly or indirectly to you,
similar to how you can't give WITH GRANT OPTION on a privilege back
to a role which granted it directly or indirectly to you.
Previously, only the superuser could specify GRANTED BY with a user
other than the current user. Relax that rule to allow the grantor
to be any role whose privileges the current user posseses. This
doesn't improve compatibility with what we do for other object types,
where support for GRANTED BY is entirely vestigial, but it makes this
feature more usable and seems to make sense to change at the same time
we're changing related behaviors.
---
doc/src/sgml/ref/grant.sgml | 12 +-
doc/src/sgml/ref/revoke.sgml | 12 +-
src/backend/commands/user.c | 533 +++++++++++++++++++---
src/backend/parser/gram.y | 2 +
src/backend/utils/adt/acl.c | 47 +-
src/backend/utils/cache/syscache.c | 8 +-
src/bin/pg_dump/pg_dumpall.c | 177 ++++++-
src/include/catalog/pg_auth_members.h | 4 +-
src/include/utils/acl.h | 1 +
src/test/regress/expected/create_role.out | 16 +-
src/test/regress/expected/privileges.out | 62 ++-
src/test/regress/sql/create_role.sql | 7 +-
src/test/regress/sql/privileges.sql | 33 +-
13 files changed, 779 insertions(+), 135 deletions(-)
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index f744b05b55..1f828d386a 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -267,8 +267,14 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
<para>
If <literal>GRANTED BY</literal> is specified, the grant is recorded as
- having been done by the specified role. Only database superusers may
- use this option, except when it names the same role executing the command.
+ having been done by the specified role. A user can only attribute a grant
+ to another role if they possess the privileges of that role. A role can
+ only be recorded as a grantor if has <literal>ADMIN OPTION</literal> on
+ a role or is the bootstrap superuser. When a grant is recorded as having
+ a grantor other than the bootstrap superuser, it depends on the grantor
+ continuing to posess <literal>ADMIN OPTION</literal> on the role; so,
+ if <literal>ADMIN OPTION</literal> is revoked, dependent grants must be
+ revoked as well.
</para>
<para>
@@ -333,7 +339,7 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
owner of the affected object. In particular, privileges granted via
such a command will appear to have been granted by the object owner.
(For role membership, the membership appears to have been granted
- by the containing role itself.)
+ by the bootstrap superuser.)
</para>
<para>
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 62f1971036..16e840458c 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -198,9 +198,10 @@ REVOKE [ ADMIN OPTION FOR ]
<para>
When revoking membership in a role, <literal>GRANT OPTION</literal> is instead
called <literal>ADMIN OPTION</literal>, but the behavior is similar.
- This form of the command also allows a <literal>GRANTED BY</literal>
- option, but that option is currently ignored (except for checking
- the existence of the named role).
+ Note that, in releases prior to <productname>PostgreSQL</productname> 16,
+ dependent privileges were not tracked for grants of role membership,
+ and thus <literal>CASCADE</literal> had no effect for role membership.
+ This is no longer the case.
Note also that this form of the command does not
allow the noise word <literal>GROUP</literal>
in <replaceable class="parameter">role_specification</replaceable>.
@@ -239,7 +240,10 @@ REVOKE [ ADMIN OPTION FOR ]
<para>
If a superuser chooses to issue a <command>GRANT</command> or <command>REVOKE</command>
command, the command is performed as though it were issued by the
- owner of the affected object. Since all privileges ultimately come
+ owner of the affected object. (Since roles do not have owners, in the
+ case of a <command>GRANT</command> of role membership, the command is
+ performed as though it were issued by the bootstrap superuser.)
+ Since all privileges ultimately come
from the object owner (possibly indirectly via chains of grant options),
it is possible for a superuser to revoke all privileges, but this might
require use of <literal>CASCADE</literal> as stated above.
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 258943094a..8ab2fecf3a 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -35,10 +35,32 @@
#include "storage/lmgr.h"
#include "utils/acl.h"
#include "utils/builtins.h"
+#include "utils/catcache.h"
#include "utils/fmgroids.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
+/*
+ * Removing a role grant - or the admin option on it - might recurse to
+ * dependent grants. We use these values to reason about what would need to
+ * be done in such cases.
+ *
+ * RRG_NOOP indicates a grant that would not need to be altered by the
+ * operation.
+ *
+ * RRG_REMOVE_ADMIN_OPTION indicates a grant that would need to have
+ * admin_option set to false by the operation.
+ *
+ * RRG_DELETE_GRANT indicates a grant that would need to be removed entirely
+ * by the operation.
+ */
+typedef enum
+{
+ RRG_NOOP,
+ RRG_REMOVE_ADMIN_OPTION,
+ RRG_DELETE_GRANT
+} RevokeRoleGrantAction;
+
/* Potentially set by pg_upgrade_support functions */
Oid binary_upgrade_next_pg_authid_oid = InvalidOid;
@@ -54,7 +76,22 @@ static void AddRoleMems(const char *rolename, Oid roleid,
Oid grantorId, bool admin_opt);
static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
- bool admin_opt);
+ Oid grantorId, bool admin_opt, DropBehavior behavior);
+static Oid check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId,
+ bool is_grant);
+static RevokeRoleGrantAction *initialize_revoke_actions(CatCList *memlist);
+static bool plan_single_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions,
+ Oid member, Oid grantor,
+ bool revoke_admin_option_only,
+ DropBehavior behavior);
+static void plan_member_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions, Oid member);
+static void plan_recursive_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions,
+ int index,
+ bool revoke_admin_option_only,
+ DropBehavior behavior);
/* Check if current user has createrole privileges */
@@ -449,7 +486,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
AddRoleMems(oldrolename, oldroleid,
thisrole_list,
thisrole_oidlist,
- GetUserId(), false);
+ InvalidOid, false);
ReleaseSysCache(oldroletup);
}
@@ -461,10 +498,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
AddRoleMems(stmt->role, roleid,
adminmembers, roleSpecsToIds(adminmembers),
- GetUserId(), true);
+ InvalidOid, true);
AddRoleMems(stmt->role, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- GetUserId(), false);
+ InvalidOid, false);
/* Post creation hook for new role */
InvokeObjectPostCreateHook(AuthIdRelationId, roleid, 0);
@@ -805,11 +842,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
if (stmt->action == +1) /* add members to role */
AddRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- GetUserId(), false);
+ InvalidOid, false);
else if (stmt->action == -1) /* drop members from role */
DelRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- false);
+ InvalidOid, false, DROP_RESTRICT); /* XXX sketchy - hint
+ * may mislead */
}
/*
@@ -1027,7 +1065,7 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
- Form_pg_auth_members authmem_form;
+ Form_pg_auth_members authmem_form;
authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
deleteSharedDependencyRecordsFor(AuthMemRelationId,
@@ -1047,7 +1085,7 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
- Form_pg_auth_members authmem_form;
+ Form_pg_auth_members authmem_form;
authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
deleteSharedDependencyRecordsFor(AuthMemRelationId,
@@ -1296,7 +1334,7 @@ GrantRole(GrantRoleStmt *stmt)
if (stmt->grantor)
grantor = get_rolespec_oid(stmt->grantor, false);
else
- grantor = GetUserId();
+ grantor = InvalidOid;
grantee_ids = roleSpecsToIds(stmt->grantee_roles);
@@ -1330,7 +1368,7 @@ GrantRole(GrantRoleStmt *stmt)
else
DelRoleMems(rolename, roleid,
stmt->grantee_roles, grantee_ids,
- stmt->admin_opt);
+ grantor, stmt->admin_opt, stmt->behavior);
}
/*
@@ -1431,7 +1469,7 @@ roleSpecsToIds(List *memberNames)
* roleid: OID of role to add to
* memberSpecs: list of RoleSpec of roles to add (used only for error messages)
* memberIds: OIDs of roles to add
- * grantorId: who is granting the membership
+ * grantorId: who is granting the membership (InvalidOid if not set explicitly)
* admin_opt: granting admin option?
*/
static void
@@ -1443,6 +1481,7 @@ AddRoleMems(const char *rolename, Oid roleid,
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
+ Oid currentUserId = GetUserId();
Assert(list_length(memberSpecs) == list_length(memberIds));
@@ -1464,7 +1503,7 @@ AddRoleMems(const char *rolename, Oid roleid,
else
{
if (!have_createrole_privilege() &&
- !is_admin_of_role(grantorId, roleid))
+ !is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
@@ -1483,29 +1522,25 @@ AddRoleMems(const char *rolename, Oid roleid,
ereport(ERROR,
errmsg("role \"%s\" cannot have explicit members", rolename));
- /*
- * The role membership grantor of record has little significance at
- * present. Nonetheless, inasmuch as users might look to it for a crude
- * audit trail, let only superusers impute the grant to a third party.
- */
- if (grantorId != GetUserId() && !superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to set grantor")));
+ /* Validate grantor (and resolve implicit grantor if not specified). */
+ grantorId = check_role_grantor(currentUserId, roleid, grantorId, true);
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
+ /*
+ * Only allow changes to this role by one backend at a time, so that we
+ * can check integrity constraints like the lack of circular ADMIN OPTION
+ * grants without fear of race conditions.
+ */
+ LockSharedObject(AuthIdRelationId, roleid, 0,
+ ShareUpdateExclusiveLock);
+
+ /* Preliminary sanity checks. */
forboth(specitem, memberSpecs, iditem, memberIds)
{
RoleSpec *memberRole = lfirst_node(RoleSpec, specitem);
Oid memberid = lfirst_oid(iditem);
- HeapTuple authmem_tuple;
- HeapTuple tuple;
- Datum new_record[Natts_pg_auth_members] = {0};
- bool new_record_nulls[Natts_pg_auth_members] = {0};
- bool new_record_repl[Natts_pg_auth_members] = {0};
- Form_pg_auth_members authmem_form;
/*
* pg_database_owner is never a role member. Lifting this restriction
@@ -1543,14 +1578,94 @@ AddRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INVALID_GRANT_OPERATION),
errmsg("role \"%s\" is a member of role \"%s\"",
rolename, get_rolespec_name(memberRole))));
+ }
+
+ /*
+ * Disallow attempts to grant ADMIN OPTION back to a user who granted it
+ * to you, similar to what check_circularity does for ACLs. We want the
+ * chains of grants to remain acyclic, so that it's always possible to use
+ * REVOKE .. CASCADE to clean up all grants that depend on the one being
+ * revoked.
+ *
+ * NB: This check might look redundant with the check for membership
+ * loops above, but it isn't. That's checking for role-member loop (e.g.
+ * A is a member of B and B is a member of A) while this is checking for
+ * a member-grantor loop (e.g. A gave ADMIN OPTION to X to B and now B, who
+ * has no other source of ADMIN OPTION on X, tries to give ADMIN OPTION
+ * on X back to A).
+ */
+ if (admin_opt && grantorId != BOOTSTRAP_SUPERUSERID)
+ {
+ CatCList *memlist;
+ RevokeRoleGrantAction *actions;
+ int i;
+
+ /* Get the list of members for this role. */
+ memlist = SearchSysCacheList1(AUTHMEMROLEMEM,
+ ObjectIdGetDatum(roleid));
+
+ /*
+ * Figure out what would happen if we removed all existing grants to
+ * every role to which we've been asked to make a new grant.
+ */
+ actions = initialize_revoke_actions(memlist);
+ foreach(iditem, memberIds)
+ {
+ Oid memberid = lfirst_oid(iditem);
+
+ if (memberid == BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_GRANT_OPERATION),
+ errmsg("grants with admin options cannot be circular")));
+ plan_member_revoke(memlist, actions, memberid);
+ }
+
+ /*
+ * If the result would be that the grantor role would no longer have
+ * the ability to perform the grant, then the proposed grant would
+ * create a circularity.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (actions[i] == RRG_NOOP &&
+ authmem_form->member == grantorId &&
+ authmem_form->admin_option)
+ break;
+ }
+ if (i >= memlist->n_members)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_GRANT_OPERATION),
+ errmsg("admin options cannot be granted back to your own grantor")));
+
+ ReleaseSysCacheList(memlist);
+ }
+
+ /* Now perform the catalog updates. */
+ forboth(specitem, memberSpecs, iditem, memberIds)
+ {
+ RoleSpec *memberRole = lfirst_node(RoleSpec, specitem);
+ Oid memberid = lfirst_oid(iditem);
+ HeapTuple authmem_tuple;
+ HeapTuple tuple;
+ Datum new_record[Natts_pg_auth_members] = {0};
+ bool new_record_nulls[Natts_pg_auth_members] = {0};
+ bool new_record_repl[Natts_pg_auth_members] = {0};
+ Form_pg_auth_members authmem_form;
/*
* Check if entry for this role/member already exists; if so, give
* warning unless we are adding admin option.
*/
- authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
+ authmem_tuple = SearchSysCache3(AUTHMEMROLEMEM,
ObjectIdGetDatum(roleid),
- ObjectIdGetDatum(memberid));
+ ObjectIdGetDatum(memberid),
+ ObjectIdGetDatum(grantorId));
if (!HeapTupleIsValid(authmem_tuple))
{
authmem_form = NULL;
@@ -1562,8 +1677,9 @@ AddRoleMems(const char *rolename, Oid roleid,
if (!admin_opt || authmem_form->admin_option)
{
ereport(NOTICE,
- (errmsg("role \"%s\" is already a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
+ (errmsg("role \"%s\" has already been granted membership in role \"%s\" by role \"%s\"",
+ get_rolespec_name(memberRole), rolename,
+ GetUserNameFromId(grantorId, false))));
ReleaseSysCache(authmem_tuple);
continue;
}
@@ -1577,28 +1693,12 @@ AddRoleMems(const char *rolename, Oid roleid,
if (HeapTupleIsValid(authmem_tuple))
{
- new_record_repl[Anum_pg_auth_members_grantor - 1] = true;
new_record_repl[Anum_pg_auth_members_admin_option - 1] = true;
tuple = heap_modify_tuple(authmem_tuple, pg_authmem_dsc,
new_record,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
- if (authmem_form->grantor != grantorId)
- {
- Oid *oldmembers = palloc(sizeof(Oid));
- Oid *newmembers = palloc(sizeof(Oid));
-
- /* updateAclDependencies wants to pfree array inputs */
- oldmembers[0] = authmem_form->grantor;
- newmembers[0] = grantorId;
-
- updateAclDependencies(AuthMemRelationId, authmem_form->oid,
- 0, InvalidOid,
- 1, oldmembers,
- 1, newmembers);
- }
-
ReleaseSysCache(authmem_tuple);
}
else
@@ -1637,17 +1737,22 @@ AddRoleMems(const char *rolename, Oid roleid,
* roleid: OID of role to del from
* memberSpecs: list of RoleSpec of roles to del (used only for error messages)
* memberIds: OIDs of roles to del
+ * grantorId: who is revoking the membership
* admin_opt: remove admin option only?
*/
static void
DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
- bool admin_opt)
+ Oid grantorId, bool admin_opt, DropBehavior behavior)
{
Relation pg_authmem_rel;
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
+ Oid currentUserId = GetUserId();
+ CatCList *memlist;
+ RevokeRoleGrantAction *actions;
+ int i;
Assert(list_length(memberSpecs) == list_length(memberIds));
@@ -1669,40 +1774,69 @@ DelRoleMems(const char *rolename, Oid roleid,
else
{
if (!have_createrole_privilege() &&
- !is_admin_of_role(GetUserId(), roleid))
+ !is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
rolename)));
}
+ /* Validate grantor (and resolve implicit grantor if not specified). */
+ grantorId = check_role_grantor(currentUserId, roleid, grantorId, false);
+
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
+ /*
+ * Only allow changes to this role by one backend at a time, so that we
+ * can check for things like dependent privileges without fear of race
+ * conditions.
+ */
+ LockSharedObject(AuthIdRelationId, roleid, 0,
+ ShareUpdateExclusiveLock);
+
+ memlist = SearchSysCacheList1(AUTHMEMROLEMEM, ObjectIdGetDatum(roleid));
+ actions = initialize_revoke_actions(memlist);
+
+ /*
+ * We may need to recurse to dependent privileges if DROP_CASCADE was
+ * specified, or refuse to perform the operation if dependent privileges
+ * exist and DROP_RECURSE was specified. plan_single_revoke() will
+ * figure out what to do with each catalog tuple.
+ */
forboth(specitem, memberSpecs, iditem, memberIds)
{
RoleSpec *memberRole = lfirst(specitem);
Oid memberid = lfirst_oid(iditem);
- HeapTuple authmem_tuple;
- Form_pg_auth_members authmem_form;
- /*
- * Find entry for this role/member
- */
- authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
- ObjectIdGetDatum(roleid),
- ObjectIdGetDatum(memberid));
- if (!HeapTupleIsValid(authmem_tuple))
+ if (!plan_single_revoke(memlist, actions, memberid, grantorId,
+ admin_opt, behavior))
{
ereport(WARNING,
- (errmsg("role \"%s\" is not a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
+ (errmsg("role \"%s\" has not been granted membership in role \"%s\" by role \"%s\"",
+ get_rolespec_name(memberRole), rolename,
+ GetUserNameFromId(grantorId, false))));
continue;
}
+ }
+ /*
+ * We now know what to do with each catalog tuple: it should either be
+ * left alone, deleted, or just have the admin_option flag cleared.
+ * Perform the appropriate action in each case.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ if (actions[i] == RRG_NOOP)
+ continue;
+
+ authmem_tuple = &memlist->members[i]->tuple;
authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
- if (!admin_opt)
+ if (actions[i] == RRG_DELETE_GRANT)
{
/*
* Remove the entry altogether, after first removing its
@@ -1729,15 +1863,282 @@ DelRoleMems(const char *rolename, Oid roleid,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
}
-
- ReleaseSysCache(authmem_tuple);
-
- /* CCI after each change, in case there are duplicates in list */
- CommandCounterIncrement();
}
+ ReleaseSysCacheList(memlist);
+
/*
* Close pg_authmem, but keep lock till commit.
*/
table_close(pg_authmem_rel, NoLock);
}
+
+/*
+ * Sanity-check, or infer, the grantor for a GRANT or REVOKE statement
+ * targeting a role.
+ *
+ * The grantor must always be either a role with ADMIN OPTION on the role in
+ * which membership is being granted, or the bootstrap superuser. This is
+ * similar to the restriction enforced by select_best_grantor, except that
+ * roles don't have owners, so we regard the bootstrap superuser as the
+ * implicit owner.
+ *
+ * The return value is the OID to be regarded as the grantor when executing
+ * the operation.
+ */
+static Oid
+check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
+{
+ /* If the grantor ID was not specified, pick one to use. */
+ if (!OidIsValid(grantorId))
+ {
+ /*
+ * Grants where the grantor is recorded as the bootstrap superuser
+ * do not depend on any other existing grants, so always default to
+ * this interpretation when possible.
+ */
+ if (has_createrole_privilege(currentUserId))
+ return BOOTSTRAP_SUPERUSERID;
+
+ /*
+ * Otherwise, the grantor must either have ADMIN OPTION on the role
+ * or inherit the privileges of a role which does. In the former case,
+ * record the grantor as the current user; in the latter, pick one
+ * of the roles that is "most directly" inherited by the current role
+ * (i.e. fewest "hops").
+ *
+ * (We shouldn't fail to find a best grantor, because we've already
+ * established that the current user has permission to perform the
+ * operation.)
+ */
+ grantorId = select_best_admin(currentUserId, roleid);
+ if (!OidIsValid(grantorId))
+ elog(ERROR, "no possible grantors");
+ return grantorId;
+ }
+
+ /*
+ * If an explicit grantor is specified, it must be a role whose privileges
+ * the current user possesses.
+ *
+ * It should also be a role that has ADMIN OPTION on the target role, but
+ * we check this condition only in case of GRANT. For REVOKE, no matching
+ * grant should exist anyway, but if it somehow does, let the user get rid
+ * of it.
+ */
+ if (is_grant)
+ {
+ if (!has_privs_of_role(currentUserId, grantorId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to grant privileges as role \"%s\"",
+ GetUserNameFromId(grantorId, false))));
+
+ if (grantorId != BOOTSTRAP_SUPERUSERID &&
+ select_best_admin(grantorId, roleid) != grantorId)
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("grantor must have ADMIN OPTION on \"%s\"",
+ GetUserNameFromId(roleid, false))));
+ }
+ else
+ {
+ if (!has_privs_of_role(currentUserId, grantorId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to revoke privileges granted by role \"%s\"",
+ GetUserNameFromId(grantorId, false))));
+ }
+
+ /*
+ * If a grantor was specified explicitly, always attribute the grant to
+ * that role (unless we error out above).
+ */
+ return grantorId;
+}
+
+/*
+ * Initialize an array of RevokeRoleGrantAction objects.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * We here construct an array indicating that no actions are to be performed;
+ * that is, every element is intiially RRG_NOOP.
+ */
+static RevokeRoleGrantAction *
+initialize_revoke_actions(CatCList *memlist)
+{
+ RevokeRoleGrantAction *result;
+ int i;
+
+ if (memlist->n_members == 0)
+ return NULL;
+
+ result = palloc(sizeof(RevokeRoleGrantAction) * memlist->n_members);
+ for (i = 0; i < memlist->n_members; i++)
+ result[i] = RRG_NOOP;
+ return result;
+}
+
+/*
+ * Figure out what we would need to do in order to revoke a grant, or just the
+ * admin option on a grant, given that there might be dependent privileges.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * Whatever actions prove to be necessary will be signalled by updating
+ * 'actions'.
+ *
+ * If behavior is DROP_RESTRICT, an error will occur if there are dependent
+ * role membership grants; if DROP_CASCADE, those grants will be scheduled
+ * for deletion.
+ *
+ * The return value is true if the matching grant was found in the list,
+ * and false if not.
+ */
+static bool
+plan_single_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ Oid member, Oid grantor, bool revoke_admin_option_only,
+ DropBehavior behavior)
+{
+ int i;
+
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (authmem_form->member == member &&
+ authmem_form->grantor == grantor)
+ {
+ plan_recursive_revoke(memlist, actions, i,
+ revoke_admin_option_only, behavior);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Figure out what we would need to do in order to revoke all grants to
+ * a given member, given that there might be dependent privileges.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * Whatever actions prove to be necessary will be signalled by updating
+ * 'actions'.
+ */
+static void
+plan_member_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ Oid member)
+{
+ int i;
+
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (authmem_form->member == member)
+ plan_recursive_revoke(memlist, actions, i, false, DROP_CASCADE);
+ }
+}
+
+/*
+ * Workhorse for figuring out recursive revocation of role grants.
+ *
+ * This is similar to what recursive_revoke() does for ACLs.
+ */
+static void
+plan_recursive_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ int index,
+ bool revoke_admin_option_only, DropBehavior behavior)
+{
+ bool would_still_have_admin_option = false;
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+ int i;
+
+ /* If it's already been done, we can just return. */
+ if (actions[index] == RRG_DELETE_GRANT)
+ return;
+ if (actions[index] == RRG_REMOVE_ADMIN_OPTION &&
+ revoke_admin_option_only)
+ return;
+
+ /* Locate tuple data. */
+ authmem_tuple = &memlist->members[index]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ /*
+ * If the existing tuple does not have admin_option set, then we do not
+ * need to recurse. If we're just supposed to clear that bit we don't
+ * need to do anything at all; if we're supposed to remove the grant,
+ * we need to do something, but only to the tuple, and not any others.
+ */
+ if (!revoke_admin_option_only)
+ {
+ actions[index] = RRG_DELETE_GRANT;
+ if (!authmem_form->admin_option)
+ return;
+ }
+ else
+ {
+ if (!authmem_form->admin_option)
+ return;
+ actions[index] = RRG_REMOVE_ADMIN_OPTION;
+ }
+
+ /* Determine whether the member would still have ADMIN OPTION. */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple am_cascade_tuple;
+ Form_pg_auth_members am_cascade_form;
+
+ am_cascade_tuple = &memlist->members[i]->tuple;
+ am_cascade_form = (Form_pg_auth_members) GETSTRUCT(am_cascade_tuple);
+
+ if (am_cascade_form->member == authmem_form->member &&
+ am_cascade_form->admin_option && actions[i] == RRG_NOOP)
+ {
+ would_still_have_admin_option = true;
+ break;
+ }
+ }
+
+ /* If the member would still have ADMIN OPTION, we need not recurse. */
+ if (would_still_have_admin_option)
+ return;
+
+ /*
+ * Recurse to grants that are not yet slated for deletion which have this
+ * member as the grantor.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple am_cascade_tuple;
+ Form_pg_auth_members am_cascade_form;
+
+ am_cascade_tuple = &memlist->members[i]->tuple;
+ am_cascade_form = (Form_pg_auth_members) GETSTRUCT(am_cascade_tuple);
+
+ if (am_cascade_form->grantor == authmem_form->member &&
+ actions[i] != RRG_DELETE_GRANT)
+ {
+ if (behavior == DROP_RESTRICT)
+ ereport(ERROR,
+ (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
+ errmsg("dependent privileges exist"),
+ errhint("Use CASCADE to revoke them too.")));
+
+ plan_recursive_revoke(memlist, actions, i, false, behavior);
+ }
+ }
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f9037761f9..c8bd66dd54 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -7870,6 +7870,7 @@ RevokeRoleStmt:
n->admin_opt = false;
n->granted_roles = $2;
n->grantee_roles = $4;
+ n->grantor = $5;
n->behavior = $6;
$$ = (Node *) n;
}
@@ -7881,6 +7882,7 @@ RevokeRoleStmt:
n->admin_opt = true;
n->granted_roles = $5;
n->grantee_roles = $7;
+ n->grantor = $8;
n->behavior = $9;
$$ = (Node *) n;
}
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 6fa58dd8eb..3e045da31f 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4791,9 +4791,7 @@ has_rolinherit(Oid roleid)
* Get a list of roles that the specified roleid is a member of
*
* Type ROLERECURSE_PRIVS recurses only through roles that have rolinherit
- * set, while ROLERECURSE_MEMBERS recurses through all roles. This sets
- * *is_admin==true if and only if role "roleid" has an ADMIN OPTION membership
- * in role "admin_of".
+ * set, while ROLERECURSE_MEMBERS recurses through all roles.
*
* Since indirect membership testing is relatively expensive, we cache
* a list of memberships. Hence, the result is only guaranteed good until
@@ -4801,10 +4799,15 @@ has_rolinherit(Oid roleid)
*
* For the benefit of select_best_grantor, the result is defined to be
* in breadth-first order, ie, closer relationships earlier.
+ *
+ * If admin_of is not InvalidOid, this function sets *admin_role, either
+ * to the OID of the first role in the result list that directly possesses
+ * ADMIN OPTION on the role corresponding to admin_of, or to InvalidOid if
+ * there is no such role.
*/
static List *
roles_is_member_of(Oid roleid, enum RoleRecurseType type,
- Oid admin_of, bool *is_admin)
+ Oid admin_of, Oid *admin_role)
{
Oid dba;
List *roles_list;
@@ -4812,7 +4815,9 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
List *new_cached_roles;
MemoryContext oldctx;
- Assert(OidIsValid(admin_of) == PointerIsValid(is_admin));
+ Assert(OidIsValid(admin_of) == PointerIsValid(admin_role));
+ if (admin_role != NULL)
+ *admin_role = InvalidOid;
/* If cache is valid and ADMIN OPTION not sought, just return the list */
if (cached_role[type] == roleid && !OidIsValid(admin_of) &&
@@ -4873,8 +4878,8 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
*/
if (otherid == admin_of &&
((Form_pg_auth_members) GETSTRUCT(tup))->admin_option &&
- OidIsValid(admin_of))
- *is_admin = true;
+ OidIsValid(admin_of) && !OidIsValid(*admin_role))
+ *admin_role = memberid;
/*
* Even though there shouldn't be any loops in the membership
@@ -5014,7 +5019,7 @@ is_member_of_role_nosuper(Oid member, Oid role)
bool
is_admin_of_role(Oid member, Oid role)
{
- bool result = false;
+ Oid admin_role;
if (superuser_arg(member))
return true;
@@ -5023,8 +5028,30 @@ is_admin_of_role(Oid member, Oid role)
if (member == role)
return false;
- (void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &result);
- return result;
+ (void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &admin_role);
+ return OidIsValid(admin_role);
+}
+
+/*
+ * Find a role whose privileges "member" inherits which has ADMIN OPTION
+ * on "role", ignoring super-userness.
+ *
+ * There might be more than one such role; prefer one which involves fewer
+ * hops. That is, if member has ADMIN OPTION, prefer that over all other
+ * options; if not, prefer a role from which member inherits more directly
+ * over more indirect inheritance.
+ */
+Oid
+select_best_admin(Oid member, Oid role)
+{
+ Oid admin_role;
+
+ /* By policy, a role cannot have WITH ADMIN OPTION on itself. */
+ if (member == role)
+ return InvalidOid;
+
+ (void) roles_is_member_of(member, ROLERECURSE_PRIVS, role, &admin_role);
+ return admin_role;
}
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 1912b12146..eec644ec84 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -213,22 +213,22 @@ static const struct cachedesc cacheinfo[] = {
},
{AuthMemRelationId, /* AUTHMEMMEMROLE */
AuthMemMemRoleIndexId,
- 2,
+ 3,
{
Anum_pg_auth_members_member,
Anum_pg_auth_members_roleid,
- 0,
+ Anum_pg_auth_members_grantor,
0
},
8
},
{AuthMemRelationId, /* AUTHMEMROLEMEM */
AuthMemRoleMemIndexId,
- 2,
+ 3,
{
Anum_pg_auth_members_roleid,
Anum_pg_auth_members_member,
- 0,
+ Anum_pg_auth_members_grantor,
0
},
8
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 26d3d53809..e8a2bfa6bd 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -21,6 +21,7 @@
#include "catalog/pg_authid_d.h"
#include "common/connect.h"
#include "common/file_utils.h"
+#include "common/hashfn.h"
#include "common/logging.h"
#include "common/string.h"
#include "dumputils.h"
@@ -31,6 +32,28 @@
/* version string we expect back from pg_dump */
#define PGDUMP_VERSIONSTR "pg_dump (PostgreSQL) " PG_VERSION "\n"
+static uint32 hash_string_pointer(char *s);
+
+typedef struct
+{
+ uint32 status;
+ uint32 hashval;
+ char *rolename;
+} RoleNameEntry;
+
+#define SH_PREFIX rolename
+#define SH_ELEMENT_TYPE RoleNameEntry
+#define SH_KEY_TYPE char *
+#define SH_KEY rolename
+#define SH_HASH_KEY(tb, key) hash_string_pointer(key)
+#define SH_EQUAL(tb, a, b) (strcmp(a, b) == 0)
+#define SH_STORE_HASH
+#define SH_GET_HASH(tb, a) (a)->hashval
+#define SH_SCOPE static inline
+#define SH_RAW_ALLOCATOR pg_malloc0
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
static void help(void);
@@ -925,45 +948,150 @@ dumpRoleMembership(PGconn *conn)
{
PQExpBuffer buf = createPQExpBuffer();
PGresult *res;
- int i;
+ int start = 0,
+ end,
+ total;
+ bool dump_grantors;
- printfPQExpBuffer(buf, "SELECT ur.rolname AS roleid, "
+ /*
+ * Previous versions of PostgreSQL didn't used to track the grantor very
+ * carefully in the backend, and the grantor could be any user even if
+ * they didn't have ADMIN OPTION on the role, or a user that no longer
+ * existed. To avoid dump and restore failures, don't dump the grantor
+ * when talking to an old server version.
+ */
+ dump_grantors = (PQserverVersion(conn) >= 160000);
+
+ /* Generate and execute query. */
+ printfPQExpBuffer(buf, "SELECT ur.rolname AS role, "
"um.rolname AS member, "
- "a.admin_option, "
- "ug.rolname AS grantor "
+ "ug.oid AS grantorid, "
+ "ug.rolname AS grantor, "
+ "a.admin_option "
"FROM pg_auth_members a "
"LEFT JOIN %s ur on ur.oid = a.roleid "
"LEFT JOIN %s um on um.oid = a.member "
"LEFT JOIN %s ug on ug.oid = a.grantor "
"WHERE NOT (ur.rolname ~ '^pg_' AND um.rolname ~ '^pg_')"
- "ORDER BY 1,2,3", role_catalog, role_catalog, role_catalog);
+ "ORDER BY 1,2,4", role_catalog, role_catalog, role_catalog);
res = executeQuery(conn, buf->data);
if (PQntuples(res) > 0)
fprintf(OPF, "--\n-- Role memberships\n--\n\n");
- for (i = 0; i < PQntuples(res); i++)
+ /*
+ * We can't dump these GRANT commands in arbitary order, because a role
+ * that is named as a grantor must already have ADMIN OPTION on the
+ * role for which it is granting permissions, except for the boostrap
+ * superuser, who can always be named as the grantor.
+ *
+ * We handle this by considering these grants role by role. For each role,
+ * we initially consider the only allowable grantor to be the boostrap
+ * superuser. Every time we grant ADMIN OPTION on the role to some user,
+ * that user also becomes an allowable grantor. We make repeated passes
+ * over the grants for the role, each time dumping those whose grantors
+ * are allowable and which we haven't done yet. Eventually this should
+ * let us dump all the grants.
+ */
+ total = PQntuples(res);
+ while (start < total)
{
- char *roleid = PQgetvalue(res, i, 0);
- char *member = PQgetvalue(res, i, 1);
- char *option = PQgetvalue(res, i, 2);
+ char *role = PQgetvalue(res, start, 0);
+ int i;
+ bool *done;
+ int remaining;
+ int prev_remaining = 0;
+ rolename_hash *ht;
+
+ /* All memberships for a single role should be adjacent. */
+ for (end = start; end < total; ++end)
+ {
+ char *otherrole;
+
+ otherrole = PQgetvalue(res, end, 0);
+ if (strcmp(role, otherrole) != 0)
+ break;
+ }
- fprintf(OPF, "GRANT %s", fmtId(roleid));
- fprintf(OPF, " TO %s", fmtId(member));
- if (*option == 't')
- fprintf(OPF, " WITH ADMIN OPTION");
+ role = PQgetvalue(res, start, 0);
+ remaining = end - start;
+ done = pg_malloc0(remaining * sizeof(bool));
+ ht = rolename_create(remaining, NULL);
/*
- * We don't track the grantor very carefully in the backend, so cope
- * with the possibility that it has been dropped.
+ * Make repeated passses over the grants for this role until all have
+ * been dumped.
*/
- if (!PQgetisnull(res, i, 3))
+ while (remaining > 0)
{
- char *grantor = PQgetvalue(res, i, 3);
+ /*
+ * We should make progress on every iteration, because a notional
+ * graph whose vertices are grants and whose edges point from
+ * grantors to members should be connected and acyclic. If we fail
+ * to make progress, either we or the server have messed up.
+ */
+ if (remaining == prev_remaining)
+ {
+ pg_log_error("could not find a legal dump ordering for memberships in role \"%s\"",
+ role);
+ PQfinish(conn);
+ exit_nicely(1);
+ }
- fprintf(OPF, " GRANTED BY %s", fmtId(grantor));
+ /* Make one pass over the grants for this role. */
+ for (i = start; i < end; ++i)
+ {
+ char *member;
+ char *admin_option;
+ char *grantorid;
+ char *grantor;
+ bool found;
+
+ /* If we already did this grant, don't do it again. */
+ if (done[i - start])
+ continue;
+
+ member = PQgetvalue(res, i, 1);
+ grantorid = PQgetvalue(res, i, 2);
+ grantor = PQgetvalue(res, i, 3);
+ admin_option = PQgetvalue(res, i, 4);
+
+ /*
+ * If we're not dumping grantors or if the grantor is the
+ * bootstrap superuser, it's fine to dump this now. Otherwise,
+ * it's got to be someone who has already been granted ADMIN
+ * OPTION.
+ */
+ if (dump_grantors &&
+ atooid(grantorid) != BOOTSTRAP_SUPERUSERID &&
+ rolename_lookup(ht, grantor) == NULL)
+ continue;
+
+ /* Remember that we did this so that we don't do it again. */
+ done[i - start] = true;
+ --remaining;
+
+ /*
+ * If ADMIN OPTION is being granted, remember that grants
+ * listing this member as the grantor can now be dumped.
+ */
+ if (*admin_option == 't')
+ rolename_insert(ht, member, &found);
+
+ /* Generate the actual GRANT statement. */
+ fprintf(OPF, "GRANT %s", fmtId(role));
+ fprintf(OPF, " TO %s", fmtId(member));
+ if (*admin_option == 't')
+ fprintf(OPF, " WITH ADMIN OPTION");
+ if (dump_grantors)
+ fprintf(OPF, " GRANTED BY %s", fmtId(grantor));
+ fprintf(OPF, ";\n");
+ }
}
- fprintf(OPF, ";\n");
+
+ rolename_destroy(ht);
+ pg_free(done);
+ start = end;
}
PQclear(res);
@@ -1748,3 +1876,14 @@ dumpTimestamp(const char *msg)
if (strftime(buf, sizeof(buf), PGDUMP_STRFTIME_FMT, localtime(&now)) != 0)
fprintf(OPF, "-- %s %s\n\n", msg, buf);
}
+
+/*
+ * Helper function for rolenamehash hash table.
+ */
+static uint32
+hash_string_pointer(char *s)
+{
+ unsigned char *ss = (unsigned char *) s;
+
+ return hash_bytes(ss, strlen(s));
+}
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index c9d7697730..e57ec4f810 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -44,8 +44,8 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
typedef FormData_pg_auth_members *Form_pg_auth_members;
DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_oid_index, 9385, AuthMemOidIndexId, on pg_auth_members using btree(oid oid_ops));
-DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
-DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops, grantor oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops, grantor oid_ops));
DECLARE_INDEX(pg_auth_members_grantor_index, 9384, AuthMemGrantorIndexId, on pg_auth_members using btree(grantor oid_ops));
#endif /* PG_AUTH_MEMBERS_H */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 48f7d72add..3d6411197c 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -212,6 +212,7 @@ extern bool has_privs_of_role(Oid member, Oid role);
extern bool is_member_of_role(Oid member, Oid role);
extern bool is_member_of_role_nosuper(Oid member, Oid role);
extern bool is_admin_of_role(Oid member, Oid role);
+extern Oid select_best_admin(Oid member, Oid role);
extern void check_is_member_of_role(Oid member, Oid role);
extern Oid get_role_oid(const char *rolename, bool missing_ok);
extern Oid get_role_oid_or_public(const char *rolename);
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index c2465d0f49..4e67d72760 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -103,21 +103,9 @@ ERROR: role "regress_nosuch_recursive" does not exist
DROP ROLE regress_nosuch_admin_recursive;
ERROR: role "regress_nosuch_admin_recursive" does not exist
DROP ROLE regress_plainrole;
--- fail, can't drop regress_createrole yet, due to outstanding grants
-DROP ROLE regress_createrole;
-ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
-DETAIL: privileges for membership of role regress_read_all_data in role pg_read_all_data
-privileges for membership of role regress_write_all_data in role pg_write_all_data
-privileges for membership of role regress_monitor in role pg_monitor
-privileges for membership of role regress_read_all_settings in role pg_read_all_settings
-privileges for membership of role regress_read_all_stats in role pg_read_all_stats
-privileges for membership of role regress_stat_scan_tables in role pg_stat_scan_tables
-privileges for membership of role regress_read_server_files in role pg_read_server_files
-privileges for membership of role regress_write_server_files in role pg_write_server_files
-privileges for membership of role regress_execute_server_program in role pg_execute_server_program
-privileges for membership of role regress_signal_backend in role pg_signal_backend
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
+DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -137,8 +125,6 @@ DROP ROLE regress_read_server_files;
DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
--- ok, dropped the other roles first so this is ok now
-DROP ROLE regress_createrole;
-- fail, role still owns database objects
DROP ROLE regress_tenant;
ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 65b4a22ebc..8f5072bbda 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -33,6 +33,54 @@ CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- circular ADMIN OPTION grants should be disallowed
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION GRANTED BY regress_priv_user2;
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION GRANTED BY regress_priv_user3;
+ERROR: admin options cannot be granted back to your own grantor
+-- need CASCADE to revoke grant or admin option if dependent grants exist
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2; -- fail
+ERROR: dependent privileges exist
+HINT: Use CASCADE to revoke them too.
+REVOKE regress_priv_user1 FROM regress_priv_user2; -- fail
+ERROR: dependent privileges exist
+HINT: Use CASCADE to revoke them too.
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ member | admin_option
+--------------------+--------------
+ regress_priv_user2 | t
+ regress_priv_user3 | t
+(2 rows)
+
+BEGIN;
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ member | admin_option
+--------------------+--------------
+ regress_priv_user2 | f
+(1 row)
+
+ROLLBACK;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ member | admin_option
+--------+--------------
+(0 rows)
+
+-- inferred grantor must be a role with ADMIN OPTION
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user2 TO regress_priv_user3;
+SET ROLE regress_priv_user3;
+GRANT regress_priv_user1 TO regress_priv_user4;
+SELECT grantor::regrole FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole and member = 'regress_priv_user4'::regrole;
+ grantor
+--------------------
+ regress_priv_user2
+(1 row)
+
+RESET ROLE;
+REVOKE regress_priv_user2 FROM regress_priv_user3;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
@@ -68,15 +116,17 @@ CREATE USER regress_priv_user5;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
+GRANT regress_priv_user9 TO regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
GRANT pg_read_all_settings TO regress_priv_user9 WITH ADMIN OPTION;
SET SESSION AUTHORIZATION regress_priv_user9;
GRANT pg_read_all_settings TO regress_priv_user10;
SET SESSION AUTHORIZATION regress_priv_user8;
-REVOKE pg_read_all_settings FROM regress_priv_user10;
+REVOKE pg_read_all_settings FROM regress_priv_user10 GRANTED BY regress_priv_user9;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user9;
REVOKE pg_read_all_settings FROM regress_priv_user9;
RESET SESSION AUTHORIZATION;
+REVOKE regress_priv_user9 FROM regress_priv_user8;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
SET ROLE pg_read_all_settings;
@@ -90,7 +140,7 @@ CREATE GROUP regress_priv_group1;
CREATE GROUP regress_priv_group2 WITH USER regress_priv_user1, regress_priv_user2;
ALTER GROUP regress_priv_group1 ADD USER regress_priv_user4;
ALTER GROUP regress_priv_group2 ADD USER regress_priv_user2; -- duplicate
-NOTICE: role "regress_priv_user2" is already a member of role "regress_priv_group2"
+NOTICE: role "regress_priv_user2" has already been granted membership in role "regress_priv_group2" by role "rhaas"
ALTER GROUP regress_priv_group2 DROP USER regress_priv_user2;
GRANT regress_priv_group2 TO regress_priv_user4 WITH ADMIN OPTION;
-- prepare non-leakproof function for later
@@ -99,9 +149,13 @@ CREATE FUNCTION leak(integer,integer) RETURNS boolean
LANGUAGE internal IMMUTABLE STRICT; -- but deliberately not LEAKPROOF
ALTER FUNCTION leak(integer,integer) OWNER TO regress_priv_user1;
-- test owner privileges
+GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY regress_priv_role; -- error, doesn't have ADMIN OPTION
+ERROR: grantor must have ADMIN OPTION on "regress_priv_role"
GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY CURRENT_ROLE;
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY foo; -- error
-REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- error
+ERROR: role "foo" does not exist
+REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- warning, noop
+WARNING: role "regress_priv_user1" has not been granted membership in role "regress_priv_role" by role "regress_priv_user2"
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_USER;
REVOKE regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_ROLE;
DROP ROLE regress_priv_role;
@@ -1746,7 +1800,7 @@ SET SESSION AUTHORIZATION regress_priv_user1;
GRANT regress_priv_group2 TO regress_priv_user5; -- fails: no ADMIN OPTION
ERROR: must have admin option on role "regress_priv_group2"
SELECT dogrant_ok(); -- ok: SECURITY DEFINER conveys ADMIN
-NOTICE: role "regress_priv_user5" is already a member of role "regress_priv_group2"
+NOTICE: role "regress_priv_user5" has already been granted membership in role "regress_priv_group2" by role "regress_priv_user4"
dogrant_ok
------------
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index b696628238..292dc08797 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -98,11 +98,9 @@ DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
--- fail, can't drop regress_createrole yet, due to outstanding grants
-DROP ROLE regress_createrole;
-
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
+DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -123,9 +121,6 @@ DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
--- ok, dropped the other roles first so this is ok now
-DROP ROLE regress_createrole;
-
-- fail, role still owns database objects
DROP ROLE regress_tenant;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 66834e32a7..034ebbbf94 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -37,6 +37,32 @@ CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- circular ADMIN OPTION grants should be disallowed
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION GRANTED BY regress_priv_user2;
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION GRANTED BY regress_priv_user3;
+
+-- need CASCADE to revoke grant or admin option if dependent grants exist
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2; -- fail
+REVOKE regress_priv_user1 FROM regress_priv_user2; -- fail
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+BEGIN;
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ROLLBACK;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+
+-- inferred grantor must be a role with ADMIN OPTION
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user2 TO regress_priv_user3;
+SET ROLE regress_priv_user3;
+GRANT regress_priv_user1 TO regress_priv_user4;
+SELECT grantor::regrole FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole and member = 'regress_priv_user4'::regrole;
+RESET ROLE;
+REVOKE regress_priv_user2 FROM regress_priv_user3;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
+
-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
@@ -67,6 +93,7 @@ CREATE USER regress_priv_user5;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
+GRANT regress_priv_user9 TO regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
GRANT pg_read_all_settings TO regress_priv_user9 WITH ADMIN OPTION;
@@ -75,11 +102,12 @@ SET SESSION AUTHORIZATION regress_priv_user9;
GRANT pg_read_all_settings TO regress_priv_user10;
SET SESSION AUTHORIZATION regress_priv_user8;
-REVOKE pg_read_all_settings FROM regress_priv_user10;
+REVOKE pg_read_all_settings FROM regress_priv_user10 GRANTED BY regress_priv_user9;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user9;
REVOKE pg_read_all_settings FROM regress_priv_user9;
RESET SESSION AUTHORIZATION;
+REVOKE regress_priv_user9 FROM regress_priv_user8;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
@@ -110,9 +138,10 @@ ALTER FUNCTION leak(integer,integer) OWNER TO regress_priv_user1;
-- test owner privileges
+GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY regress_priv_role; -- error, doesn't have ADMIN OPTION
GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY CURRENT_ROLE;
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY foo; -- error
-REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- error
+REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- warning, noop
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_USER;
REVOKE regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_ROLE;
DROP ROLE regress_priv_role;
--
2.24.3 (Apple Git-128)
On Thu, Jul 28, 2022 at 12:09 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Tue, Jul 26, 2022 at 12:46 PM Robert Haas <robertmhaas@gmail.com>
wrote:I believe that these patches are mostly complete, but I think that
dumpRoleMembership() probably needs some more work. I don't know what
exactly, but there's nothing to cause it to dump the role grants in an
order that will create dependent grants after the things that they
depend on, which seems essential.OK, so I fixed that, and also updated the documentation a bit more. I
think these patches are basically done, and I'd like to get them
committed before too much more time goes by, because I have other
things that depend on this which I also want to get done for this
release. Anybody object?I'm hoping not, because, while this is a behavior change, the current
state of play in this area is just terrible. To my knowledge, this is
the only place in the system where we allow a dangling OID reference
in a catalog table to persist after the object to which it refers has
been dropped. I believe it's also the object type where multiple
grants by different grantors aren't tracked separately, and where the
grantor need not themselves have the permission being granted. It
doesn't really look like any of these things were intentional behavior
so much as just ... nobody ever bothered to write the code to make it
work properly. I'm hoping the fact that I have now done that will be
viewed as a good thing, but maybe that won't turn out to be the case.
I suggest changing \du memberof to output something like this:
select r.rolname,
array(
select format('%s:%s/%s',
b.rolname,
case when m.admin_option then 'admin' else 'member' end,
g.rolname)
from pg_catalog.pg_auth_members m
join pg_catalog.pg_roles b on (m.roleid = b.oid)
join pg_catalog.pg_roles g on (m.grantor = g.oid)
where m.member = r.oid
) as memberof
from pg_catalog.pg_roles r where r.rolname !~ '^pg_';
rolname | memberof
---------+------------------------------------
vagrant | {}
o | {}
a | {o:admin/p,o:admin/vagrant}
x | {o:admin/a,p:member/vagrant}
b | {o:admin/a}
p | {o:admin/vagrant}
y | {x:member/vagrant}
q | {}
r | {q:admin/vagrant}
s | {}
t | {q:admin/vagrant,s:member/vagrant}
(needs sorting, tried to model it after ACL - column privileges
specifically)
=> \dp mytable
Access privileges
Schema | Name | Type | Access privileges | Column privileges |
Policies
--------+---------+-------+-----------------------+-----------------------+----------
public | mytable | table | miriam=arwdDxt/miriam+| col1: +|
| | | =r/miriam +| miriam_rw=rw/miriam |
| | | admin=arw/miriam | |
(1 row)
If we aren't dead set on having \du and \dg be aliases for each other I'd
rather redesign \dg (or add a new meta-command) to be a group-centric view
of this exact same data instead of user-centric one. Namely it has a
"members" column instead of "memberof" and have it output, one line per
member:
user=[admin|member]/grantor
I looked over the rest of the patch and played with the circularity a bit,
which motivated the expanded info in \du, and the confirmation that two
separate admin grants that are not circular can exist.
I don't have any meaningful insight as to breaking things with these
changes but I am strongly in favor of tightening this up and formalizing it.
David J.
On Thu, Jul 28, 2022 at 5:17 PM David G. Johnston
<david.g.johnston@gmail.com> wrote:
I suggest changing \du memberof to output something like this:
rolname | memberof
---------+------------------------------------
vagrant | {}
r | {q:admin/vagrant}
t | {q:admin/vagrant,s:member/vagrant}(needs sorting, tried to model it after ACL - column privileges specifically)
I don't know. I agree with you that we should probably think about
changing the \du output, but I'm not sure if I like this particular
idea about how to do it. I mean, the ACL format that we use for tables
and other objects is basically an internal format which we throw at
the user, hoping they'll know how to interpret it. I don't know if
it's what we should pick when we don't have that kind of internal
format already. On the other hand, consistency is worth something, and
I'm not sure that I have a better idea.
https://commitfest.postgresql.org/38/3744/ might affect what we want
to do here, too.
If we aren't dead set on having \du and \dg be aliases for each other I'd rather redesign \dg (or add a new meta-command) to be a group-centric view of this exact same data instead of user-centric one. Namely it has a "members" column instead of "memberof" and have it output, one line per member:
user=[admin|member]/grantor
That seems like a topic for a separate thread, but I agree that a
flipped view of this data would be more useful than using two letters
of the alphabet for exactly the same thing, especially given that
we're pretty short on unused letters.
I don't have any meaningful insight as to breaking things with these changes but I am strongly in favor of tightening this up and formalizing it.
Cool.
--
Robert Haas
EDB: http://www.enterprisedb.com
Greetings,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Tue, Jul 26, 2022 at 12:46 PM Robert Haas <robertmhaas@gmail.com> wrote:
I believe that these patches are mostly complete, but I think that
dumpRoleMembership() probably needs some more work. I don't know what
exactly, but there's nothing to cause it to dump the role grants in an
order that will create dependent grants after the things that they
depend on, which seems essential.OK, so I fixed that, and also updated the documentation a bit more. I
think these patches are basically done, and I'd like to get them
committed before too much more time goes by, because I have other
things that depend on this which I also want to get done for this
release. Anybody object?
Thanks for working on this.
Subject: [PATCH v3 1/2] Ensure that pg_auth_members.grantor is always valid.
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 94135fdd6b..258943094a 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -919,7 +920,7 @@ DropRole(DropRoleStmt *stmt)
/*
* Scan the pg_authid relation to find the Oid of the role(s) to be
- * deleted.
+ * deleted and perform prleliminary permissions and sanity checks.
Should be preliminary, I'm guessing.
Overall, this looks like a solid improvement.
Subject: [PATCH v3 2/2] Make role grant system more consistent with other
privileges.
Previously, only the superuser could specify GRANTED BY with a user
other than the current user. Relax that rule to allow the grantor
to be any role whose privileges the current user posseses. This
doesn't improve compatibility with what we do for other object types,
where support for GRANTED BY is entirely vestigial, but it makes this
feature more usable and seems to make sense to change at the same time
we're changing related behaviors.
Presumably the GRANTED BY user in this case still has to have the
ability to have performed the GRANT themselves? Looks that way below
and it's just the commit message, but was the first question that came
to mind when I read through this.
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index f744b05b55..1f828d386a 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -267,8 +267,14 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
<para>
If <literal>GRANTED BY</literal> is specified, the grant is recorded as
- having been done by the specified role. Only database superusers may
- use this option, except when it names the same role executing the command.
+ having been done by the specified role. A user can only attribute a grant
+ to another role if they possess the privileges of that role. A role can
+ only be recorded as a grantor if has <literal>ADMIN OPTION</literal> on
Should be: if they have
+ a role or is the bootstrap superuser. When a grant is recorded as having
on *that* role seems like it'd be better. And maybe 'or if they are the
bootstrap superuser'?
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 258943094a..8ab2fecf3a 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -805,11 +842,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
if (stmt->action == +1) /* add members to role */
AddRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- GetUserId(), false);
+ InvalidOid, false);
else if (stmt->action == -1) /* drop members from role */
DelRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- false);
+ InvalidOid, false, DROP_RESTRICT); /* XXX sketchy - hint
+ * may mislead */
}
This comment seems a little concerning..? Also isn't very clear.
@@ -1027,7 +1065,7 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
- Form_pg_auth_members authmem_form;
+ Form_pg_auth_members authmem_form;
authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
deleteSharedDependencyRecordsFor(AuthMemRelationId,
Some random whitespace changes that seems a bit odd given that they
should have been already correct thanks to pgindent- will these end up
just getting undone again?
@@ -1543,14 +1578,94 @@ AddRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INVALID_GRANT_OPERATION),
errmsg("role \"%s\" is a member of role \"%s\"",
rolename, get_rolespec_name(memberRole))));
+ }
+
+ /*
+ * Disallow attempts to grant ADMIN OPTION back to a user who granted it
+ * to you, similar to what check_circularity does for ACLs. We want the
+ * chains of grants to remain acyclic, so that it's always possible to use
+ * REVOKE .. CASCADE to clean up all grants that depend on the one being
+ * revoked.
+ *
+ * NB: This check might look redundant with the check for membership
+ * loops above, but it isn't. That's checking for role-member loop (e.g.
+ * A is a member of B and B is a member of A) while this is checking for
+ * a member-grantor loop (e.g. A gave ADMIN OPTION to X to B and now B, who
+ * has no other source of ADMIN OPTION on X, tries to give ADMIN OPTION
+ * on X back to A).
+ */
With this exact scenario, wouldn't it just be a no-op as A must have
ADMIN OPTION already on X? The spec says that no cycles of role
authorizations are allowed. Presumably we'd continue this for other
GRANT'able things which can be further GRANT'd (should we add them) in
the future? Just trying to think ahead a bit here in case it's
worthwhile. Those would likely be ABC WITH GRANT OPTION too, right?
+ if (admin_opt && grantorId != BOOTSTRAP_SUPERUSERID)
+ {
+ CatCList *memlist;
+ RevokeRoleGrantAction *actions;
+ int i;
+
+ /* Get the list of members for this role. */
+ memlist = SearchSysCacheList1(AUTHMEMROLEMEM,
+ ObjectIdGetDatum(roleid));
+
+ /*
+ * Figure out what would happen if we removed all existing grants to
+ * every role to which we've been asked to make a new grant.
+ */
+ actions = initialize_revoke_actions(memlist);
+ foreach(iditem, memberIds)
+ {
+ Oid memberid = lfirst_oid(iditem);
+
+ if (memberid == BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_GRANT_OPERATION),
+ errmsg("grants with admin options cannot be circular")));
+ plan_member_revoke(memlist, actions, memberid);
+ }
I don't see a regression test added which produces the above error
message. The memberid == BOOTSTRAP_SUPERUSERID seems odd too?
+ /*
+ * If the result would be that the grantor role would no longer have
+ * the ability to perform the grant, then the proposed grant would
+ * create a circularity.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (actions[i] == RRG_NOOP &&
+ authmem_form->member == grantorId &&
+ authmem_form->admin_option)
+ break;
+ }
+ if (i >= memlist->n_members)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_GRANT_OPERATION),
+ errmsg("admin options cannot be granted back to your own grantor")));
I do see this in the regression tests. There though, the GRANTs are
being performed by someone else, so saying 'your' isn't quite right.
I'm trying to get this review out sooner than later and so I might be
missing something, but looking at the regression test for this and these
error messages, feels like the 'circular' error message makes more sense
than the 'your own grantor' message that actually ends up being
returned in that regression test.
@@ -1637,17 +1737,22 @@ AddRoleMems(const char *rolename, Oid roleid,
* roleid: OID of role to del from
* memberSpecs: list of RoleSpec of roles to del (used only for error messages)
* memberIds: OIDs of roles to del
+ * grantorId: who is revoking the membership
* admin_opt: remove admin option only?
*/
static void
DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
- bool admin_opt)
+ Oid grantorId, bool admin_opt, DropBehavior behavior)
The comment above DropRoleMems missed adding a description for
the 'behavior' parameter.
@@ -1669,40 +1774,69 @@ DelRoleMems(const char *rolename, Oid roleid,
+ /*
+ * We may need to recurse to dependent privileges if DROP_CASCADE was
+ * specified, or refuse to perform the operation if dependent privileges
+ * exist and DROP_RECURSE was specified. plan_single_revoke() will
+ * figure out what to do with each catalog tuple.
+ */
Pretty sure that should be DROP_RESTRICT, not DROP_RECURSE.
+/*
+ * Sanity-check, or infer, the grantor for a GRANT or REVOKE statement
+ * targeting a role.
+ *
+ * The grantor must always be either a role with ADMIN OPTION on the role in
+ * which membership is being granted, or the bootstrap superuser. This is
+ * similar to the restriction enforced by select_best_grantor, except that
+ * roles don't have owners, so we regard the bootstrap superuser as the
+ * implicit owner.
+ *
+ * The return value is the OID to be regarded as the grantor when executing
+ * the operation.
+ */
+static Oid
+check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
As this also does some permission checks, it seems like it'd be good to
mention that in the function description. Maybe also in the places that
call into this function with the expectation that the privilege check
will be taken care of here.
Indeed, I wonder if maybe we should really split this function into two
as the "give me what the best grantor is" is a fair bit different from
"check if this user has permission to grant as this role". As noted in
the comments, the current function also only does privilege checking in
some case- when InvalidOid is passed in we've also already done
permissions checks to make sure that the GRANT will succeed.
+/*
+ * Initialize an array of RevokeRoleGrantAction objects.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * We here construct an array indicating that no actions are to be performed;
+ * that is, every element is intiially RRG_NOOP.
+ */
"We here construct" seems odd wording to me. Maybe "Here we construct"?
Thanks,
Stephen
Stephen Frost <sfrost@snowman.net> writes:
* Robert Haas (robertmhaas@gmail.com) wrote:
OK, so I fixed that, and also updated the documentation a bit more. I
think these patches are basically done, and I'd like to get them
committed before too much more time goes by, because I have other
things that depend on this which I also want to get done for this
release. Anybody object?
Thanks for working on this.
Indeed. I've not read the patch, but I just wanted to mention that
the cfbot shows it as failing regression tests on all platforms.
Possibly a conflict with some recent commit?
regards, tom lane
On Sun, Jul 31, 2022 at 11:18 AM Stephen Frost <sfrost@snowman.net> wrote:
Greetings,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Tue, Jul 26, 2022 at 12:46 PM Robert Haas <robertmhaas@gmail.com>
wrote:
+ } + + /* + * Disallow attempts to grant ADMIN OPTION back to a user who granted it + * to you, similar to what check_circularity does for ACLs. We want the + * chains of grants to remain acyclic, so that it's always possible to use + * REVOKE .. CASCADE to clean up all grants that depend on the one being + * revoked. + * + * NB: This check might look redundant with the check for membership + * loops above, but it isn't. That's checking for role-member loop (e.g. + * A is a member of B and B is a member of A) while this is checking for + * a member-grantor loop (e.g. A gave ADMIN OPTION to X to B and now B, who + * has no other source of ADMIN OPTION on X, tries to give ADMIN OPTION + * on X back to A). + */With this exact scenario, wouldn't it just be a no-op as A must have
ADMIN OPTION already on X? The spec says that no cycles of role
authorizations are allowed.
Role A must have admin option for X to grant membership in X (with or
without admin option) to B. But that doesn't preclude A from getting
another admin option from someone else. That someone else cannot be
someone to whom they gave admin option to however. So B cannot grant admin
option back to A but role P could if it was basically a sibling of A (i.e.,
both getting their initial admin option from someone else).
If they do have admin option twice it should be possible to drop one of
them, the prohibition should be on dropping the only admin option
permission a role has for some other role. The commit message for 2
contemplates this though I haven't gone through the revocation code in
detail.
I'm trying to get this review out sooner than later and so I might be
missing something, but looking at the regression test for this and these
error messages, feels like the 'circular' error message makes more sense
than the 'your own grantor' message that actually ends up being
returned in that regression test.
Having a more specific error seems reasonable, faster to track down what
the problem is.
I think that the whole graph dynamic of this might need some presentation
work (messages and/or psql and/or functions) ; but assuming the errors are
handled improved messages and/or presentation of graphs can be a separate
enhancement.
David J.
Greetings,
On Sun, Jul 31, 2022 at 11:44 David G. Johnston <david.g.johnston@gmail.com>
wrote:
On Sun, Jul 31, 2022 at 11:18 AM Stephen Frost <sfrost@snowman.net> wrote:
Greetings,
* Robert Haas (robertmhaas@gmail.com) wrote:
On Tue, Jul 26, 2022 at 12:46 PM Robert Haas <robertmhaas@gmail.com>
wrote:
+ } + + /* + * Disallow attempts to grant ADMIN OPTION back to a user who granted it + * to you, similar to what check_circularity does for ACLs. We want the + * chains of grants to remain acyclic, so that it's always possible to use + * REVOKE .. CASCADE to clean up all grants that depend on the one being + * revoked. + * + * NB: This check might look redundant with the check for membership + * loops above, but it isn't. That's checking for role-member loop (e.g. + * A is a member of B and B is a member of A) while this is checking for + * a member-grantor loop (e.g. A gave ADMIN OPTION to X to B and now B, who + * has no other source of ADMIN OPTION on X, tries to give ADMIN OPTION + * on X back to A). + */With this exact scenario, wouldn't it just be a no-op as A must have
ADMIN OPTION already on X? The spec says that no cycles of role
authorizations are allowed.
I’ve realized that what I hadn’t been contemplating here is actually that
the GRANT from B to A for X wouldn’t be redundant because grantor is part
of the key (A got the right from someone else, but this would be giving it
to A from B and therefore would be distinct and would also create a loop
which is no good). Haven’t got a good idea on how to improve on the
comment based off of that though it still feels like it could be clearer.
If I think of something, I’ll share it.
Role A must have admin option for X to grant membership in X (with or
without admin option) to B. But that doesn't preclude A from getting
another admin option from someone else. That someone else cannot be
someone to whom they gave admin option to however. So B cannot grant admin
option back to A but role P could if it was basically a sibling of A (i.e.,
both getting their initial admin option from someone else).
Right but that wasn’t what I had been trying to get at above.
If they do have admin option twice it should be possible to drop one of
them, the prohibition should be on dropping the only admin option
permission a role has for some other role. The commit message for 2
contemplates this though I haven't gone through the revocation code in
detail.
Yes, think I agree with this also- if A has been given the WITH ADMIN right
from Q and P to GRANT X to other roles, and uses that to GRANT X to B, then
the GRANT of X to B should be retained even if Q decides to revoke their
GRANT as A still has the right from P. If both remove the right, however,
either B should lose the right (if CASCADE was passed in) or an error
should be returned saying that there’s a dependent GRANT and CASCADE wasn’t
given.
I'm trying to get this review out sooner than later and so I might be
missing something, but looking at the regression test for this and these
error messages, feels like the 'circular' error message makes more sense
than the 'your own grantor' message that actually ends up being
returned in that regression test.Having a more specific error seems reasonable, faster to track down what
the problem is.
Yeah, but also making sure that all the error messages we have in this area
are in the regression test output would be good.
Makes me wonder if we might try to figure out a way to globally check for
that. I suppose one could review coverage.p.o for any ereport() calls that
aren’t ever called. I wonder what that would turn up.
I think that the whole graph dynamic of this might need some presentation
work (messages and/or psql and/or functions) ; but assuming the errors are
handled improved messages and/or presentation of graphs can be a separate
enhancement.
Yes, we can further improve this later too but that doesn’t mean we should
just commit this as-is when some deficiencies have been pointed out. If the
only comments were “would be good to improve this error message but I
haven’t got a great idea how”, then sure, but there were other items
pointed out which were clear corrections and we should make sure to cover
in the regression tests all these scenarios that we are checking for and
erroring on, lest we end up breaking them unintentionally later.
Thanks,
Stephen
Show quoted text
On Sun, Jul 31, 2022 at 2:18 PM Stephen Frost <sfrost@snowman.net> wrote:
Thanks for working on this.
Thanks for the review.
Previously, only the superuser could specify GRANTED BY with a user
other than the current user. Relax that rule to allow the grantor
to be any role whose privileges the current user posseses. This
doesn't improve compatibility with what we do for other object types,
where support for GRANTED BY is entirely vestigial, but it makes this
feature more usable and seems to make sense to change at the same time
we're changing related behaviors.Presumably the GRANTED BY user in this case still has to have the
ability to have performed the GRANT themselves? Looks that way below
and it's just the commit message, but was the first question that came
to mind when I read through this.
Yes. The previous paragraph in this commit message seems to cover this
point pretty thoroughly.
<para> If <literal>GRANTED BY</literal> is specified, the grant is recorded as - having been done by the specified role. Only database superusers may - use this option, except when it names the same role executing the command. + having been done by the specified role. A user can only attribute a grant + to another role if they possess the privileges of that role. A role can + only be recorded as a grantor if has <literal>ADMIN OPTION</literal> onShould be: if they have
+ a role or is the bootstrap superuser. When a grant is recorded as having
on *that* role seems like it'd be better. And maybe 'or if they are the
bootstrap superuser'?
Will fix.
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c index 258943094a..8ab2fecf3a 100644 --- a/src/backend/commands/user.c +++ b/src/backend/commands/user.c @@ -805,11 +842,12 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt) if (stmt->action == +1) /* add members to role */ AddRoleMems(rolename, roleid, rolemembers, roleSpecsToIds(rolemembers), - GetUserId(), false); + InvalidOid, false); else if (stmt->action == -1) /* drop members from role */ DelRoleMems(rolename, roleid, rolemembers, roleSpecsToIds(rolemembers), - false); + InvalidOid, false, DROP_RESTRICT); /* XXX sketchy - hint + * may mislead */ }This comment seems a little concerning..? Also isn't very clear.
Oh right. That was a note to myself to look into that more. And then I
didn't. I'll look into that more and report back.
@@ -1027,7 +1065,7 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan))) { - Form_pg_auth_members authmem_form; + Form_pg_auth_members authmem_form;authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
deleteSharedDependencyRecordsFor(AuthMemRelationId,Some random whitespace changes that seems a bit odd given that they
should have been already correct thanks to pgindent- will these end up
just getting undone again?
Will fix.
@@ -1543,14 +1578,94 @@ AddRoleMems(const char *rolename, Oid roleid, (errcode(ERRCODE_INVALID_GRANT_OPERATION), errmsg("role \"%s\" is a member of role \"%s\"", rolename, get_rolespec_name(memberRole)))); + } + + /* + * Disallow attempts to grant ADMIN OPTION back to a user who granted it + * to you, similar to what check_circularity does for ACLs. We want the + * chains of grants to remain acyclic, so that it's always possible to use + * REVOKE .. CASCADE to clean up all grants that depend on the one being + * revoked. + * + * NB: This check might look redundant with the check for membership + * loops above, but it isn't. That's checking for role-member loop (e.g. + * A is a member of B and B is a member of A) while this is checking for + * a member-grantor loop (e.g. A gave ADMIN OPTION to X to B and now B, who + * has no other source of ADMIN OPTION on X, tries to give ADMIN OPTION + * on X back to A). + */With this exact scenario, wouldn't it just be a no-op as A must have
ADMIN OPTION already on X? The spec says that no cycles of role
authorizations are allowed. Presumably we'd continue this for other
GRANT'able things which can be further GRANT'd (should we add them) in
the future? Just trying to think ahead a bit here in case it's
worthwhile. Those would likely be ABC WITH GRANT OPTION too, right?
I don't believe there's anything novel here - at least there isn't
supposed to be. Here's the equivalent with table privileges:
rhaas=# create table t1();
CREATE TABLE
rhaas=# create role foo;
CREATE ROLE
rhaas=# create role bar;
CREATE ROLE
rhaas=# grant select on t1 to foo with grant option;
GRANT
rhaas=# set role foo;
SET
rhaas=> grant select on t1 to bar with grant option;
GRANT
rhaas=> set role bar;
SET
rhaas=> grant select on t1 to foo with grant option;
ERROR: grant options cannot be granted back to your own grantor
+ if (admin_opt && grantorId != BOOTSTRAP_SUPERUSERID) + { + CatCList *memlist; + RevokeRoleGrantAction *actions; + int i; + + /* Get the list of members for this role. */ + memlist = SearchSysCacheList1(AUTHMEMROLEMEM, + ObjectIdGetDatum(roleid)); + + /* + * Figure out what would happen if we removed all existing grants to + * every role to which we've been asked to make a new grant. + */ + actions = initialize_revoke_actions(memlist); + foreach(iditem, memberIds) + { + Oid memberid = lfirst_oid(iditem); + + if (memberid == BOOTSTRAP_SUPERUSERID) + ereport(ERROR, + (errcode(ERRCODE_INVALID_GRANT_OPERATION), + errmsg("grants with admin options cannot be circular"))); + plan_member_revoke(memlist, actions, memberid); + }I don't see a regression test added which produces the above error
message. The memberid == BOOTSTRAP_SUPERUSERID seems odd too?
Do we guarantee that the regression tests are running as the bootstrap
superuser, or just as some superuser? I am a bit reluctant to add a
regression test that assumes the former unless we're assuming it
already. For 'make check' it doesn't matter but 'make installcheck' is
another story.
The memberid == BOOTSTRAP_SUPERUSERID case is very much intentional.
The code will detect any loops in the catalog, but the implicit grant
to the bootstrap superuser doesn't exist in the catalog
representation, so it needs a separate check. I think I should sync
the two error messages though, i.e. this should say "admin options
cannot be granted back to your own grantor" like the other one just
below.
I do see this in the regression tests. There though, the GRANTs are
being performed by someone else, so saying 'your' isn't quite right.
I'm trying to get this review out sooner than later and so I might be
missing something, but looking at the regression test for this and these
error messages, feels like the 'circular' error message makes more sense
than the 'your own grantor' message that actually ends up being
returned in that regression test.
I think it's a bit off too, but I didn't invent it. See check_circularity():
if ((ACLITEM_GET_GOPTIONS(*mod_aip) & ~own_privs) != 0)
ereport(ERROR,
(errcode(ERRCODE_INVALID_GRANT_OPERATION),
errmsg("grant options cannot be granted back to your
own grantor")));
Looks like Tom Lane, vintage 2004, 4b2dafcc0b1a579ef5daaa2728223006d1ff98e9.
The comment above DropRoleMems missed adding a description for
the 'behavior' parameter.
Will fix.
@@ -1669,40 +1774,69 @@ DelRoleMems(const char *rolename, Oid roleid, + /* + * We may need to recurse to dependent privileges if DROP_CASCADE was + * specified, or refuse to perform the operation if dependent privileges + * exist and DROP_RECURSE was specified. plan_single_revoke() will + * figure out what to do with each catalog tuple. + */Pretty sure that should be DROP_RESTRICT, not DROP_RECURSE.
I'm pretty sure you are right.
+/* + * Sanity-check, or infer, the grantor for a GRANT or REVOKE statement + * targeting a role. + * + * The grantor must always be either a role with ADMIN OPTION on the role in + * which membership is being granted, or the bootstrap superuser. This is + * similar to the restriction enforced by select_best_grantor, except that + * roles don't have owners, so we regard the bootstrap superuser as the + * implicit owner. + * + * The return value is the OID to be regarded as the grantor when executing + * the operation. + */ +static Oid +check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)As this also does some permission checks, it seems like it'd be good to
mention that in the function description. Maybe also in the places that
call into this function with the expectation that the privilege check
will be taken care of here.Indeed, I wonder if maybe we should really split this function into two
as the "give me what the best grantor is" is a fair bit different from
"check if this user has permission to grant as this role". As noted in
the comments, the current function also only does privilege checking in
some case- when InvalidOid is passed in we've also already done
permissions checks to make sure that the GRANT will succeed.
I'll think about this some more, but I don't want to commit to
changing it very much. IMHO, the whole split into AddRoleMems() and
DelRoleMems() for what is basically the same operation seems pretty
dubious, but this commit's intended purpose is to clean up the
behavior rather than to rewrite the code. So I left the existing logic
in AddRoleMems() and DelRoleMems() alone, and when I realized I needed
something else that was mostly common to both, I made this function
instead of duplicating the logic in two places. I realize there are
other ways that it could be split up, and maybe some of those are
better in theory, but they'd likely also expand the scope of the patch
to things that it doesn't quite need to touch. I'm not real keen to go
there. That can be done later, in a separate patch, or never, and I
don't think we'll really be any the worse for it.
+/* + * Initialize an array of RevokeRoleGrantAction objects. + * + * 'memlist' should be a list of all grants for the target role. + * + * We here construct an array indicating that no actions are to be performed; + * that is, every element is intiially RRG_NOOP. + */"We here construct" seems odd wording to me. Maybe "Here we construct"?
It seems completely fine to me, but I'll change it somehow to avoid
annoying you. :-)
--
Robert Haas
EDB: http://www.enterprisedb.com
On Sun, Jul 31, 2022 at 2:34 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Indeed. I've not read the patch, but I just wanted to mention that
the cfbot shows it as failing regression tests on all platforms.
Possibly a conflict with some recent commit?
I can't see this on cfbot - either I don't know how to use it
properly, which is quite possible, or the results aren't showing up
because of the close of the July CommitFest.
I tried a rebase locally and it didn't seem to change anything
material, not even context lines.
Can you provide a link or something that I can look at?
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
On Sun, Jul 31, 2022 at 2:34 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
Indeed. I've not read the patch, but I just wanted to mention that
the cfbot shows it as failing regression tests on all platforms.
Possibly a conflict with some recent commit?
I can't see this on cfbot - either I don't know how to use it
properly, which is quite possible, or the results aren't showing up
because of the close of the July CommitFest.
I think the latter --- the cfbot thinks the July CF is no longer relevant,
but Jacob hasn't yet moved your patches forward. You could wait for
him to do that, or do it yourself.
(Probably our nonexistent SOP manual for CFMs ought to say "don't
close the old CF till you've moved everything forward".)
regards, tom lane
On Mon, Aug 1, 2022 at 1:38 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I think the latter --- the cfbot thinks the July CF is no longer relevant,
but Jacob hasn't yet moved your patches forward. You could wait for
him to do that, or do it yourself.
Done. New patches attached.
Changes in v4, for 0001:
- Typo fix.
- Whitespace fixes.
Changes in v4, for 0002:
- Remove "XXX sketchy" comment because the thing in question turns out
not to be sketchy. It has to do with the behavior of ALTER GROUP ..
DROP USER and, having investigated the situation, I think the
messaging is clear enough.
- But just to be sure, add a note to the ALTER GROUP documentation to
try to make things more clear.
- Wording fixes to the "If <literal>GRANTED BY</literal> is
specified..." paragraph of the GRANT documentation. I reworded this a
bit more extensively than what Stephen proposed. Hopefully this is
clearer now, or at least no longer missing any words.
- Change message to "admin option cannot be granted back to your own
grantor". The choice of message is intended to be consistent with the
existing message "grant options cannot be granted back to your own
grantor," but while there's one grant option per privilege, there's
only one admin option. Stephen suggested adopting a message that I had
meant to take out of the version I posted, but which ended up
surviving in one place, "grants with admin options cannot be
circular". And we could still decide to do something like that, but my
enthusiasm for that direction was considerably reduced when I realized
that "circular" is not very clear at all, because there are multiple
kinds of circularities (role-member, member-grantor).
- Fix comment to say DROP_RESTRICT instead of DROP_RECURSE.
- Make the comment for check_role_grantor() longer so that it can
better explain itself.
- Rephrase part of the header comment for initialize_revoke_actions()
because Stephen found it awkward.
- Whitespace fixes.
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v4-0002-Make-role-grant-system-more-consistent-with-other.patchapplication/octet-stream; name=v4-0002-Make-role-grant-system-more-consistent-with-other.patchDownload
From 388932bc9b90f322bc05e0d1ce79f4e791db5840 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 1 Aug 2022 15:29:36 -0400
Subject: [PATCH v4 2/2] Make role grant system more consistent with other
privileges.
Previously, membership of role A in role B could be recorded in the
catalog tables only once. This meant that a new grant of role A to
role B would overwrite the previous grant. For other object types, a
new grant of permission on an object - in this case role A - exists
along side the existing grant provided that the grantor is different.
Either grant can be revoked independently of the other, and
permissions remain so long as at least one grant remains. Make role
grants work similarly.
Previously, when granting membership in a role, the superuser could
specify any role whatsoever as the grantor, but for other object types,
the grantor of record must be either the owner of the object, or a
role that currently has privileges to perform a similar GRANT.
Implement the same scheme for role grants, treating the bootstrap
superuser as the role owner since roles do not have owners. This means
that attempting to revoke a grant, or admin option on a grant, can now
fail if there are dependent privileges, and that CASCADE can be used
to revoke these. It also means that you can't grant ADMIN OPTION on
a role back to a user who granted it directly or indirectly to you,
similar to how you can't give WITH GRANT OPTION on a privilege back
to a role which granted it directly or indirectly to you.
Previously, only the superuser could specify GRANTED BY with a user
other than the current user. Relax that rule to allow the grantor
to be any role whose privileges the current user posseses. This
doesn't improve compatibility with what we do for other object types,
where support for GRANTED BY is entirely vestigial, but it makes this
feature more usable and seems to make sense to change at the same time
we're changing related behaviors.
---
doc/src/sgml/ref/alter_group.sgml | 6 +-
doc/src/sgml/ref/grant.sgml | 12 +-
doc/src/sgml/ref/revoke.sgml | 12 +-
src/backend/commands/user.c | 545 +++++++++++++++++++---
src/backend/parser/gram.y | 2 +
src/backend/utils/adt/acl.c | 47 +-
src/backend/utils/cache/syscache.c | 8 +-
src/bin/pg_dump/pg_dumpall.c | 177 ++++++-
src/include/catalog/pg_auth_members.h | 4 +-
src/include/utils/acl.h | 1 +
src/test/regress/expected/create_role.out | 16 +-
src/test/regress/expected/privileges.out | 62 ++-
src/test/regress/sql/create_role.sql | 7 +-
src/test/regress/sql/privileges.sql | 33 +-
14 files changed, 798 insertions(+), 134 deletions(-)
diff --git a/doc/src/sgml/ref/alter_group.sgml b/doc/src/sgml/ref/alter_group.sgml
index fa4a8df912..b9e641818c 100644
--- a/doc/src/sgml/ref/alter_group.sgml
+++ b/doc/src/sgml/ref/alter_group.sgml
@@ -52,7 +52,11 @@ ALTER GROUP <replaceable class="parameter">group_name</replaceable> RENAME TO <r
equivalent to granting or revoking membership in the role named as the
<quote>group</quote>; so the preferred way to do this is to use
<link linkend="sql-grant"><command>GRANT</command></link> or
- <link linkend="sql-revoke"><command>REVOKE</command></link>.
+ <link linkend="sql-revoke"><command>REVOKE</command></link>. Note that
+ <command>GRANT</command> and <command>REVOKE</command> have additional
+ options which are not available with this command, such as the ability
+ to grant and revoke <literal>ADMIN OPTION</literal>, and the ability to
+ specify the grantor.
</para>
<para>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index f744b05b55..2fd0f34d55 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -267,8 +267,14 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
<para>
If <literal>GRANTED BY</literal> is specified, the grant is recorded as
- having been done by the specified role. Only database superusers may
- use this option, except when it names the same role executing the command.
+ having been done by the specified role. A user can only attribute a grant
+ to another role if they possess the privileges of that role. The role
+ recorded as the grantor must have <literal>ADMIN OPTION</literal> on the
+ target role, unless it is the bootstrap superuser. When a grant is recorded
+ as having a grantor other than the bootstrap superuser, it depends on the
+ grantor continuing to posess <literal>ADMIN OPTION</literal> on the role;
+ so, if <literal>ADMIN OPTION</literal> is revoked, dependent grants must
+ be revoked as well.
</para>
<para>
@@ -333,7 +339,7 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
owner of the affected object. In particular, privileges granted via
such a command will appear to have been granted by the object owner.
(For role membership, the membership appears to have been granted
- by the containing role itself.)
+ by the bootstrap superuser.)
</para>
<para>
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 62f1971036..16e840458c 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -198,9 +198,10 @@ REVOKE [ ADMIN OPTION FOR ]
<para>
When revoking membership in a role, <literal>GRANT OPTION</literal> is instead
called <literal>ADMIN OPTION</literal>, but the behavior is similar.
- This form of the command also allows a <literal>GRANTED BY</literal>
- option, but that option is currently ignored (except for checking
- the existence of the named role).
+ Note that, in releases prior to <productname>PostgreSQL</productname> 16,
+ dependent privileges were not tracked for grants of role membership,
+ and thus <literal>CASCADE</literal> had no effect for role membership.
+ This is no longer the case.
Note also that this form of the command does not
allow the noise word <literal>GROUP</literal>
in <replaceable class="parameter">role_specification</replaceable>.
@@ -239,7 +240,10 @@ REVOKE [ ADMIN OPTION FOR ]
<para>
If a superuser chooses to issue a <command>GRANT</command> or <command>REVOKE</command>
command, the command is performed as though it were issued by the
- owner of the affected object. Since all privileges ultimately come
+ owner of the affected object. (Since roles do not have owners, in the
+ case of a <command>GRANT</command> of role membership, the command is
+ performed as though it were issued by the bootstrap superuser.)
+ Since all privileges ultimately come
from the object owner (possibly indirectly via chains of grant options),
it is possible for a superuser to revoke all privileges, but this might
require use of <literal>CASCADE</literal> as stated above.
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index fc42b1cfd7..201f1aae7f 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -35,10 +35,32 @@
#include "storage/lmgr.h"
#include "utils/acl.h"
#include "utils/builtins.h"
+#include "utils/catcache.h"
#include "utils/fmgroids.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
+/*
+ * Removing a role grant - or the admin option on it - might recurse to
+ * dependent grants. We use these values to reason about what would need to
+ * be done in such cases.
+ *
+ * RRG_NOOP indicates a grant that would not need to be altered by the
+ * operation.
+ *
+ * RRG_REMOVE_ADMIN_OPTION indicates a grant that would need to have
+ * admin_option set to false by the operation.
+ *
+ * RRG_DELETE_GRANT indicates a grant that would need to be removed entirely
+ * by the operation.
+ */
+typedef enum
+{
+ RRG_NOOP,
+ RRG_REMOVE_ADMIN_OPTION,
+ RRG_DELETE_GRANT
+} RevokeRoleGrantAction;
+
/* Potentially set by pg_upgrade_support functions */
Oid binary_upgrade_next_pg_authid_oid = InvalidOid;
@@ -54,7 +76,22 @@ static void AddRoleMems(const char *rolename, Oid roleid,
Oid grantorId, bool admin_opt);
static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
- bool admin_opt);
+ Oid grantorId, bool admin_opt, DropBehavior behavior);
+static Oid check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId,
+ bool is_grant);
+static RevokeRoleGrantAction *initialize_revoke_actions(CatCList *memlist);
+static bool plan_single_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions,
+ Oid member, Oid grantor,
+ bool revoke_admin_option_only,
+ DropBehavior behavior);
+static void plan_member_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions, Oid member);
+static void plan_recursive_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions,
+ int index,
+ bool revoke_admin_option_only,
+ DropBehavior behavior);
/* Check if current user has createrole privileges */
@@ -449,7 +486,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
AddRoleMems(oldrolename, oldroleid,
thisrole_list,
thisrole_oidlist,
- GetUserId(), false);
+ InvalidOid, false);
ReleaseSysCache(oldroletup);
}
@@ -461,10 +498,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
AddRoleMems(stmt->role, roleid,
adminmembers, roleSpecsToIds(adminmembers),
- GetUserId(), true);
+ InvalidOid, true);
AddRoleMems(stmt->role, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- GetUserId(), false);
+ InvalidOid, false);
/* Post creation hook for new role */
InvokeObjectPostCreateHook(AuthIdRelationId, roleid, 0);
@@ -805,11 +842,11 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
if (stmt->action == +1) /* add members to role */
AddRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- GetUserId(), false);
+ InvalidOid, false);
else if (stmt->action == -1) /* drop members from role */
DelRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- false);
+ InvalidOid, false, DROP_RESTRICT);
}
/*
@@ -1296,7 +1333,7 @@ GrantRole(GrantRoleStmt *stmt)
if (stmt->grantor)
grantor = get_rolespec_oid(stmt->grantor, false);
else
- grantor = GetUserId();
+ grantor = InvalidOid;
grantee_ids = roleSpecsToIds(stmt->grantee_roles);
@@ -1330,7 +1367,7 @@ GrantRole(GrantRoleStmt *stmt)
else
DelRoleMems(rolename, roleid,
stmt->grantee_roles, grantee_ids,
- stmt->admin_opt);
+ grantor, stmt->admin_opt, stmt->behavior);
}
/*
@@ -1431,7 +1468,7 @@ roleSpecsToIds(List *memberNames)
* roleid: OID of role to add to
* memberSpecs: list of RoleSpec of roles to add (used only for error messages)
* memberIds: OIDs of roles to add
- * grantorId: who is granting the membership
+ * grantorId: who is granting the membership (InvalidOid if not set explicitly)
* admin_opt: granting admin option?
*/
static void
@@ -1443,6 +1480,7 @@ AddRoleMems(const char *rolename, Oid roleid,
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
+ Oid currentUserId = GetUserId();
Assert(list_length(memberSpecs) == list_length(memberIds));
@@ -1464,7 +1502,7 @@ AddRoleMems(const char *rolename, Oid roleid,
else
{
if (!have_createrole_privilege() &&
- !is_admin_of_role(grantorId, roleid))
+ !is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
@@ -1483,29 +1521,25 @@ AddRoleMems(const char *rolename, Oid roleid,
ereport(ERROR,
errmsg("role \"%s\" cannot have explicit members", rolename));
- /*
- * The role membership grantor of record has little significance at
- * present. Nonetheless, inasmuch as users might look to it for a crude
- * audit trail, let only superusers impute the grant to a third party.
- */
- if (grantorId != GetUserId() && !superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to set grantor")));
+ /* Validate grantor (and resolve implicit grantor if not specified). */
+ grantorId = check_role_grantor(currentUserId, roleid, grantorId, true);
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
+ /*
+ * Only allow changes to this role by one backend at a time, so that we
+ * can check integrity constraints like the lack of circular ADMIN OPTION
+ * grants without fear of race conditions.
+ */
+ LockSharedObject(AuthIdRelationId, roleid, 0,
+ ShareUpdateExclusiveLock);
+
+ /* Preliminary sanity checks. */
forboth(specitem, memberSpecs, iditem, memberIds)
{
RoleSpec *memberRole = lfirst_node(RoleSpec, specitem);
Oid memberid = lfirst_oid(iditem);
- HeapTuple authmem_tuple;
- HeapTuple tuple;
- Datum new_record[Natts_pg_auth_members] = {0};
- bool new_record_nulls[Natts_pg_auth_members] = {0};
- bool new_record_repl[Natts_pg_auth_members] = {0};
- Form_pg_auth_members authmem_form;
/*
* pg_database_owner is never a role member. Lifting this restriction
@@ -1543,14 +1577,94 @@ AddRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INVALID_GRANT_OPERATION),
errmsg("role \"%s\" is a member of role \"%s\"",
rolename, get_rolespec_name(memberRole))));
+ }
+
+ /*
+ * Disallow attempts to grant ADMIN OPTION back to a user who granted it
+ * to you, similar to what check_circularity does for ACLs. We want the
+ * chains of grants to remain acyclic, so that it's always possible to use
+ * REVOKE .. CASCADE to clean up all grants that depend on the one being
+ * revoked.
+ *
+ * NB: This check might look redundant with the check for membership loops
+ * above, but it isn't. That's checking for role-member loop (e.g. A is a
+ * member of B and B is a member of A) while this is checking for a
+ * member-grantor loop (e.g. A gave ADMIN OPTION to X to B and now B, who
+ * has no other source of ADMIN OPTION on X, tries to give ADMIN OPTION on
+ * X back to A).
+ */
+ if (admin_opt && grantorId != BOOTSTRAP_SUPERUSERID)
+ {
+ CatCList *memlist;
+ RevokeRoleGrantAction *actions;
+ int i;
+
+ /* Get the list of members for this role. */
+ memlist = SearchSysCacheList1(AUTHMEMROLEMEM,
+ ObjectIdGetDatum(roleid));
+
+ /*
+ * Figure out what would happen if we removed all existing grants to
+ * every role to which we've been asked to make a new grant.
+ */
+ actions = initialize_revoke_actions(memlist);
+ foreach(iditem, memberIds)
+ {
+ Oid memberid = lfirst_oid(iditem);
+
+ if (memberid == BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_GRANT_OPERATION),
+ errmsg("admin option cannot be granted back to your own grantor")));
+ plan_member_revoke(memlist, actions, memberid);
+ }
+
+ /*
+ * If the result would be that the grantor role would no longer have
+ * the ability to perform the grant, then the proposed grant would
+ * create a circularity.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (actions[i] == RRG_NOOP &&
+ authmem_form->member == grantorId &&
+ authmem_form->admin_option)
+ break;
+ }
+ if (i >= memlist->n_members)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_GRANT_OPERATION),
+ errmsg("admin option cannot be granted back to your own grantor")));
+
+ ReleaseSysCacheList(memlist);
+ }
+
+ /* Now perform the catalog updates. */
+ forboth(specitem, memberSpecs, iditem, memberIds)
+ {
+ RoleSpec *memberRole = lfirst_node(RoleSpec, specitem);
+ Oid memberid = lfirst_oid(iditem);
+ HeapTuple authmem_tuple;
+ HeapTuple tuple;
+ Datum new_record[Natts_pg_auth_members] = {0};
+ bool new_record_nulls[Natts_pg_auth_members] = {0};
+ bool new_record_repl[Natts_pg_auth_members] = {0};
+ Form_pg_auth_members authmem_form;
/*
* Check if entry for this role/member already exists; if so, give
* warning unless we are adding admin option.
*/
- authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
+ authmem_tuple = SearchSysCache3(AUTHMEMROLEMEM,
ObjectIdGetDatum(roleid),
- ObjectIdGetDatum(memberid));
+ ObjectIdGetDatum(memberid),
+ ObjectIdGetDatum(grantorId));
if (!HeapTupleIsValid(authmem_tuple))
{
authmem_form = NULL;
@@ -1562,8 +1676,9 @@ AddRoleMems(const char *rolename, Oid roleid,
if (!admin_opt || authmem_form->admin_option)
{
ereport(NOTICE,
- (errmsg("role \"%s\" is already a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
+ (errmsg("role \"%s\" has already been granted membership in role \"%s\" by role \"%s\"",
+ get_rolespec_name(memberRole), rolename,
+ GetUserNameFromId(grantorId, false))));
ReleaseSysCache(authmem_tuple);
continue;
}
@@ -1577,28 +1692,12 @@ AddRoleMems(const char *rolename, Oid roleid,
if (HeapTupleIsValid(authmem_tuple))
{
- new_record_repl[Anum_pg_auth_members_grantor - 1] = true;
new_record_repl[Anum_pg_auth_members_admin_option - 1] = true;
tuple = heap_modify_tuple(authmem_tuple, pg_authmem_dsc,
new_record,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
- if (authmem_form->grantor != grantorId)
- {
- Oid *oldmembers = palloc(sizeof(Oid));
- Oid *newmembers = palloc(sizeof(Oid));
-
- /* updateAclDependencies wants to pfree array inputs */
- oldmembers[0] = authmem_form->grantor;
- newmembers[0] = grantorId;
-
- updateAclDependencies(AuthMemRelationId, authmem_form->oid,
- 0, InvalidOid,
- 1, oldmembers,
- 1, newmembers);
- }
-
ReleaseSysCache(authmem_tuple);
}
else
@@ -1637,17 +1736,23 @@ AddRoleMems(const char *rolename, Oid roleid,
* roleid: OID of role to del from
* memberSpecs: list of RoleSpec of roles to del (used only for error messages)
* memberIds: OIDs of roles to del
+ * grantorId: who is revoking the membership
* admin_opt: remove admin option only?
+ * behavior: RESTRICT or CASCADE behavior for recursive removal
*/
static void
DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
- bool admin_opt)
+ Oid grantorId, bool admin_opt, DropBehavior behavior)
{
Relation pg_authmem_rel;
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
+ Oid currentUserId = GetUserId();
+ CatCList *memlist;
+ RevokeRoleGrantAction *actions;
+ int i;
Assert(list_length(memberSpecs) == list_length(memberIds));
@@ -1669,40 +1774,69 @@ DelRoleMems(const char *rolename, Oid roleid,
else
{
if (!have_createrole_privilege() &&
- !is_admin_of_role(GetUserId(), roleid))
+ !is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
rolename)));
}
+ /* Validate grantor (and resolve implicit grantor if not specified). */
+ grantorId = check_role_grantor(currentUserId, roleid, grantorId, false);
+
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
+ /*
+ * Only allow changes to this role by one backend at a time, so that we
+ * can check for things like dependent privileges without fear of race
+ * conditions.
+ */
+ LockSharedObject(AuthIdRelationId, roleid, 0,
+ ShareUpdateExclusiveLock);
+
+ memlist = SearchSysCacheList1(AUTHMEMROLEMEM, ObjectIdGetDatum(roleid));
+ actions = initialize_revoke_actions(memlist);
+
+ /*
+ * We may need to recurse to dependent privileges if DROP_CASCADE was
+ * specified, or refuse to perform the operation if dependent privileges
+ * exist and DROP_RESTRICT was specified. plan_single_revoke() will figure
+ * out what to do with each catalog tuple.
+ */
forboth(specitem, memberSpecs, iditem, memberIds)
{
RoleSpec *memberRole = lfirst(specitem);
Oid memberid = lfirst_oid(iditem);
- HeapTuple authmem_tuple;
- Form_pg_auth_members authmem_form;
- /*
- * Find entry for this role/member
- */
- authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
- ObjectIdGetDatum(roleid),
- ObjectIdGetDatum(memberid));
- if (!HeapTupleIsValid(authmem_tuple))
+ if (!plan_single_revoke(memlist, actions, memberid, grantorId,
+ admin_opt, behavior))
{
ereport(WARNING,
- (errmsg("role \"%s\" is not a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
+ (errmsg("role \"%s\" has not been granted membership in role \"%s\" by role \"%s\"",
+ get_rolespec_name(memberRole), rolename,
+ GetUserNameFromId(grantorId, false))));
continue;
}
+ }
+
+ /*
+ * We now know what to do with each catalog tuple: it should either be
+ * left alone, deleted, or just have the admin_option flag cleared.
+ * Perform the appropriate action in each case.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ if (actions[i] == RRG_NOOP)
+ continue;
+ authmem_tuple = &memlist->members[i]->tuple;
authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
- if (!admin_opt)
+ if (actions[i] == RRG_DELETE_GRANT)
{
/*
* Remove the entry altogether, after first removing its
@@ -1729,15 +1863,298 @@ DelRoleMems(const char *rolename, Oid roleid,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
}
-
- ReleaseSysCache(authmem_tuple);
-
- /* CCI after each change, in case there are duplicates in list */
- CommandCounterIncrement();
}
+ ReleaseSysCacheList(memlist);
+
/*
* Close pg_authmem, but keep lock till commit.
*/
table_close(pg_authmem_rel, NoLock);
}
+
+/*
+ * Sanity-check, or infer, the grantor for a GRANT or REVOKE statement
+ * targeting a role.
+ *
+ * The grantor must always be either a role with ADMIN OPTION on the role in
+ * which membership is being granted, or the bootstrap superuser. This is
+ * similar to the restriction enforced by select_best_grantor, except that
+ * roles don't have owners, so we regard the bootstrap superuser as the
+ * implicit owner.
+ *
+ * If the grantor was not explicitly specified by the user, grantorId should
+ * be passed as InvalidOid, and this function will infer the user to be
+ * recorded as the grantor. In many cases, this will be the current user, but
+ * things get more complicated when the current user doesn't possess ADMIN
+ * OPTION on the role but rather relies on having CREATEROLE privileges, or
+ * on inheriting the privileges of a role which does have ADMIN OPTION. See
+ * below for details.
+ *
+ * If the grantor was specified by the user, then it must be a user that
+ * can legally be recorded as the grantor, as per the rule stated above.
+ * This is an integrity constraint, not a permissions check, and thus even
+ * superusers are subject to this restriction. However, there is also a
+ * permissions check: to specify a role as the grantor, the current user
+ * must possess the privileges of that role. Superusers will always pass
+ * this check, but for non-superusers it may lead to an error.
+ *
+ * The return value is the OID to be regarded as the grantor when executing
+ * the operation.
+ */
+static Oid
+check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
+{
+ /* If the grantor ID was not specified, pick one to use. */
+ if (!OidIsValid(grantorId))
+ {
+ /*
+ * Grants where the grantor is recorded as the bootstrap superuser do
+ * not depend on any other existing grants, so always default to this
+ * interpretation when possible.
+ */
+ if (has_createrole_privilege(currentUserId))
+ return BOOTSTRAP_SUPERUSERID;
+
+ /*
+ * Otherwise, the grantor must either have ADMIN OPTION on the role or
+ * inherit the privileges of a role which does. In the former case,
+ * record the grantor as the current user; in the latter, pick one of
+ * the roles that is "most directly" inherited by the current role
+ * (i.e. fewest "hops").
+ *
+ * (We shouldn't fail to find a best grantor, because we've already
+ * established that the current user has permission to perform the
+ * operation.)
+ */
+ grantorId = select_best_admin(currentUserId, roleid);
+ if (!OidIsValid(grantorId))
+ elog(ERROR, "no possible grantors");
+ return grantorId;
+ }
+
+ /*
+ * If an explicit grantor is specified, it must be a role whose privileges
+ * the current user possesses.
+ *
+ * It should also be a role that has ADMIN OPTION on the target role, but
+ * we check this condition only in case of GRANT. For REVOKE, no matching
+ * grant should exist anyway, but if it somehow does, let the user get rid
+ * of it.
+ */
+ if (is_grant)
+ {
+ if (!has_privs_of_role(currentUserId, grantorId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to grant privileges as role \"%s\"",
+ GetUserNameFromId(grantorId, false))));
+
+ if (grantorId != BOOTSTRAP_SUPERUSERID &&
+ select_best_admin(grantorId, roleid) != grantorId)
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("grantor must have ADMIN OPTION on \"%s\"",
+ GetUserNameFromId(roleid, false))));
+ }
+ else
+ {
+ if (!has_privs_of_role(currentUserId, grantorId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to revoke privileges granted by role \"%s\"",
+ GetUserNameFromId(grantorId, false))));
+ }
+
+ /*
+ * If a grantor was specified explicitly, always attribute the grant to
+ * that role (unless we error out above).
+ */
+ return grantorId;
+}
+
+/*
+ * Initialize an array of RevokeRoleGrantAction objects.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * This constructs an array indicating that no actions are to be performed;
+ * that is, every element is initially RRG_NOOP.
+ */
+static RevokeRoleGrantAction *
+initialize_revoke_actions(CatCList *memlist)
+{
+ RevokeRoleGrantAction *result;
+ int i;
+
+ if (memlist->n_members == 0)
+ return NULL;
+
+ result = palloc(sizeof(RevokeRoleGrantAction) * memlist->n_members);
+ for (i = 0; i < memlist->n_members; i++)
+ result[i] = RRG_NOOP;
+ return result;
+}
+
+/*
+ * Figure out what we would need to do in order to revoke a grant, or just the
+ * admin option on a grant, given that there might be dependent privileges.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * Whatever actions prove to be necessary will be signalled by updating
+ * 'actions'.
+ *
+ * If behavior is DROP_RESTRICT, an error will occur if there are dependent
+ * role membership grants; if DROP_CASCADE, those grants will be scheduled
+ * for deletion.
+ *
+ * The return value is true if the matching grant was found in the list,
+ * and false if not.
+ */
+static bool
+plan_single_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ Oid member, Oid grantor, bool revoke_admin_option_only,
+ DropBehavior behavior)
+{
+ int i;
+
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (authmem_form->member == member &&
+ authmem_form->grantor == grantor)
+ {
+ plan_recursive_revoke(memlist, actions, i,
+ revoke_admin_option_only, behavior);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Figure out what we would need to do in order to revoke all grants to
+ * a given member, given that there might be dependent privileges.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * Whatever actions prove to be necessary will be signalled by updating
+ * 'actions'.
+ */
+static void
+plan_member_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ Oid member)
+{
+ int i;
+
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (authmem_form->member == member)
+ plan_recursive_revoke(memlist, actions, i, false, DROP_CASCADE);
+ }
+}
+
+/*
+ * Workhorse for figuring out recursive revocation of role grants.
+ *
+ * This is similar to what recursive_revoke() does for ACLs.
+ */
+static void
+plan_recursive_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ int index,
+ bool revoke_admin_option_only, DropBehavior behavior)
+{
+ bool would_still_have_admin_option = false;
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+ int i;
+
+ /* If it's already been done, we can just return. */
+ if (actions[index] == RRG_DELETE_GRANT)
+ return;
+ if (actions[index] == RRG_REMOVE_ADMIN_OPTION &&
+ revoke_admin_option_only)
+ return;
+
+ /* Locate tuple data. */
+ authmem_tuple = &memlist->members[index]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ /*
+ * If the existing tuple does not have admin_option set, then we do not
+ * need to recurse. If we're just supposed to clear that bit we don't need
+ * to do anything at all; if we're supposed to remove the grant, we need
+ * to do something, but only to the tuple, and not any others.
+ */
+ if (!revoke_admin_option_only)
+ {
+ actions[index] = RRG_DELETE_GRANT;
+ if (!authmem_form->admin_option)
+ return;
+ }
+ else
+ {
+ if (!authmem_form->admin_option)
+ return;
+ actions[index] = RRG_REMOVE_ADMIN_OPTION;
+ }
+
+ /* Determine whether the member would still have ADMIN OPTION. */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple am_cascade_tuple;
+ Form_pg_auth_members am_cascade_form;
+
+ am_cascade_tuple = &memlist->members[i]->tuple;
+ am_cascade_form = (Form_pg_auth_members) GETSTRUCT(am_cascade_tuple);
+
+ if (am_cascade_form->member == authmem_form->member &&
+ am_cascade_form->admin_option && actions[i] == RRG_NOOP)
+ {
+ would_still_have_admin_option = true;
+ break;
+ }
+ }
+
+ /* If the member would still have ADMIN OPTION, we need not recurse. */
+ if (would_still_have_admin_option)
+ return;
+
+ /*
+ * Recurse to grants that are not yet slated for deletion which have this
+ * member as the grantor.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple am_cascade_tuple;
+ Form_pg_auth_members am_cascade_form;
+
+ am_cascade_tuple = &memlist->members[i]->tuple;
+ am_cascade_form = (Form_pg_auth_members) GETSTRUCT(am_cascade_tuple);
+
+ if (am_cascade_form->grantor == authmem_form->member &&
+ actions[i] != RRG_DELETE_GRANT)
+ {
+ if (behavior == DROP_RESTRICT)
+ ereport(ERROR,
+ (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
+ errmsg("dependent privileges exist"),
+ errhint("Use CASCADE to revoke them too.")));
+
+ plan_recursive_revoke(memlist, actions, i, false, behavior);
+ }
+ }
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f9037761f9..c8bd66dd54 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -7870,6 +7870,7 @@ RevokeRoleStmt:
n->admin_opt = false;
n->granted_roles = $2;
n->grantee_roles = $4;
+ n->grantor = $5;
n->behavior = $6;
$$ = (Node *) n;
}
@@ -7881,6 +7882,7 @@ RevokeRoleStmt:
n->admin_opt = true;
n->granted_roles = $5;
n->grantee_roles = $7;
+ n->grantor = $8;
n->behavior = $9;
$$ = (Node *) n;
}
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 6fa58dd8eb..3e045da31f 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4791,9 +4791,7 @@ has_rolinherit(Oid roleid)
* Get a list of roles that the specified roleid is a member of
*
* Type ROLERECURSE_PRIVS recurses only through roles that have rolinherit
- * set, while ROLERECURSE_MEMBERS recurses through all roles. This sets
- * *is_admin==true if and only if role "roleid" has an ADMIN OPTION membership
- * in role "admin_of".
+ * set, while ROLERECURSE_MEMBERS recurses through all roles.
*
* Since indirect membership testing is relatively expensive, we cache
* a list of memberships. Hence, the result is only guaranteed good until
@@ -4801,10 +4799,15 @@ has_rolinherit(Oid roleid)
*
* For the benefit of select_best_grantor, the result is defined to be
* in breadth-first order, ie, closer relationships earlier.
+ *
+ * If admin_of is not InvalidOid, this function sets *admin_role, either
+ * to the OID of the first role in the result list that directly possesses
+ * ADMIN OPTION on the role corresponding to admin_of, or to InvalidOid if
+ * there is no such role.
*/
static List *
roles_is_member_of(Oid roleid, enum RoleRecurseType type,
- Oid admin_of, bool *is_admin)
+ Oid admin_of, Oid *admin_role)
{
Oid dba;
List *roles_list;
@@ -4812,7 +4815,9 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
List *new_cached_roles;
MemoryContext oldctx;
- Assert(OidIsValid(admin_of) == PointerIsValid(is_admin));
+ Assert(OidIsValid(admin_of) == PointerIsValid(admin_role));
+ if (admin_role != NULL)
+ *admin_role = InvalidOid;
/* If cache is valid and ADMIN OPTION not sought, just return the list */
if (cached_role[type] == roleid && !OidIsValid(admin_of) &&
@@ -4873,8 +4878,8 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
*/
if (otherid == admin_of &&
((Form_pg_auth_members) GETSTRUCT(tup))->admin_option &&
- OidIsValid(admin_of))
- *is_admin = true;
+ OidIsValid(admin_of) && !OidIsValid(*admin_role))
+ *admin_role = memberid;
/*
* Even though there shouldn't be any loops in the membership
@@ -5014,7 +5019,7 @@ is_member_of_role_nosuper(Oid member, Oid role)
bool
is_admin_of_role(Oid member, Oid role)
{
- bool result = false;
+ Oid admin_role;
if (superuser_arg(member))
return true;
@@ -5023,8 +5028,30 @@ is_admin_of_role(Oid member, Oid role)
if (member == role)
return false;
- (void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &result);
- return result;
+ (void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &admin_role);
+ return OidIsValid(admin_role);
+}
+
+/*
+ * Find a role whose privileges "member" inherits which has ADMIN OPTION
+ * on "role", ignoring super-userness.
+ *
+ * There might be more than one such role; prefer one which involves fewer
+ * hops. That is, if member has ADMIN OPTION, prefer that over all other
+ * options; if not, prefer a role from which member inherits more directly
+ * over more indirect inheritance.
+ */
+Oid
+select_best_admin(Oid member, Oid role)
+{
+ Oid admin_role;
+
+ /* By policy, a role cannot have WITH ADMIN OPTION on itself. */
+ if (member == role)
+ return InvalidOid;
+
+ (void) roles_is_member_of(member, ROLERECURSE_PRIVS, role, &admin_role);
+ return admin_role;
}
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 1912b12146..eec644ec84 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -213,22 +213,22 @@ static const struct cachedesc cacheinfo[] = {
},
{AuthMemRelationId, /* AUTHMEMMEMROLE */
AuthMemMemRoleIndexId,
- 2,
+ 3,
{
Anum_pg_auth_members_member,
Anum_pg_auth_members_roleid,
- 0,
+ Anum_pg_auth_members_grantor,
0
},
8
},
{AuthMemRelationId, /* AUTHMEMROLEMEM */
AuthMemRoleMemIndexId,
- 2,
+ 3,
{
Anum_pg_auth_members_roleid,
Anum_pg_auth_members_member,
- 0,
+ Anum_pg_auth_members_grantor,
0
},
8
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 26d3d53809..e8a2bfa6bd 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -21,6 +21,7 @@
#include "catalog/pg_authid_d.h"
#include "common/connect.h"
#include "common/file_utils.h"
+#include "common/hashfn.h"
#include "common/logging.h"
#include "common/string.h"
#include "dumputils.h"
@@ -31,6 +32,28 @@
/* version string we expect back from pg_dump */
#define PGDUMP_VERSIONSTR "pg_dump (PostgreSQL) " PG_VERSION "\n"
+static uint32 hash_string_pointer(char *s);
+
+typedef struct
+{
+ uint32 status;
+ uint32 hashval;
+ char *rolename;
+} RoleNameEntry;
+
+#define SH_PREFIX rolename
+#define SH_ELEMENT_TYPE RoleNameEntry
+#define SH_KEY_TYPE char *
+#define SH_KEY rolename
+#define SH_HASH_KEY(tb, key) hash_string_pointer(key)
+#define SH_EQUAL(tb, a, b) (strcmp(a, b) == 0)
+#define SH_STORE_HASH
+#define SH_GET_HASH(tb, a) (a)->hashval
+#define SH_SCOPE static inline
+#define SH_RAW_ALLOCATOR pg_malloc0
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
static void help(void);
@@ -925,45 +948,150 @@ dumpRoleMembership(PGconn *conn)
{
PQExpBuffer buf = createPQExpBuffer();
PGresult *res;
- int i;
+ int start = 0,
+ end,
+ total;
+ bool dump_grantors;
- printfPQExpBuffer(buf, "SELECT ur.rolname AS roleid, "
+ /*
+ * Previous versions of PostgreSQL didn't used to track the grantor very
+ * carefully in the backend, and the grantor could be any user even if
+ * they didn't have ADMIN OPTION on the role, or a user that no longer
+ * existed. To avoid dump and restore failures, don't dump the grantor
+ * when talking to an old server version.
+ */
+ dump_grantors = (PQserverVersion(conn) >= 160000);
+
+ /* Generate and execute query. */
+ printfPQExpBuffer(buf, "SELECT ur.rolname AS role, "
"um.rolname AS member, "
- "a.admin_option, "
- "ug.rolname AS grantor "
+ "ug.oid AS grantorid, "
+ "ug.rolname AS grantor, "
+ "a.admin_option "
"FROM pg_auth_members a "
"LEFT JOIN %s ur on ur.oid = a.roleid "
"LEFT JOIN %s um on um.oid = a.member "
"LEFT JOIN %s ug on ug.oid = a.grantor "
"WHERE NOT (ur.rolname ~ '^pg_' AND um.rolname ~ '^pg_')"
- "ORDER BY 1,2,3", role_catalog, role_catalog, role_catalog);
+ "ORDER BY 1,2,4", role_catalog, role_catalog, role_catalog);
res = executeQuery(conn, buf->data);
if (PQntuples(res) > 0)
fprintf(OPF, "--\n-- Role memberships\n--\n\n");
- for (i = 0; i < PQntuples(res); i++)
+ /*
+ * We can't dump these GRANT commands in arbitary order, because a role
+ * that is named as a grantor must already have ADMIN OPTION on the
+ * role for which it is granting permissions, except for the boostrap
+ * superuser, who can always be named as the grantor.
+ *
+ * We handle this by considering these grants role by role. For each role,
+ * we initially consider the only allowable grantor to be the boostrap
+ * superuser. Every time we grant ADMIN OPTION on the role to some user,
+ * that user also becomes an allowable grantor. We make repeated passes
+ * over the grants for the role, each time dumping those whose grantors
+ * are allowable and which we haven't done yet. Eventually this should
+ * let us dump all the grants.
+ */
+ total = PQntuples(res);
+ while (start < total)
{
- char *roleid = PQgetvalue(res, i, 0);
- char *member = PQgetvalue(res, i, 1);
- char *option = PQgetvalue(res, i, 2);
+ char *role = PQgetvalue(res, start, 0);
+ int i;
+ bool *done;
+ int remaining;
+ int prev_remaining = 0;
+ rolename_hash *ht;
+
+ /* All memberships for a single role should be adjacent. */
+ for (end = start; end < total; ++end)
+ {
+ char *otherrole;
+
+ otherrole = PQgetvalue(res, end, 0);
+ if (strcmp(role, otherrole) != 0)
+ break;
+ }
- fprintf(OPF, "GRANT %s", fmtId(roleid));
- fprintf(OPF, " TO %s", fmtId(member));
- if (*option == 't')
- fprintf(OPF, " WITH ADMIN OPTION");
+ role = PQgetvalue(res, start, 0);
+ remaining = end - start;
+ done = pg_malloc0(remaining * sizeof(bool));
+ ht = rolename_create(remaining, NULL);
/*
- * We don't track the grantor very carefully in the backend, so cope
- * with the possibility that it has been dropped.
+ * Make repeated passses over the grants for this role until all have
+ * been dumped.
*/
- if (!PQgetisnull(res, i, 3))
+ while (remaining > 0)
{
- char *grantor = PQgetvalue(res, i, 3);
+ /*
+ * We should make progress on every iteration, because a notional
+ * graph whose vertices are grants and whose edges point from
+ * grantors to members should be connected and acyclic. If we fail
+ * to make progress, either we or the server have messed up.
+ */
+ if (remaining == prev_remaining)
+ {
+ pg_log_error("could not find a legal dump ordering for memberships in role \"%s\"",
+ role);
+ PQfinish(conn);
+ exit_nicely(1);
+ }
- fprintf(OPF, " GRANTED BY %s", fmtId(grantor));
+ /* Make one pass over the grants for this role. */
+ for (i = start; i < end; ++i)
+ {
+ char *member;
+ char *admin_option;
+ char *grantorid;
+ char *grantor;
+ bool found;
+
+ /* If we already did this grant, don't do it again. */
+ if (done[i - start])
+ continue;
+
+ member = PQgetvalue(res, i, 1);
+ grantorid = PQgetvalue(res, i, 2);
+ grantor = PQgetvalue(res, i, 3);
+ admin_option = PQgetvalue(res, i, 4);
+
+ /*
+ * If we're not dumping grantors or if the grantor is the
+ * bootstrap superuser, it's fine to dump this now. Otherwise,
+ * it's got to be someone who has already been granted ADMIN
+ * OPTION.
+ */
+ if (dump_grantors &&
+ atooid(grantorid) != BOOTSTRAP_SUPERUSERID &&
+ rolename_lookup(ht, grantor) == NULL)
+ continue;
+
+ /* Remember that we did this so that we don't do it again. */
+ done[i - start] = true;
+ --remaining;
+
+ /*
+ * If ADMIN OPTION is being granted, remember that grants
+ * listing this member as the grantor can now be dumped.
+ */
+ if (*admin_option == 't')
+ rolename_insert(ht, member, &found);
+
+ /* Generate the actual GRANT statement. */
+ fprintf(OPF, "GRANT %s", fmtId(role));
+ fprintf(OPF, " TO %s", fmtId(member));
+ if (*admin_option == 't')
+ fprintf(OPF, " WITH ADMIN OPTION");
+ if (dump_grantors)
+ fprintf(OPF, " GRANTED BY %s", fmtId(grantor));
+ fprintf(OPF, ";\n");
+ }
}
- fprintf(OPF, ";\n");
+
+ rolename_destroy(ht);
+ pg_free(done);
+ start = end;
}
PQclear(res);
@@ -1748,3 +1876,14 @@ dumpTimestamp(const char *msg)
if (strftime(buf, sizeof(buf), PGDUMP_STRFTIME_FMT, localtime(&now)) != 0)
fprintf(OPF, "-- %s %s\n\n", msg, buf);
}
+
+/*
+ * Helper function for rolenamehash hash table.
+ */
+static uint32
+hash_string_pointer(char *s)
+{
+ unsigned char *ss = (unsigned char *) s;
+
+ return hash_bytes(ss, strlen(s));
+}
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index c9d7697730..e57ec4f810 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -44,8 +44,8 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
typedef FormData_pg_auth_members *Form_pg_auth_members;
DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_oid_index, 9385, AuthMemOidIndexId, on pg_auth_members using btree(oid oid_ops));
-DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
-DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops, grantor oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops, grantor oid_ops));
DECLARE_INDEX(pg_auth_members_grantor_index, 9384, AuthMemGrantorIndexId, on pg_auth_members using btree(grantor oid_ops));
#endif /* PG_AUTH_MEMBERS_H */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 48f7d72add..3d6411197c 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -212,6 +212,7 @@ extern bool has_privs_of_role(Oid member, Oid role);
extern bool is_member_of_role(Oid member, Oid role);
extern bool is_member_of_role_nosuper(Oid member, Oid role);
extern bool is_admin_of_role(Oid member, Oid role);
+extern Oid select_best_admin(Oid member, Oid role);
extern void check_is_member_of_role(Oid member, Oid role);
extern Oid get_role_oid(const char *rolename, bool missing_ok);
extern Oid get_role_oid_or_public(const char *rolename);
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index c2465d0f49..4e67d72760 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -103,21 +103,9 @@ ERROR: role "regress_nosuch_recursive" does not exist
DROP ROLE regress_nosuch_admin_recursive;
ERROR: role "regress_nosuch_admin_recursive" does not exist
DROP ROLE regress_plainrole;
--- fail, can't drop regress_createrole yet, due to outstanding grants
-DROP ROLE regress_createrole;
-ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
-DETAIL: privileges for membership of role regress_read_all_data in role pg_read_all_data
-privileges for membership of role regress_write_all_data in role pg_write_all_data
-privileges for membership of role regress_monitor in role pg_monitor
-privileges for membership of role regress_read_all_settings in role pg_read_all_settings
-privileges for membership of role regress_read_all_stats in role pg_read_all_stats
-privileges for membership of role regress_stat_scan_tables in role pg_stat_scan_tables
-privileges for membership of role regress_read_server_files in role pg_read_server_files
-privileges for membership of role regress_write_server_files in role pg_write_server_files
-privileges for membership of role regress_execute_server_program in role pg_execute_server_program
-privileges for membership of role regress_signal_backend in role pg_signal_backend
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
+DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -137,8 +125,6 @@ DROP ROLE regress_read_server_files;
DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
--- ok, dropped the other roles first so this is ok now
-DROP ROLE regress_createrole;
-- fail, role still owns database objects
DROP ROLE regress_tenant;
ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 65b4a22ebc..c71ee9a0da 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -33,6 +33,54 @@ CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- circular ADMIN OPTION grants should be disallowed
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION GRANTED BY regress_priv_user2;
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION GRANTED BY regress_priv_user3;
+ERROR: admin option cannot be granted back to your own grantor
+-- need CASCADE to revoke grant or admin option if dependent grants exist
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2; -- fail
+ERROR: dependent privileges exist
+HINT: Use CASCADE to revoke them too.
+REVOKE regress_priv_user1 FROM regress_priv_user2; -- fail
+ERROR: dependent privileges exist
+HINT: Use CASCADE to revoke them too.
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ member | admin_option
+--------------------+--------------
+ regress_priv_user2 | t
+ regress_priv_user3 | t
+(2 rows)
+
+BEGIN;
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ member | admin_option
+--------------------+--------------
+ regress_priv_user2 | f
+(1 row)
+
+ROLLBACK;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ member | admin_option
+--------+--------------
+(0 rows)
+
+-- inferred grantor must be a role with ADMIN OPTION
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user2 TO regress_priv_user3;
+SET ROLE regress_priv_user3;
+GRANT regress_priv_user1 TO regress_priv_user4;
+SELECT grantor::regrole FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole and member = 'regress_priv_user4'::regrole;
+ grantor
+--------------------
+ regress_priv_user2
+(1 row)
+
+RESET ROLE;
+REVOKE regress_priv_user2 FROM regress_priv_user3;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
@@ -68,15 +116,17 @@ CREATE USER regress_priv_user5;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
+GRANT regress_priv_user9 TO regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
GRANT pg_read_all_settings TO regress_priv_user9 WITH ADMIN OPTION;
SET SESSION AUTHORIZATION regress_priv_user9;
GRANT pg_read_all_settings TO regress_priv_user10;
SET SESSION AUTHORIZATION regress_priv_user8;
-REVOKE pg_read_all_settings FROM regress_priv_user10;
+REVOKE pg_read_all_settings FROM regress_priv_user10 GRANTED BY regress_priv_user9;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user9;
REVOKE pg_read_all_settings FROM regress_priv_user9;
RESET SESSION AUTHORIZATION;
+REVOKE regress_priv_user9 FROM regress_priv_user8;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
SET ROLE pg_read_all_settings;
@@ -90,7 +140,7 @@ CREATE GROUP regress_priv_group1;
CREATE GROUP regress_priv_group2 WITH USER regress_priv_user1, regress_priv_user2;
ALTER GROUP regress_priv_group1 ADD USER regress_priv_user4;
ALTER GROUP regress_priv_group2 ADD USER regress_priv_user2; -- duplicate
-NOTICE: role "regress_priv_user2" is already a member of role "regress_priv_group2"
+NOTICE: role "regress_priv_user2" has already been granted membership in role "regress_priv_group2" by role "rhaas"
ALTER GROUP regress_priv_group2 DROP USER regress_priv_user2;
GRANT regress_priv_group2 TO regress_priv_user4 WITH ADMIN OPTION;
-- prepare non-leakproof function for later
@@ -99,9 +149,13 @@ CREATE FUNCTION leak(integer,integer) RETURNS boolean
LANGUAGE internal IMMUTABLE STRICT; -- but deliberately not LEAKPROOF
ALTER FUNCTION leak(integer,integer) OWNER TO regress_priv_user1;
-- test owner privileges
+GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY regress_priv_role; -- error, doesn't have ADMIN OPTION
+ERROR: grantor must have ADMIN OPTION on "regress_priv_role"
GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY CURRENT_ROLE;
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY foo; -- error
-REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- error
+ERROR: role "foo" does not exist
+REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- warning, noop
+WARNING: role "regress_priv_user1" has not been granted membership in role "regress_priv_role" by role "regress_priv_user2"
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_USER;
REVOKE regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_ROLE;
DROP ROLE regress_priv_role;
@@ -1746,7 +1800,7 @@ SET SESSION AUTHORIZATION regress_priv_user1;
GRANT regress_priv_group2 TO regress_priv_user5; -- fails: no ADMIN OPTION
ERROR: must have admin option on role "regress_priv_group2"
SELECT dogrant_ok(); -- ok: SECURITY DEFINER conveys ADMIN
-NOTICE: role "regress_priv_user5" is already a member of role "regress_priv_group2"
+NOTICE: role "regress_priv_user5" has already been granted membership in role "regress_priv_group2" by role "regress_priv_user4"
dogrant_ok
------------
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index b696628238..292dc08797 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -98,11 +98,9 @@ DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
--- fail, can't drop regress_createrole yet, due to outstanding grants
-DROP ROLE regress_createrole;
-
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
+DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -123,9 +121,6 @@ DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
--- ok, dropped the other roles first so this is ok now
-DROP ROLE regress_createrole;
-
-- fail, role still owns database objects
DROP ROLE regress_tenant;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 66834e32a7..034ebbbf94 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -37,6 +37,32 @@ CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- circular ADMIN OPTION grants should be disallowed
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION GRANTED BY regress_priv_user2;
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION GRANTED BY regress_priv_user3;
+
+-- need CASCADE to revoke grant or admin option if dependent grants exist
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2; -- fail
+REVOKE regress_priv_user1 FROM regress_priv_user2; -- fail
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+BEGIN;
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ROLLBACK;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+
+-- inferred grantor must be a role with ADMIN OPTION
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user2 TO regress_priv_user3;
+SET ROLE regress_priv_user3;
+GRANT regress_priv_user1 TO regress_priv_user4;
+SELECT grantor::regrole FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole and member = 'regress_priv_user4'::regrole;
+RESET ROLE;
+REVOKE regress_priv_user2 FROM regress_priv_user3;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
+
-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
@@ -67,6 +93,7 @@ CREATE USER regress_priv_user5;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
+GRANT regress_priv_user9 TO regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
GRANT pg_read_all_settings TO regress_priv_user9 WITH ADMIN OPTION;
@@ -75,11 +102,12 @@ SET SESSION AUTHORIZATION regress_priv_user9;
GRANT pg_read_all_settings TO regress_priv_user10;
SET SESSION AUTHORIZATION regress_priv_user8;
-REVOKE pg_read_all_settings FROM regress_priv_user10;
+REVOKE pg_read_all_settings FROM regress_priv_user10 GRANTED BY regress_priv_user9;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user9;
REVOKE pg_read_all_settings FROM regress_priv_user9;
RESET SESSION AUTHORIZATION;
+REVOKE regress_priv_user9 FROM regress_priv_user8;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
@@ -110,9 +138,10 @@ ALTER FUNCTION leak(integer,integer) OWNER TO regress_priv_user1;
-- test owner privileges
+GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY regress_priv_role; -- error, doesn't have ADMIN OPTION
GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY CURRENT_ROLE;
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY foo; -- error
-REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- error
+REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- warning, noop
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_USER;
REVOKE regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_ROLE;
DROP ROLE regress_priv_role;
--
2.24.3 (Apple Git-128)
v4-0001-Ensure-that-pg_auth_members.grantor-is-always-val.patchapplication/octet-stream; name=v4-0001-Ensure-that-pg_auth_members.grantor-is-always-val.patchDownload
From 582a6540bc6c182d0d1a2526e56c99a8a3f5bcbc Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 1 Aug 2022 13:40:54 -0400
Subject: [PATCH v4 1/2] Ensure that pg_auth_members.grantor is always valid.
Previously, "GRANT foo TO bar" or "GRANT foo TO bar GRANTED BY baz"
would record the OID of the grantor in pg_auth_members.grantor, but
that role could later be dropped without modifying or removing the
pg_auth_members record. That's not great, because we typically try
to avoid dangling references in catalog data.
Now, a role grant depends on the grantor, and the grantor can't be
dropped without removing the grant or changing the grantor. "DROP
OWNED BY" will remove the grant, just as it does for other kinds of
privileges. "REASSIGN OWNED BY" will not, again just like what we do
in other cases involving privileges.
pg_auth_members now has an OID column, because that is needed in order
for dependencies to work. It also now has an index on the grantor
column, because otherwise dropping a role would require a sequential
scan of the entire table to see whether the role's OID is in use as
a grantor. That probably wouldn't be too large a problem in practice,
but it seems better to have an index just in case.
Patch by me, reviewed by Stephen Frost
---
doc/src/sgml/catalogs.sgml | 9 +
src/backend/catalog/catalog.c | 2 +
src/backend/catalog/dependency.c | 14 +-
src/backend/catalog/objectaddress.c | 108 ++++++++++++
src/backend/catalog/pg_shdepend.c | 47 ++++--
src/backend/catalog/system_views.sql | 2 +-
src/backend/commands/alter.c | 1 +
src/backend/commands/event_trigger.c | 1 +
src/backend/commands/tablecmds.c | 1 +
src/backend/commands/user.c | 192 +++++++++++++++++-----
src/include/catalog/dependency.h | 1 +
src/include/catalog/pg_auth_members.h | 5 +-
src/test/regress/expected/create_role.out | 16 +-
src/test/regress/expected/privileges.out | 32 ++++
src/test/regress/sql/create_role.sql | 7 +-
src/test/regress/sql/privileges.sql | 27 +++
16 files changed, 397 insertions(+), 68 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index cd2cc37aeb..7605af75d4 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1669,6 +1669,15 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</thead>
<tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>oid</structfield> <type>oid</type>
+ </para>
+ <para>
+ Row identifier
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>roleid</structfield> <type>oid</type>
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 6f43870779..2abd6b007a 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -262,6 +262,8 @@ IsSharedRelation(Oid relationId)
relationId == AuthIdRolnameIndexId ||
relationId == AuthMemMemRoleIndexId ||
relationId == AuthMemRoleMemIndexId ||
+ relationId == AuthMemOidIndexId ||
+ relationId == AuthMemGrantorIndexId ||
relationId == DatabaseNameIndexId ||
relationId == DatabaseOidIndexId ||
relationId == DbRoleSettingDatidRolidIndexId ||
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index e119674b1f..39768fa22b 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -28,6 +28,7 @@
#include "catalog/pg_amproc.h"
#include "catalog/pg_attrdef.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_cast.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
@@ -172,6 +173,7 @@ static const Oid object_classes[] = {
TSTemplateRelationId, /* OCLASS_TSTEMPLATE */
TSConfigRelationId, /* OCLASS_TSCONFIG */
AuthIdRelationId, /* OCLASS_ROLE */
+ AuthMemRelationId, /* OCLASS_ROLE_MEMBERSHIP */
DatabaseRelationId, /* OCLASS_DATABASE */
TableSpaceRelationId, /* OCLASS_TBLSPACE */
ForeignDataWrapperRelationId, /* OCLASS_FDW */
@@ -1502,6 +1504,7 @@ doDeletion(const ObjectAddress *object, int flags)
case OCLASS_DEFACL:
case OCLASS_EVENT_TRIGGER:
case OCLASS_TRANSFORM:
+ case OCLASS_ROLE_MEMBERSHIP:
DropObjectById(object);
break;
@@ -1529,9 +1532,8 @@ doDeletion(const ObjectAddress *object, int flags)
* Accepts the same flags as performDeletion (though currently only
* PERFORM_DELETION_CONCURRENTLY does anything).
*
- * We use LockRelation for relations, LockDatabaseObject for everything
- * else. Shared-across-databases objects are not currently supported
- * because no caller cares, but could be modified to use LockSharedObject.
+ * We use LockRelation for relations, and otherwise LockSharedObject or
+ * LockDatabaseObject as appropriate for the object type.
*/
void
AcquireDeletionLock(const ObjectAddress *object, int flags)
@@ -1549,6 +1551,9 @@ AcquireDeletionLock(const ObjectAddress *object, int flags)
else
LockRelationOid(object->objectId, AccessExclusiveLock);
}
+ else if (object->classId == AuthMemRelationId)
+ LockSharedObject(object->classId, object->objectId, 0,
+ AccessExclusiveLock);
else
{
/* assume we should lock the whole object not a sub-object */
@@ -2914,6 +2919,9 @@ getObjectClass(const ObjectAddress *object)
case AuthIdRelationId:
return OCLASS_ROLE;
+ case AuthMemRelationId:
+ return OCLASS_ROLE_MEMBERSHIP;
+
case DatabaseRelationId:
return OCLASS_DATABASE;
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 6080ff8f5f..cc80172113 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -27,6 +27,7 @@
#include "catalog/pg_amproc.h"
#include "catalog/pg_attrdef.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_cast.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
@@ -386,6 +387,20 @@ static const ObjectPropertyType ObjectProperty[] =
-1,
true
},
+ {
+ "role membership",
+ AuthMemRelationId,
+ AuthMemOidIndexId,
+ -1,
+ -1,
+ Anum_pg_auth_members_oid,
+ InvalidAttrNumber,
+ InvalidAttrNumber,
+ Anum_pg_auth_members_grantor,
+ InvalidAttrNumber,
+ -1,
+ true
+ },
{
"rule",
RewriteRelationId,
@@ -787,6 +802,10 @@ static const struct object_type_map
{
"role", OBJECT_ROLE
},
+ /* OCLASS_ROLE_MEMBERSHIP */
+ {
+ "role membership", -1 /* unmapped */
+ },
/* OCLASS_DATABASE */
{
"database", OBJECT_DATABASE
@@ -3644,6 +3663,48 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
break;
}
+ case OCLASS_ROLE_MEMBERSHIP:
+ {
+ Relation amDesc;
+ ScanKeyData skey[1];
+ SysScanDesc rcscan;
+ HeapTuple tup;
+ Form_pg_auth_members amForm;
+
+ amDesc = table_open(AuthMemRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_auth_members_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+
+ rcscan = systable_beginscan(amDesc, AuthMemOidIndexId, true,
+ NULL, 1, skey);
+
+ tup = systable_getnext(rcscan);
+
+ if (!HeapTupleIsValid(tup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for role membership %u",
+ object->objectId);
+
+ systable_endscan(rcscan);
+ table_close(amDesc, AccessShareLock);
+ break;
+ }
+
+ amForm = (Form_pg_auth_members) GETSTRUCT(tup);
+
+ appendStringInfo(&buffer, _("membership of role %s in role %s"),
+ GetUserNameFromId(amForm->member, false),
+ GetUserNameFromId(amForm->roleid, false));
+
+ systable_endscan(rcscan);
+ table_close(amDesc, AccessShareLock);
+ break;
+ }
+
case OCLASS_DATABASE:
{
char *datname;
@@ -4533,6 +4594,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
appendStringInfoString(&buffer, "role");
break;
+ case OCLASS_ROLE_MEMBERSHIP:
+ appendStringInfoString(&buffer, "role membership");
+ break;
+
case OCLASS_DATABASE:
appendStringInfoString(&buffer, "database");
break;
@@ -5476,6 +5541,49 @@ getObjectIdentityParts(const ObjectAddress *object,
break;
}
+ case OCLASS_ROLE_MEMBERSHIP:
+ {
+ Relation authMemDesc;
+ ScanKeyData skey[1];
+ SysScanDesc amscan;
+ HeapTuple tup;
+ Form_pg_auth_members amForm;
+
+ authMemDesc = table_open(AuthMemRelationId,
+ AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_auth_members_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+
+ amscan = systable_beginscan(authMemDesc, AuthMemOidIndexId, true,
+ NULL, 1, skey);
+
+ tup = systable_getnext(amscan);
+
+ if (!HeapTupleIsValid(tup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for pg_auth_members entry %u",
+ object->objectId);
+
+ systable_endscan(amscan);
+ table_close(authMemDesc, AccessShareLock);
+ break;
+ }
+
+ amForm = (Form_pg_auth_members) GETSTRUCT(tup);
+
+ appendStringInfo(&buffer, _("membership of role %s in role %s"),
+ GetUserNameFromId(amForm->member, false),
+ GetUserNameFromId(amForm->roleid, false));
+
+ systable_endscan(amscan);
+ table_close(authMemDesc, AccessShareLock);
+ break;
+ }
+
case OCLASS_DATABASE:
{
char *datname;
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index 3e8fa008b9..f2f227f887 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -22,6 +22,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_conversion.h"
#include "catalog/pg_database.h"
@@ -1364,11 +1365,6 @@ shdepDropOwned(List *roleids, DropBehavior behavior)
case SHARED_DEPENDENCY_INVALID:
elog(ERROR, "unexpected dependency type");
break;
- case SHARED_DEPENDENCY_ACL:
- RemoveRoleFromObjectACL(roleid,
- sdepForm->classid,
- sdepForm->objid);
- break;
case SHARED_DEPENDENCY_POLICY:
/*
@@ -1398,22 +1394,37 @@ shdepDropOwned(List *roleids, DropBehavior behavior)
add_exact_object_address(&obj, deleteobjs);
}
break;
+ case SHARED_DEPENDENCY_ACL:
+
+ /*
+ * Dependencies on role grants are recorded using
+ * SHARED_DEPENDENCY_ACL, but unlike a regular ACL list
+ * which stores all permissions for a particular object in
+ * a single ACL array, there's a separate catalog row for
+ * each grant - so removing the grant just means removing
+ * the entire row.
+ */
+ if (sdepForm->classid != AuthMemRelationId)
+ {
+ RemoveRoleFromObjectACL(roleid,
+ sdepForm->classid,
+ sdepForm->objid);
+ break;
+ }
+ /* FALLTHROUGH */
case SHARED_DEPENDENCY_OWNER:
- /* If a local object, save it for deletion below */
- if (sdepForm->dbid == MyDatabaseId)
+ /* Save it for deletion below */
+ obj.classId = sdepForm->classid;
+ obj.objectId = sdepForm->objid;
+ obj.objectSubId = sdepForm->objsubid;
+ /* as above */
+ AcquireDeletionLock(&obj, 0);
+ if (!systable_recheck_tuple(scan, tuple))
{
- obj.classId = sdepForm->classid;
- obj.objectId = sdepForm->objid;
- obj.objectSubId = sdepForm->objsubid;
- /* as above */
- AcquireDeletionLock(&obj, 0);
- if (!systable_recheck_tuple(scan, tuple))
- {
- ReleaseDeletionLock(&obj);
- break;
- }
- add_exact_object_address(&obj, deleteobjs);
+ ReleaseDeletionLock(&obj);
+ break;
}
+ add_exact_object_address(&obj, deleteobjs);
break;
}
}
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index f369b1fc14..5a844b63a1 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -53,7 +53,7 @@ CREATE VIEW pg_group AS
SELECT
rolname AS groname,
oid AS grosysid,
- ARRAY(SELECT member FROM pg_auth_members WHERE roleid = oid) AS grolist
+ ARRAY(SELECT member FROM pg_auth_members WHERE roleid = pg_authid.oid) AS grolist
FROM pg_authid
WHERE NOT rolcanlogin;
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 5456b8222b..55219bb097 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -650,6 +650,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
case OCLASS_TRIGGER:
case OCLASS_SCHEMA:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_FDW:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index eef3e5d56e..549e30da34 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -1015,6 +1015,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_PARAMETER_ACL:
/* no support for global objects */
return false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index d22dd44712..fc36e2d231 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12645,6 +12645,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
case OCLASS_TSTEMPLATE:
case OCLASS_TSCONFIG:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_FDW:
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 94135fdd6b..fc42b1cfd7 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -911,6 +911,7 @@ DropRole(DropRoleStmt *stmt)
Relation pg_authid_rel,
pg_auth_members_rel;
ListCell *item;
+ List *role_addresses = NIL;
if (!have_createrole_privilege())
ereport(ERROR,
@@ -919,7 +920,7 @@ DropRole(DropRoleStmt *stmt)
/*
* Scan the pg_authid relation to find the Oid of the role(s) to be
- * deleted.
+ * deleted and perform preliminary permissions and sanity checks.
*/
pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
pg_auth_members_rel = table_open(AuthMemRelationId, RowExclusiveLock);
@@ -932,10 +933,9 @@ DropRole(DropRoleStmt *stmt)
tmp_tuple;
Form_pg_authid roleform;
ScanKeyData scankey;
- char *detail;
- char *detail_log;
SysScanDesc sscan;
Oid roleid;
+ ObjectAddress *role_address;
if (rolspec->roletype != ROLESPEC_CSTRING)
ereport(ERROR,
@@ -991,34 +991,31 @@ DropRole(DropRoleStmt *stmt)
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
+ /* Don't leak the syscache tuple */
+ ReleaseSysCache(tuple);
+
/*
* Lock the role, so nobody can add dependencies to her while we drop
* her. We keep the lock until the end of transaction.
*/
LockSharedObject(AuthIdRelationId, roleid, 0, AccessExclusiveLock);
- /* Check for pg_shdepend entries depending on this role */
- if (checkSharedDependencies(AuthIdRelationId, roleid,
- &detail, &detail_log))
- ereport(ERROR,
- (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
- errmsg("role \"%s\" cannot be dropped because some objects depend on it",
- role),
- errdetail_internal("%s", detail),
- errdetail_log("%s", detail_log)));
-
- /*
- * Remove the role from the pg_authid table
- */
- CatalogTupleDelete(pg_authid_rel, &tuple->t_self);
-
- ReleaseSysCache(tuple);
-
/*
- * Remove role from the pg_auth_members table. We have to remove all
- * tuples that show it as either a role or a member.
+ * If there is a pg_auth_members entry that has one of the roles to be
+ * dropped as the roleid or member, it should be silently removed, but
+ * if there is a pg_auth_members entry that has one of the roles to be
+ * dropped as the grantor, the operation should fail.
+ *
+ * It's possible, however, that a single pg_auth_members entry could
+ * fall into multiple categories - e.g. the user could do "GRANT foo
+ * TO bar GRANTED BY baz" and then "DROP ROLE baz, bar". We want such
+ * an operation to succeed regardless of the order in which the
+ * to-be-dropped roles are passed to DROP ROLE.
*
- * XXX what about grantor entries? Maybe we should do one heap scan.
+ * To make that work, we remove all pg_auth_members entries that can
+ * be silently removed in this loop, and then below we'll make a
+ * second pass over the list of roles to be removed and check for any
+ * remaining dependencies.
*/
ScanKeyInit(&scankey,
Anum_pg_auth_members_roleid,
@@ -1030,6 +1027,11 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
+ Form_pg_auth_members authmem_form;
+
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
}
@@ -1045,22 +1047,16 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
+ Form_pg_auth_members authmem_form;
+
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
}
systable_endscan(sscan);
- /*
- * Remove any comments or security labels on this role.
- */
- DeleteSharedComments(roleid, AuthIdRelationId);
- DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
-
- /*
- * Remove settings for this role.
- */
- DropSetting(InvalidOid, roleid);
-
/*
* Advance command counter so that later iterations of this loop will
* see the changes already made. This is essential if, for example,
@@ -1071,6 +1067,72 @@ DropRole(DropRoleStmt *stmt)
* itself.)
*/
CommandCounterIncrement();
+
+ /* Looks tentatively OK, add it to the list. */
+ role_address = palloc(sizeof(ObjectAddress));
+ role_address->classId = AuthIdRelationId;
+ role_address->objectId = roleid;
+ role_address->objectSubId = 0;
+ role_addresses = lappend(role_addresses, role_address);
+ }
+
+ /*
+ * Second pass over the roles to be removed.
+ */
+ foreach(item, role_addresses)
+ {
+ ObjectAddress *role_address = lfirst(item);
+ Oid roleid = role_address->objectId;
+ HeapTuple tuple;
+ Form_pg_authid roleform;
+ char *detail;
+ char *detail_log;
+
+ /*
+ * Re-find the pg_authid tuple.
+ *
+ * Since we've taken a lock on the role OID, it shouldn't be possible
+ * for the tuple to have been deleted -- or for that matter updated --
+ * unless the user is manually modifying the system catalogs.
+ */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for role %u", roleid);
+ roleform = (Form_pg_authid) GETSTRUCT(tuple);
+
+ /*
+ * Check for pg_shdepend entries depending on this role.
+ *
+ * This needs to happen after we've completed removing any
+ * pg_auth_members entries that can be removed silently, in order to
+ * avoid spurious failures. See notes above for more details.
+ */
+ if (checkSharedDependencies(AuthIdRelationId, roleid,
+ &detail, &detail_log))
+ ereport(ERROR,
+ (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
+ errmsg("role \"%s\" cannot be dropped because some objects depend on it",
+ NameStr(roleform->rolname)),
+ errdetail_internal("%s", detail),
+ errdetail_log("%s", detail_log)));
+
+ /*
+ * Remove the role from the pg_authid table
+ */
+ CatalogTupleDelete(pg_authid_rel, &tuple->t_self);
+
+ ReleaseSysCache(tuple);
+
+ /*
+ * Remove any comments or security labels on this role.
+ */
+ DeleteSharedComments(roleid, AuthIdRelationId);
+ DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
+
+ /*
+ * Remove settings for this role.
+ */
+ DropSetting(InvalidOid, roleid);
}
/*
@@ -1443,6 +1505,7 @@ AddRoleMems(const char *rolename, Oid roleid,
Datum new_record[Natts_pg_auth_members] = {0};
bool new_record_nulls[Natts_pg_auth_members] = {0};
bool new_record_repl[Natts_pg_auth_members] = {0};
+ Form_pg_auth_members authmem_form;
/*
* pg_database_owner is never a role member. Lifting this restriction
@@ -1488,15 +1551,22 @@ AddRoleMems(const char *rolename, Oid roleid,
authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
ObjectIdGetDatum(roleid),
ObjectIdGetDatum(memberid));
- if (HeapTupleIsValid(authmem_tuple) &&
- (!admin_opt ||
- ((Form_pg_auth_members) GETSTRUCT(authmem_tuple))->admin_option))
+ if (!HeapTupleIsValid(authmem_tuple))
{
- ereport(NOTICE,
- (errmsg("role \"%s\" is already a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
- ReleaseSysCache(authmem_tuple);
- continue;
+ authmem_form = NULL;
+ }
+ else
+ {
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (!admin_opt || authmem_form->admin_option)
+ {
+ ereport(NOTICE,
+ (errmsg("role \"%s\" is already a member of role \"%s\"",
+ get_rolespec_name(memberRole), rolename)));
+ ReleaseSysCache(authmem_tuple);
+ continue;
+ }
}
/* Build a tuple to insert or update */
@@ -1513,13 +1583,41 @@ AddRoleMems(const char *rolename, Oid roleid,
new_record,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
+
+ if (authmem_form->grantor != grantorId)
+ {
+ Oid *oldmembers = palloc(sizeof(Oid));
+ Oid *newmembers = palloc(sizeof(Oid));
+
+ /* updateAclDependencies wants to pfree array inputs */
+ oldmembers[0] = authmem_form->grantor;
+ newmembers[0] = grantorId;
+
+ updateAclDependencies(AuthMemRelationId, authmem_form->oid,
+ 0, InvalidOid,
+ 1, oldmembers,
+ 1, newmembers);
+ }
+
ReleaseSysCache(authmem_tuple);
}
else
{
+ Oid objectId;
+ Oid *newmembers = palloc(sizeof(Oid));
+
+ objectId = GetNewObjectId();
+ new_record[Anum_pg_auth_members_oid - 1] = objectId;
tuple = heap_form_tuple(pg_authmem_dsc,
new_record, new_record_nulls);
CatalogTupleInsert(pg_authmem_rel, tuple);
+
+ /* updateAclDependencies wants to pfree array inputs */
+ newmembers[0] = grantorId;
+ updateAclDependencies(AuthMemRelationId, objectId,
+ 0, InvalidOid,
+ 0, NULL,
+ 1, newmembers);
}
/* CCI after each change, in case there are duplicates in list */
@@ -1586,6 +1684,7 @@ DelRoleMems(const char *rolename, Oid roleid,
RoleSpec *memberRole = lfirst(specitem);
Oid memberid = lfirst_oid(iditem);
HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
/*
* Find entry for this role/member
@@ -1601,9 +1700,16 @@ DelRoleMems(const char *rolename, Oid roleid,
continue;
}
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
if (!admin_opt)
{
- /* Remove the entry altogether */
+ /*
+ * Remove the entry altogether, after first removing its
+ * dependencies
+ */
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_authmem_rel, &authmem_tuple->t_self);
}
else
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index d027075a4c..0a1e072bef 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -112,6 +112,7 @@ typedef enum ObjectClass
OCLASS_TSTEMPLATE, /* pg_ts_template */
OCLASS_TSCONFIG, /* pg_ts_config */
OCLASS_ROLE, /* pg_authid */
+ OCLASS_ROLE_MEMBERSHIP, /* pg_auth_members */
OCLASS_DATABASE, /* pg_database */
OCLASS_TBLSPACE, /* pg_tablespace */
OCLASS_FDW, /* pg_foreign_data_wrapper */
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index 1bc027f133..c9d7697730 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -29,6 +29,7 @@
*/
CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(2843,AuthMemRelation_Rowtype_Id) BKI_SCHEMA_MACRO
{
+ Oid oid; /* oid */
Oid roleid BKI_LOOKUP(pg_authid); /* ID of a role */
Oid member BKI_LOOKUP(pg_authid); /* ID of a member of that role */
Oid grantor BKI_LOOKUP(pg_authid); /* who granted the membership */
@@ -42,7 +43,9 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
*/
typedef FormData_pg_auth_members *Form_pg_auth_members;
-DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_oid_index, 9385, AuthMemOidIndexId, on pg_auth_members using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops));
+DECLARE_INDEX(pg_auth_members_grantor_index, 9384, AuthMemGrantorIndexId, on pg_auth_members using btree(grantor oid_ops));
#endif /* PG_AUTH_MEMBERS_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..c2465d0f49 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -103,9 +103,21 @@ ERROR: role "regress_nosuch_recursive" does not exist
DROP ROLE regress_nosuch_admin_recursive;
ERROR: role "regress_nosuch_admin_recursive" does not exist
DROP ROLE regress_plainrole;
+-- fail, can't drop regress_createrole yet, due to outstanding grants
+DROP ROLE regress_createrole;
+ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_read_all_data in role pg_read_all_data
+privileges for membership of role regress_write_all_data in role pg_write_all_data
+privileges for membership of role regress_monitor in role pg_monitor
+privileges for membership of role regress_read_all_settings in role pg_read_all_settings
+privileges for membership of role regress_read_all_stats in role pg_read_all_stats
+privileges for membership of role regress_stat_scan_tables in role pg_stat_scan_tables
+privileges for membership of role regress_read_server_files in role pg_read_server_files
+privileges for membership of role regress_write_server_files in role pg_write_server_files
+privileges for membership of role regress_execute_server_program in role pg_execute_server_program
+privileges for membership of role regress_signal_backend in role pg_signal_backend
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
-DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -125,6 +137,8 @@ DROP ROLE regress_read_server_files;
DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
+-- ok, dropped the other roles first so this is ok now
+DROP ROLE regress_createrole;
-- fail, role still owns database objects
DROP ROLE regress_tenant;
ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index e10dd6f9ae..65b4a22ebc 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -33,6 +33,38 @@ CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- fail, dependency
+ERROR: role "regress_priv_user2" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user3 in role regress_priv_user1
+REASSIGN OWNED BY regress_priv_user2 TO regress_priv_user4;
+DROP ROLE regress_priv_user2; -- still fail, REASSIGN OWNED doesn't help
+ERROR: role "regress_priv_user2" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user3 in role regress_priv_user1
+DROP OWNED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- ok now, DROP OWNED does the job
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user4 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- should fail, dependency
+ERROR: role "regress_priv_user3" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user4 in role regress_priv_user1
+DROP ROLE regress_priv_user4; -- ok
+DROP ROLE regress_priv_user3; -- ok now
+GRANT regress_priv_user1 TO regress_priv_user5 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user6 GRANTED BY regress_priv_user5;
+DROP ROLE regress_priv_user5; -- should fail, dependency
+ERROR: role "regress_priv_user5" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user6 in role regress_priv_user1
+DROP ROLE regress_priv_user1, regress_priv_user5; -- ok, despite order
+-- recreate the roles we just dropped
+CREATE USER regress_priv_user1;
+CREATE USER regress_priv_user2;
+CREATE USER regress_priv_user3;
+CREATE USER regress_priv_user4;
+CREATE USER regress_priv_user5;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..b696628238 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -98,9 +98,11 @@ DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
+-- fail, can't drop regress_createrole yet, due to outstanding grants
+DROP ROLE regress_createrole;
+
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
-DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -121,6 +123,9 @@ DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
+-- ok, dropped the other roles first so this is ok now
+DROP ROLE regress_createrole;
+
-- fail, role still owns database objects
DROP ROLE regress_tenant;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 6d1fd3391a..66834e32a7 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -37,6 +37,33 @@ CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- fail, dependency
+REASSIGN OWNED BY regress_priv_user2 TO regress_priv_user4;
+DROP ROLE regress_priv_user2; -- still fail, REASSIGN OWNED doesn't help
+DROP OWNED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- ok now, DROP OWNED does the job
+
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user4 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- should fail, dependency
+DROP ROLE regress_priv_user4; -- ok
+DROP ROLE regress_priv_user3; -- ok now
+GRANT regress_priv_user1 TO regress_priv_user5 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user6 GRANTED BY regress_priv_user5;
+DROP ROLE regress_priv_user5; -- should fail, dependency
+DROP ROLE regress_priv_user1, regress_priv_user5; -- ok, despite order
+
+-- recreate the roles we just dropped
+CREATE USER regress_priv_user1;
+CREATE USER regress_priv_user2;
+CREATE USER regress_priv_user3;
+CREATE USER regress_priv_user4;
+CREATE USER regress_priv_user5;
+
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
--
2.24.3 (Apple Git-128)
On Mon, Aug 1, 2022 at 10:38 AM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I can't see this on cfbot - either I don't know how to use it
properly, which is quite possible, or the results aren't showing up
because of the close of the July CommitFest.I think the latter --- the cfbot thinks the July CF is no longer relevant,
but Jacob hasn't yet moved your patches forward. You could wait for
him to do that, or do it yourself.(Probably our nonexistent SOP manual for CFMs ought to say "don't
close the old CF till you've moved everything forward".)
Sorry about that. I've made a note to add this to the manual later.
--Jacob
On Mon, Aug 1, 2022 at 3:51 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Mon, Aug 1, 2022 at 1:38 PM Tom Lane <tgl@sss.pgh.pa.us> wrote:
I think the latter --- the cfbot thinks the July CF is no longer relevant,
but Jacob hasn't yet moved your patches forward. You could wait for
him to do that, or do it yourself.Done. New patches attached.
Well, CI isn't happy with this, and for good reason:
ALTER GROUP regress_priv_group2 ADD USER regress_priv_user2; -- duplicate
-NOTICE: role "regress_priv_user2" has already been granted
membership in role "regress_priv_group2" by role "rhaas"
+NOTICE: role "regress_priv_user2" has already been granted
membership in role "regress_priv_group2" by role "postgres"
The problem here is that I revised the error message to include the
name of the grantor, since that's now a part of the identity of the
grant. It would be misleading to say, as we did previously...
NOTICE: role "regress_priv_user2" is already a member of role
"regress_priv_group2"
...because them being in the group isn't relevant so much as them
being in the group by means of the same grantor. However, I suspect
that I can't persuade all of you that we should hard-code the name of
the bootstrap superuser as "rhaas", so this test case needs some
alteration. I found, however, that the original intent of the test
case couldn't be preserved with the patch as written, because when you
grant membership in one role to another role as the superuser or a
CREATEROLE user, the grant is attributed to the bootstrap superuser,
whose name is variable, as this test failure shows. Therefore, to fix
the test, I needed to use ALTER GROUP as a non-CREATEROLE user, some
user created as part of the test, for the results to be stable. But
that was impossible, because even though "GRANT user_name TO
group_name" requires *either* CREATEROLE *or* ADMIN OPTION on the
group, the equivalent command "ALTER GROUP group_name ADD USER
user_name" requires specifically CREATEROLE.
I debated whether to fix that inconsistency or just remove this test
case and eventually came down on the side of fixing the inconsistency,
so the attached version does it that way.
--
Robert Haas
EDB: http://www.enterprisedb.com
Attachments:
v5-0001-Ensure-that-pg_auth_members.grantor-is-always-val.patchapplication/octet-stream; name=v5-0001-Ensure-that-pg_auth_members.grantor-is-always-val.patchDownload
From e84797ba2cdbe6221593c2cda59472214f24ada8 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Mon, 1 Aug 2022 13:40:54 -0400
Subject: [PATCH v5 1/2] Ensure that pg_auth_members.grantor is always valid.
Previously, "GRANT foo TO bar" or "GRANT foo TO bar GRANTED BY baz"
would record the OID of the grantor in pg_auth_members.grantor, but
that role could later be dropped without modifying or removing the
pg_auth_members record. That's not great, because we typically try
to avoid dangling references in catalog data.
Now, a role grant depends on the grantor, and the grantor can't be
dropped without removing the grant or changing the grantor. "DROP
OWNED BY" will remove the grant, just as it does for other kinds of
privileges. "REASSIGN OWNED BY" will not, again just like what we do
in other cases involving privileges.
pg_auth_members now has an OID column, because that is needed in order
for dependencies to work. It also now has an index on the grantor
column, because otherwise dropping a role would require a sequential
scan of the entire table to see whether the role's OID is in use as
a grantor. That probably wouldn't be too large a problem in practice,
but it seems better to have an index just in case.
Patch by me, reviewed by Stephen Frost
---
doc/src/sgml/catalogs.sgml | 9 +
src/backend/catalog/catalog.c | 2 +
src/backend/catalog/dependency.c | 14 +-
src/backend/catalog/objectaddress.c | 108 ++++++++++++
src/backend/catalog/pg_shdepend.c | 47 ++++--
src/backend/catalog/system_views.sql | 2 +-
src/backend/commands/alter.c | 1 +
src/backend/commands/event_trigger.c | 1 +
src/backend/commands/tablecmds.c | 1 +
src/backend/commands/user.c | 192 +++++++++++++++++-----
src/include/catalog/dependency.h | 1 +
src/include/catalog/pg_auth_members.h | 5 +-
src/test/regress/expected/create_role.out | 16 +-
src/test/regress/expected/privileges.out | 32 ++++
src/test/regress/sql/create_role.sql | 7 +-
src/test/regress/sql/privileges.sql | 27 +++
16 files changed, 397 insertions(+), 68 deletions(-)
diff --git a/doc/src/sgml/catalogs.sgml b/doc/src/sgml/catalogs.sgml
index cd2cc37aeb..7605af75d4 100644
--- a/doc/src/sgml/catalogs.sgml
+++ b/doc/src/sgml/catalogs.sgml
@@ -1669,6 +1669,15 @@ SCRAM-SHA-256$<replaceable><iteration count></replaceable>:<replaceable>&l
</thead>
<tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>oid</structfield> <type>oid</type>
+ </para>
+ <para>
+ Row identifier
+ </para></entry>
+ </row>
+
<row>
<entry role="catalog_table_entry"><para role="column_definition">
<structfield>roleid</structfield> <type>oid</type>
diff --git a/src/backend/catalog/catalog.c b/src/backend/catalog/catalog.c
index 6f43870779..2abd6b007a 100644
--- a/src/backend/catalog/catalog.c
+++ b/src/backend/catalog/catalog.c
@@ -262,6 +262,8 @@ IsSharedRelation(Oid relationId)
relationId == AuthIdRolnameIndexId ||
relationId == AuthMemMemRoleIndexId ||
relationId == AuthMemRoleMemIndexId ||
+ relationId == AuthMemOidIndexId ||
+ relationId == AuthMemGrantorIndexId ||
relationId == DatabaseNameIndexId ||
relationId == DatabaseOidIndexId ||
relationId == DbRoleSettingDatidRolidIndexId ||
diff --git a/src/backend/catalog/dependency.c b/src/backend/catalog/dependency.c
index e119674b1f..39768fa22b 100644
--- a/src/backend/catalog/dependency.c
+++ b/src/backend/catalog/dependency.c
@@ -28,6 +28,7 @@
#include "catalog/pg_amproc.h"
#include "catalog/pg_attrdef.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_cast.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
@@ -172,6 +173,7 @@ static const Oid object_classes[] = {
TSTemplateRelationId, /* OCLASS_TSTEMPLATE */
TSConfigRelationId, /* OCLASS_TSCONFIG */
AuthIdRelationId, /* OCLASS_ROLE */
+ AuthMemRelationId, /* OCLASS_ROLE_MEMBERSHIP */
DatabaseRelationId, /* OCLASS_DATABASE */
TableSpaceRelationId, /* OCLASS_TBLSPACE */
ForeignDataWrapperRelationId, /* OCLASS_FDW */
@@ -1502,6 +1504,7 @@ doDeletion(const ObjectAddress *object, int flags)
case OCLASS_DEFACL:
case OCLASS_EVENT_TRIGGER:
case OCLASS_TRANSFORM:
+ case OCLASS_ROLE_MEMBERSHIP:
DropObjectById(object);
break;
@@ -1529,9 +1532,8 @@ doDeletion(const ObjectAddress *object, int flags)
* Accepts the same flags as performDeletion (though currently only
* PERFORM_DELETION_CONCURRENTLY does anything).
*
- * We use LockRelation for relations, LockDatabaseObject for everything
- * else. Shared-across-databases objects are not currently supported
- * because no caller cares, but could be modified to use LockSharedObject.
+ * We use LockRelation for relations, and otherwise LockSharedObject or
+ * LockDatabaseObject as appropriate for the object type.
*/
void
AcquireDeletionLock(const ObjectAddress *object, int flags)
@@ -1549,6 +1551,9 @@ AcquireDeletionLock(const ObjectAddress *object, int flags)
else
LockRelationOid(object->objectId, AccessExclusiveLock);
}
+ else if (object->classId == AuthMemRelationId)
+ LockSharedObject(object->classId, object->objectId, 0,
+ AccessExclusiveLock);
else
{
/* assume we should lock the whole object not a sub-object */
@@ -2914,6 +2919,9 @@ getObjectClass(const ObjectAddress *object)
case AuthIdRelationId:
return OCLASS_ROLE;
+ case AuthMemRelationId:
+ return OCLASS_ROLE_MEMBERSHIP;
+
case DatabaseRelationId:
return OCLASS_DATABASE;
diff --git a/src/backend/catalog/objectaddress.c b/src/backend/catalog/objectaddress.c
index 6080ff8f5f..cc80172113 100644
--- a/src/backend/catalog/objectaddress.c
+++ b/src/backend/catalog/objectaddress.c
@@ -27,6 +27,7 @@
#include "catalog/pg_amproc.h"
#include "catalog/pg_attrdef.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_cast.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_constraint.h"
@@ -386,6 +387,20 @@ static const ObjectPropertyType ObjectProperty[] =
-1,
true
},
+ {
+ "role membership",
+ AuthMemRelationId,
+ AuthMemOidIndexId,
+ -1,
+ -1,
+ Anum_pg_auth_members_oid,
+ InvalidAttrNumber,
+ InvalidAttrNumber,
+ Anum_pg_auth_members_grantor,
+ InvalidAttrNumber,
+ -1,
+ true
+ },
{
"rule",
RewriteRelationId,
@@ -787,6 +802,10 @@ static const struct object_type_map
{
"role", OBJECT_ROLE
},
+ /* OCLASS_ROLE_MEMBERSHIP */
+ {
+ "role membership", -1 /* unmapped */
+ },
/* OCLASS_DATABASE */
{
"database", OBJECT_DATABASE
@@ -3644,6 +3663,48 @@ getObjectDescription(const ObjectAddress *object, bool missing_ok)
break;
}
+ case OCLASS_ROLE_MEMBERSHIP:
+ {
+ Relation amDesc;
+ ScanKeyData skey[1];
+ SysScanDesc rcscan;
+ HeapTuple tup;
+ Form_pg_auth_members amForm;
+
+ amDesc = table_open(AuthMemRelationId, AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_auth_members_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+
+ rcscan = systable_beginscan(amDesc, AuthMemOidIndexId, true,
+ NULL, 1, skey);
+
+ tup = systable_getnext(rcscan);
+
+ if (!HeapTupleIsValid(tup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for role membership %u",
+ object->objectId);
+
+ systable_endscan(rcscan);
+ table_close(amDesc, AccessShareLock);
+ break;
+ }
+
+ amForm = (Form_pg_auth_members) GETSTRUCT(tup);
+
+ appendStringInfo(&buffer, _("membership of role %s in role %s"),
+ GetUserNameFromId(amForm->member, false),
+ GetUserNameFromId(amForm->roleid, false));
+
+ systable_endscan(rcscan);
+ table_close(amDesc, AccessShareLock);
+ break;
+ }
+
case OCLASS_DATABASE:
{
char *datname;
@@ -4533,6 +4594,10 @@ getObjectTypeDescription(const ObjectAddress *object, bool missing_ok)
appendStringInfoString(&buffer, "role");
break;
+ case OCLASS_ROLE_MEMBERSHIP:
+ appendStringInfoString(&buffer, "role membership");
+ break;
+
case OCLASS_DATABASE:
appendStringInfoString(&buffer, "database");
break;
@@ -5476,6 +5541,49 @@ getObjectIdentityParts(const ObjectAddress *object,
break;
}
+ case OCLASS_ROLE_MEMBERSHIP:
+ {
+ Relation authMemDesc;
+ ScanKeyData skey[1];
+ SysScanDesc amscan;
+ HeapTuple tup;
+ Form_pg_auth_members amForm;
+
+ authMemDesc = table_open(AuthMemRelationId,
+ AccessShareLock);
+
+ ScanKeyInit(&skey[0],
+ Anum_pg_auth_members_oid,
+ BTEqualStrategyNumber, F_OIDEQ,
+ ObjectIdGetDatum(object->objectId));
+
+ amscan = systable_beginscan(authMemDesc, AuthMemOidIndexId, true,
+ NULL, 1, skey);
+
+ tup = systable_getnext(amscan);
+
+ if (!HeapTupleIsValid(tup))
+ {
+ if (!missing_ok)
+ elog(ERROR, "could not find tuple for pg_auth_members entry %u",
+ object->objectId);
+
+ systable_endscan(amscan);
+ table_close(authMemDesc, AccessShareLock);
+ break;
+ }
+
+ amForm = (Form_pg_auth_members) GETSTRUCT(tup);
+
+ appendStringInfo(&buffer, _("membership of role %s in role %s"),
+ GetUserNameFromId(amForm->member, false),
+ GetUserNameFromId(amForm->roleid, false));
+
+ systable_endscan(amscan);
+ table_close(authMemDesc, AccessShareLock);
+ break;
+ }
+
case OCLASS_DATABASE:
{
char *datname;
diff --git a/src/backend/catalog/pg_shdepend.c b/src/backend/catalog/pg_shdepend.c
index 3e8fa008b9..f2f227f887 100644
--- a/src/backend/catalog/pg_shdepend.c
+++ b/src/backend/catalog/pg_shdepend.c
@@ -22,6 +22,7 @@
#include "catalog/dependency.h"
#include "catalog/indexing.h"
#include "catalog/pg_authid.h"
+#include "catalog/pg_auth_members.h"
#include "catalog/pg_collation.h"
#include "catalog/pg_conversion.h"
#include "catalog/pg_database.h"
@@ -1364,11 +1365,6 @@ shdepDropOwned(List *roleids, DropBehavior behavior)
case SHARED_DEPENDENCY_INVALID:
elog(ERROR, "unexpected dependency type");
break;
- case SHARED_DEPENDENCY_ACL:
- RemoveRoleFromObjectACL(roleid,
- sdepForm->classid,
- sdepForm->objid);
- break;
case SHARED_DEPENDENCY_POLICY:
/*
@@ -1398,22 +1394,37 @@ shdepDropOwned(List *roleids, DropBehavior behavior)
add_exact_object_address(&obj, deleteobjs);
}
break;
+ case SHARED_DEPENDENCY_ACL:
+
+ /*
+ * Dependencies on role grants are recorded using
+ * SHARED_DEPENDENCY_ACL, but unlike a regular ACL list
+ * which stores all permissions for a particular object in
+ * a single ACL array, there's a separate catalog row for
+ * each grant - so removing the grant just means removing
+ * the entire row.
+ */
+ if (sdepForm->classid != AuthMemRelationId)
+ {
+ RemoveRoleFromObjectACL(roleid,
+ sdepForm->classid,
+ sdepForm->objid);
+ break;
+ }
+ /* FALLTHROUGH */
case SHARED_DEPENDENCY_OWNER:
- /* If a local object, save it for deletion below */
- if (sdepForm->dbid == MyDatabaseId)
+ /* Save it for deletion below */
+ obj.classId = sdepForm->classid;
+ obj.objectId = sdepForm->objid;
+ obj.objectSubId = sdepForm->objsubid;
+ /* as above */
+ AcquireDeletionLock(&obj, 0);
+ if (!systable_recheck_tuple(scan, tuple))
{
- obj.classId = sdepForm->classid;
- obj.objectId = sdepForm->objid;
- obj.objectSubId = sdepForm->objsubid;
- /* as above */
- AcquireDeletionLock(&obj, 0);
- if (!systable_recheck_tuple(scan, tuple))
- {
- ReleaseDeletionLock(&obj);
- break;
- }
- add_exact_object_address(&obj, deleteobjs);
+ ReleaseDeletionLock(&obj);
+ break;
}
+ add_exact_object_address(&obj, deleteobjs);
break;
}
}
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index f369b1fc14..5a844b63a1 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -53,7 +53,7 @@ CREATE VIEW pg_group AS
SELECT
rolname AS groname,
oid AS grosysid,
- ARRAY(SELECT member FROM pg_auth_members WHERE roleid = oid) AS grolist
+ ARRAY(SELECT member FROM pg_auth_members WHERE roleid = pg_authid.oid) AS grolist
FROM pg_authid
WHERE NOT rolcanlogin;
diff --git a/src/backend/commands/alter.c b/src/backend/commands/alter.c
index 5456b8222b..55219bb097 100644
--- a/src/backend/commands/alter.c
+++ b/src/backend/commands/alter.c
@@ -650,6 +650,7 @@ AlterObjectNamespace_oid(Oid classId, Oid objid, Oid nspOid,
case OCLASS_TRIGGER:
case OCLASS_SCHEMA:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_FDW:
diff --git a/src/backend/commands/event_trigger.c b/src/backend/commands/event_trigger.c
index eef3e5d56e..549e30da34 100644
--- a/src/backend/commands/event_trigger.c
+++ b/src/backend/commands/event_trigger.c
@@ -1015,6 +1015,7 @@ EventTriggerSupportsObjectClass(ObjectClass objclass)
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_PARAMETER_ACL:
/* no support for global objects */
return false;
diff --git a/src/backend/commands/tablecmds.c b/src/backend/commands/tablecmds.c
index 70b94bbb39..538a35851d 100644
--- a/src/backend/commands/tablecmds.c
+++ b/src/backend/commands/tablecmds.c
@@ -12667,6 +12667,7 @@ ATExecAlterColumnType(AlteredTableInfo *tab, Relation rel,
case OCLASS_TSTEMPLATE:
case OCLASS_TSCONFIG:
case OCLASS_ROLE:
+ case OCLASS_ROLE_MEMBERSHIP:
case OCLASS_DATABASE:
case OCLASS_TBLSPACE:
case OCLASS_FDW:
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index 94135fdd6b..fc42b1cfd7 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -911,6 +911,7 @@ DropRole(DropRoleStmt *stmt)
Relation pg_authid_rel,
pg_auth_members_rel;
ListCell *item;
+ List *role_addresses = NIL;
if (!have_createrole_privilege())
ereport(ERROR,
@@ -919,7 +920,7 @@ DropRole(DropRoleStmt *stmt)
/*
* Scan the pg_authid relation to find the Oid of the role(s) to be
- * deleted.
+ * deleted and perform preliminary permissions and sanity checks.
*/
pg_authid_rel = table_open(AuthIdRelationId, RowExclusiveLock);
pg_auth_members_rel = table_open(AuthMemRelationId, RowExclusiveLock);
@@ -932,10 +933,9 @@ DropRole(DropRoleStmt *stmt)
tmp_tuple;
Form_pg_authid roleform;
ScanKeyData scankey;
- char *detail;
- char *detail_log;
SysScanDesc sscan;
Oid roleid;
+ ObjectAddress *role_address;
if (rolspec->roletype != ROLESPEC_CSTRING)
ereport(ERROR,
@@ -991,34 +991,31 @@ DropRole(DropRoleStmt *stmt)
/* DROP hook for the role being removed */
InvokeObjectDropHook(AuthIdRelationId, roleid, 0);
+ /* Don't leak the syscache tuple */
+ ReleaseSysCache(tuple);
+
/*
* Lock the role, so nobody can add dependencies to her while we drop
* her. We keep the lock until the end of transaction.
*/
LockSharedObject(AuthIdRelationId, roleid, 0, AccessExclusiveLock);
- /* Check for pg_shdepend entries depending on this role */
- if (checkSharedDependencies(AuthIdRelationId, roleid,
- &detail, &detail_log))
- ereport(ERROR,
- (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
- errmsg("role \"%s\" cannot be dropped because some objects depend on it",
- role),
- errdetail_internal("%s", detail),
- errdetail_log("%s", detail_log)));
-
- /*
- * Remove the role from the pg_authid table
- */
- CatalogTupleDelete(pg_authid_rel, &tuple->t_self);
-
- ReleaseSysCache(tuple);
-
/*
- * Remove role from the pg_auth_members table. We have to remove all
- * tuples that show it as either a role or a member.
+ * If there is a pg_auth_members entry that has one of the roles to be
+ * dropped as the roleid or member, it should be silently removed, but
+ * if there is a pg_auth_members entry that has one of the roles to be
+ * dropped as the grantor, the operation should fail.
+ *
+ * It's possible, however, that a single pg_auth_members entry could
+ * fall into multiple categories - e.g. the user could do "GRANT foo
+ * TO bar GRANTED BY baz" and then "DROP ROLE baz, bar". We want such
+ * an operation to succeed regardless of the order in which the
+ * to-be-dropped roles are passed to DROP ROLE.
*
- * XXX what about grantor entries? Maybe we should do one heap scan.
+ * To make that work, we remove all pg_auth_members entries that can
+ * be silently removed in this loop, and then below we'll make a
+ * second pass over the list of roles to be removed and check for any
+ * remaining dependencies.
*/
ScanKeyInit(&scankey,
Anum_pg_auth_members_roleid,
@@ -1030,6 +1027,11 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
+ Form_pg_auth_members authmem_form;
+
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
}
@@ -1045,22 +1047,16 @@ DropRole(DropRoleStmt *stmt)
while (HeapTupleIsValid(tmp_tuple = systable_getnext(sscan)))
{
+ Form_pg_auth_members authmem_form;
+
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(tmp_tuple);
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_auth_members_rel, &tmp_tuple->t_self);
}
systable_endscan(sscan);
- /*
- * Remove any comments or security labels on this role.
- */
- DeleteSharedComments(roleid, AuthIdRelationId);
- DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
-
- /*
- * Remove settings for this role.
- */
- DropSetting(InvalidOid, roleid);
-
/*
* Advance command counter so that later iterations of this loop will
* see the changes already made. This is essential if, for example,
@@ -1071,6 +1067,72 @@ DropRole(DropRoleStmt *stmt)
* itself.)
*/
CommandCounterIncrement();
+
+ /* Looks tentatively OK, add it to the list. */
+ role_address = palloc(sizeof(ObjectAddress));
+ role_address->classId = AuthIdRelationId;
+ role_address->objectId = roleid;
+ role_address->objectSubId = 0;
+ role_addresses = lappend(role_addresses, role_address);
+ }
+
+ /*
+ * Second pass over the roles to be removed.
+ */
+ foreach(item, role_addresses)
+ {
+ ObjectAddress *role_address = lfirst(item);
+ Oid roleid = role_address->objectId;
+ HeapTuple tuple;
+ Form_pg_authid roleform;
+ char *detail;
+ char *detail_log;
+
+ /*
+ * Re-find the pg_authid tuple.
+ *
+ * Since we've taken a lock on the role OID, it shouldn't be possible
+ * for the tuple to have been deleted -- or for that matter updated --
+ * unless the user is manually modifying the system catalogs.
+ */
+ tuple = SearchSysCache1(AUTHOID, ObjectIdGetDatum(roleid));
+ if (!HeapTupleIsValid(tuple))
+ elog(ERROR, "could not find tuple for role %u", roleid);
+ roleform = (Form_pg_authid) GETSTRUCT(tuple);
+
+ /*
+ * Check for pg_shdepend entries depending on this role.
+ *
+ * This needs to happen after we've completed removing any
+ * pg_auth_members entries that can be removed silently, in order to
+ * avoid spurious failures. See notes above for more details.
+ */
+ if (checkSharedDependencies(AuthIdRelationId, roleid,
+ &detail, &detail_log))
+ ereport(ERROR,
+ (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
+ errmsg("role \"%s\" cannot be dropped because some objects depend on it",
+ NameStr(roleform->rolname)),
+ errdetail_internal("%s", detail),
+ errdetail_log("%s", detail_log)));
+
+ /*
+ * Remove the role from the pg_authid table
+ */
+ CatalogTupleDelete(pg_authid_rel, &tuple->t_self);
+
+ ReleaseSysCache(tuple);
+
+ /*
+ * Remove any comments or security labels on this role.
+ */
+ DeleteSharedComments(roleid, AuthIdRelationId);
+ DeleteSharedSecurityLabel(roleid, AuthIdRelationId);
+
+ /*
+ * Remove settings for this role.
+ */
+ DropSetting(InvalidOid, roleid);
}
/*
@@ -1443,6 +1505,7 @@ AddRoleMems(const char *rolename, Oid roleid,
Datum new_record[Natts_pg_auth_members] = {0};
bool new_record_nulls[Natts_pg_auth_members] = {0};
bool new_record_repl[Natts_pg_auth_members] = {0};
+ Form_pg_auth_members authmem_form;
/*
* pg_database_owner is never a role member. Lifting this restriction
@@ -1488,15 +1551,22 @@ AddRoleMems(const char *rolename, Oid roleid,
authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
ObjectIdGetDatum(roleid),
ObjectIdGetDatum(memberid));
- if (HeapTupleIsValid(authmem_tuple) &&
- (!admin_opt ||
- ((Form_pg_auth_members) GETSTRUCT(authmem_tuple))->admin_option))
+ if (!HeapTupleIsValid(authmem_tuple))
{
- ereport(NOTICE,
- (errmsg("role \"%s\" is already a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
- ReleaseSysCache(authmem_tuple);
- continue;
+ authmem_form = NULL;
+ }
+ else
+ {
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (!admin_opt || authmem_form->admin_option)
+ {
+ ereport(NOTICE,
+ (errmsg("role \"%s\" is already a member of role \"%s\"",
+ get_rolespec_name(memberRole), rolename)));
+ ReleaseSysCache(authmem_tuple);
+ continue;
+ }
}
/* Build a tuple to insert or update */
@@ -1513,13 +1583,41 @@ AddRoleMems(const char *rolename, Oid roleid,
new_record,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
+
+ if (authmem_form->grantor != grantorId)
+ {
+ Oid *oldmembers = palloc(sizeof(Oid));
+ Oid *newmembers = palloc(sizeof(Oid));
+
+ /* updateAclDependencies wants to pfree array inputs */
+ oldmembers[0] = authmem_form->grantor;
+ newmembers[0] = grantorId;
+
+ updateAclDependencies(AuthMemRelationId, authmem_form->oid,
+ 0, InvalidOid,
+ 1, oldmembers,
+ 1, newmembers);
+ }
+
ReleaseSysCache(authmem_tuple);
}
else
{
+ Oid objectId;
+ Oid *newmembers = palloc(sizeof(Oid));
+
+ objectId = GetNewObjectId();
+ new_record[Anum_pg_auth_members_oid - 1] = objectId;
tuple = heap_form_tuple(pg_authmem_dsc,
new_record, new_record_nulls);
CatalogTupleInsert(pg_authmem_rel, tuple);
+
+ /* updateAclDependencies wants to pfree array inputs */
+ newmembers[0] = grantorId;
+ updateAclDependencies(AuthMemRelationId, objectId,
+ 0, InvalidOid,
+ 0, NULL,
+ 1, newmembers);
}
/* CCI after each change, in case there are duplicates in list */
@@ -1586,6 +1684,7 @@ DelRoleMems(const char *rolename, Oid roleid,
RoleSpec *memberRole = lfirst(specitem);
Oid memberid = lfirst_oid(iditem);
HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
/*
* Find entry for this role/member
@@ -1601,9 +1700,16 @@ DelRoleMems(const char *rolename, Oid roleid,
continue;
}
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
if (!admin_opt)
{
- /* Remove the entry altogether */
+ /*
+ * Remove the entry altogether, after first removing its
+ * dependencies
+ */
+ deleteSharedDependencyRecordsFor(AuthMemRelationId,
+ authmem_form->oid, 0);
CatalogTupleDelete(pg_authmem_rel, &authmem_tuple->t_self);
}
else
diff --git a/src/include/catalog/dependency.h b/src/include/catalog/dependency.h
index 6684933dac..729c4c46c0 100644
--- a/src/include/catalog/dependency.h
+++ b/src/include/catalog/dependency.h
@@ -112,6 +112,7 @@ typedef enum ObjectClass
OCLASS_TSTEMPLATE, /* pg_ts_template */
OCLASS_TSCONFIG, /* pg_ts_config */
OCLASS_ROLE, /* pg_authid */
+ OCLASS_ROLE_MEMBERSHIP, /* pg_auth_members */
OCLASS_DATABASE, /* pg_database */
OCLASS_TBLSPACE, /* pg_tablespace */
OCLASS_FDW, /* pg_foreign_data_wrapper */
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index 1bc027f133..c9d7697730 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -29,6 +29,7 @@
*/
CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_OID(2843,AuthMemRelation_Rowtype_Id) BKI_SCHEMA_MACRO
{
+ Oid oid; /* oid */
Oid roleid BKI_LOOKUP(pg_authid); /* ID of a role */
Oid member BKI_LOOKUP(pg_authid); /* ID of a member of that role */
Oid grantor BKI_LOOKUP(pg_authid); /* who granted the membership */
@@ -42,7 +43,9 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
*/
typedef FormData_pg_auth_members *Form_pg_auth_members;
-DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
+DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_oid_index, 9385, AuthMemOidIndexId, on pg_auth_members using btree(oid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops));
+DECLARE_INDEX(pg_auth_members_grantor_index, 9384, AuthMemGrantorIndexId, on pg_auth_members using btree(grantor oid_ops));
#endif /* PG_AUTH_MEMBERS_H */
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index 4e67d72760..c2465d0f49 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -103,9 +103,21 @@ ERROR: role "regress_nosuch_recursive" does not exist
DROP ROLE regress_nosuch_admin_recursive;
ERROR: role "regress_nosuch_admin_recursive" does not exist
DROP ROLE regress_plainrole;
+-- fail, can't drop regress_createrole yet, due to outstanding grants
+DROP ROLE regress_createrole;
+ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_read_all_data in role pg_read_all_data
+privileges for membership of role regress_write_all_data in role pg_write_all_data
+privileges for membership of role regress_monitor in role pg_monitor
+privileges for membership of role regress_read_all_settings in role pg_read_all_settings
+privileges for membership of role regress_read_all_stats in role pg_read_all_stats
+privileges for membership of role regress_stat_scan_tables in role pg_stat_scan_tables
+privileges for membership of role regress_read_server_files in role pg_read_server_files
+privileges for membership of role regress_write_server_files in role pg_write_server_files
+privileges for membership of role regress_execute_server_program in role pg_execute_server_program
+privileges for membership of role regress_signal_backend in role pg_signal_backend
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
-DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -125,6 +137,8 @@ DROP ROLE regress_read_server_files;
DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
+-- ok, dropped the other roles first so this is ok now
+DROP ROLE regress_createrole;
-- fail, role still owns database objects
DROP ROLE regress_tenant;
ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index e10dd6f9ae..65b4a22ebc 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -33,6 +33,38 @@ CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- fail, dependency
+ERROR: role "regress_priv_user2" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user3 in role regress_priv_user1
+REASSIGN OWNED BY regress_priv_user2 TO regress_priv_user4;
+DROP ROLE regress_priv_user2; -- still fail, REASSIGN OWNED doesn't help
+ERROR: role "regress_priv_user2" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user3 in role regress_priv_user1
+DROP OWNED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- ok now, DROP OWNED does the job
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user4 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- should fail, dependency
+ERROR: role "regress_priv_user3" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user4 in role regress_priv_user1
+DROP ROLE regress_priv_user4; -- ok
+DROP ROLE regress_priv_user3; -- ok now
+GRANT regress_priv_user1 TO regress_priv_user5 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user6 GRANTED BY regress_priv_user5;
+DROP ROLE regress_priv_user5; -- should fail, dependency
+ERROR: role "regress_priv_user5" cannot be dropped because some objects depend on it
+DETAIL: privileges for membership of role regress_priv_user6 in role regress_priv_user1
+DROP ROLE regress_priv_user1, regress_priv_user5; -- ok, despite order
+-- recreate the roles we just dropped
+CREATE USER regress_priv_user1;
+CREATE USER regress_priv_user2;
+CREATE USER regress_priv_user3;
+CREATE USER regress_priv_user4;
+CREATE USER regress_priv_user5;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index 292dc08797..b696628238 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -98,9 +98,11 @@ DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
+-- fail, can't drop regress_createrole yet, due to outstanding grants
+DROP ROLE regress_createrole;
+
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
-DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -121,6 +123,9 @@ DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
+-- ok, dropped the other roles first so this is ok now
+DROP ROLE regress_createrole;
+
-- fail, role still owns database objects
DROP ROLE regress_tenant;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 6d1fd3391a..66834e32a7 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -37,6 +37,33 @@ CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- fail, dependency
+REASSIGN OWNED BY regress_priv_user2 TO regress_priv_user4;
+DROP ROLE regress_priv_user2; -- still fail, REASSIGN OWNED doesn't help
+DROP OWNED BY regress_priv_user2;
+DROP ROLE regress_priv_user2; -- ok now, DROP OWNED does the job
+
+-- test that removing granted role or grantee role removes dependency
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user4 GRANTED BY regress_priv_user3;
+DROP ROLE regress_priv_user3; -- should fail, dependency
+DROP ROLE regress_priv_user4; -- ok
+DROP ROLE regress_priv_user3; -- ok now
+GRANT regress_priv_user1 TO regress_priv_user5 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user6 GRANTED BY regress_priv_user5;
+DROP ROLE regress_priv_user5; -- should fail, dependency
+DROP ROLE regress_priv_user1, regress_priv_user5; -- ok, despite order
+
+-- recreate the roles we just dropped
+CREATE USER regress_priv_user1;
+CREATE USER regress_priv_user2;
+CREATE USER regress_priv_user3;
+CREATE USER regress_priv_user4;
+CREATE USER regress_priv_user5;
+
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
--
2.24.3 (Apple Git-128)
v5-0002-Make-role-grant-system-more-consistent-with-other.patchapplication/octet-stream; name=v5-0002-Make-role-grant-system-more-consistent-with-other.patchDownload
From 4e561f44f8fbc1aea6422a60cf1bfac598135101 Mon Sep 17 00:00:00 2001
From: Robert Haas <rhaas@postgresql.org>
Date: Wed, 10 Aug 2022 16:20:58 -0400
Subject: [PATCH v5 2/2] Make role grant system more consistent with other
privileges.
Previously, membership of role A in role B could be recorded in the
catalog tables only once. This meant that a new grant of role A to
role B would overwrite the previous grant. For other object types, a
new grant of permission on an object - in this case role A - exists
along side the existing grant provided that the grantor is different.
Either grant can be revoked independently of the other, and
permissions remain so long as at least one grant remains. Make role
grants work similarly.
Previously, when granting membership in a role, the superuser could
specify any role whatsoever as the grantor, but for other object types,
the grantor of record must be either the owner of the object, or a
role that currently has privileges to perform a similar GRANT.
Implement the same scheme for role grants, treating the bootstrap
superuser as the role owner since roles do not have owners. This means
that attempting to revoke a grant, or admin option on a grant, can now
fail if there are dependent privileges, and that CASCADE can be used
to revoke these. It also means that you can't grant ADMIN OPTION on
a role back to a user who granted it directly or indirectly to you,
similar to how you can't give WITH GRANT OPTION on a privilege back
to a role which granted it directly or indirectly to you.
Previously, only the superuser could specify GRANTED BY with a user
other than the current user. Relax that rule to allow the grantor
to be any role whose privileges the current user posseses. This
doesn't improve compatibility with what we do for other object types,
where support for GRANTED BY is entirely vestigial, but it makes this
feature more usable and seems to make sense to change at the same time
we're changing related behaviors.
Along the way, fix "ALTER GROUP group_name ADD USER user_name" to
require the same privileges as "GRANT group_name TO user_name".
Previously, CREATEROLE privileges were sufficient for either, but
only the former form was permissible with ADMIN OPTION on the role.
Now, either CREATEROLE or ADMIN OPTION on the role suffices for
either spelling.
---
doc/src/sgml/ref/alter_group.sgml | 6 +-
doc/src/sgml/ref/grant.sgml | 12 +-
doc/src/sgml/ref/revoke.sgml | 12 +-
src/backend/commands/user.c | 565 +++++++++++++++++++---
src/backend/parser/gram.y | 2 +
src/backend/utils/adt/acl.c | 47 +-
src/backend/utils/cache/syscache.c | 8 +-
src/bin/pg_dump/pg_dumpall.c | 177 ++++++-
src/include/catalog/pg_auth_members.h | 4 +-
src/include/utils/acl.h | 1 +
src/test/regress/expected/create_role.out | 16 +-
src/test/regress/expected/privileges.out | 73 ++-
src/test/regress/sql/create_role.sql | 7 +-
src/test/regress/sql/privileges.sql | 42 +-
14 files changed, 833 insertions(+), 139 deletions(-)
diff --git a/doc/src/sgml/ref/alter_group.sgml b/doc/src/sgml/ref/alter_group.sgml
index fa4a8df912..b9e641818c 100644
--- a/doc/src/sgml/ref/alter_group.sgml
+++ b/doc/src/sgml/ref/alter_group.sgml
@@ -52,7 +52,11 @@ ALTER GROUP <replaceable class="parameter">group_name</replaceable> RENAME TO <r
equivalent to granting or revoking membership in the role named as the
<quote>group</quote>; so the preferred way to do this is to use
<link linkend="sql-grant"><command>GRANT</command></link> or
- <link linkend="sql-revoke"><command>REVOKE</command></link>.
+ <link linkend="sql-revoke"><command>REVOKE</command></link>. Note that
+ <command>GRANT</command> and <command>REVOKE</command> have additional
+ options which are not available with this command, such as the ability
+ to grant and revoke <literal>ADMIN OPTION</literal>, and the ability to
+ specify the grantor.
</para>
<para>
diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml
index f744b05b55..2fd0f34d55 100644
--- a/doc/src/sgml/ref/grant.sgml
+++ b/doc/src/sgml/ref/grant.sgml
@@ -267,8 +267,14 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
<para>
If <literal>GRANTED BY</literal> is specified, the grant is recorded as
- having been done by the specified role. Only database superusers may
- use this option, except when it names the same role executing the command.
+ having been done by the specified role. A user can only attribute a grant
+ to another role if they possess the privileges of that role. The role
+ recorded as the grantor must have <literal>ADMIN OPTION</literal> on the
+ target role, unless it is the bootstrap superuser. When a grant is recorded
+ as having a grantor other than the bootstrap superuser, it depends on the
+ grantor continuing to posess <literal>ADMIN OPTION</literal> on the role;
+ so, if <literal>ADMIN OPTION</literal> is revoked, dependent grants must
+ be revoked as well.
</para>
<para>
@@ -333,7 +339,7 @@ GRANT <replaceable class="parameter">role_name</replaceable> [, ...] TO <replace
owner of the affected object. In particular, privileges granted via
such a command will appear to have been granted by the object owner.
(For role membership, the membership appears to have been granted
- by the containing role itself.)
+ by the bootstrap superuser.)
</para>
<para>
diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml
index 62f1971036..16e840458c 100644
--- a/doc/src/sgml/ref/revoke.sgml
+++ b/doc/src/sgml/ref/revoke.sgml
@@ -198,9 +198,10 @@ REVOKE [ ADMIN OPTION FOR ]
<para>
When revoking membership in a role, <literal>GRANT OPTION</literal> is instead
called <literal>ADMIN OPTION</literal>, but the behavior is similar.
- This form of the command also allows a <literal>GRANTED BY</literal>
- option, but that option is currently ignored (except for checking
- the existence of the named role).
+ Note that, in releases prior to <productname>PostgreSQL</productname> 16,
+ dependent privileges were not tracked for grants of role membership,
+ and thus <literal>CASCADE</literal> had no effect for role membership.
+ This is no longer the case.
Note also that this form of the command does not
allow the noise word <literal>GROUP</literal>
in <replaceable class="parameter">role_specification</replaceable>.
@@ -239,7 +240,10 @@ REVOKE [ ADMIN OPTION FOR ]
<para>
If a superuser chooses to issue a <command>GRANT</command> or <command>REVOKE</command>
command, the command is performed as though it were issued by the
- owner of the affected object. Since all privileges ultimately come
+ owner of the affected object. (Since roles do not have owners, in the
+ case of a <command>GRANT</command> of role membership, the command is
+ performed as though it were issued by the bootstrap superuser.)
+ Since all privileges ultimately come
from the object owner (possibly indirectly via chains of grant options),
it is possible for a superuser to revoke all privileges, but this might
require use of <literal>CASCADE</literal> as stated above.
diff --git a/src/backend/commands/user.c b/src/backend/commands/user.c
index fc42b1cfd7..c482a53b3c 100644
--- a/src/backend/commands/user.c
+++ b/src/backend/commands/user.c
@@ -35,10 +35,32 @@
#include "storage/lmgr.h"
#include "utils/acl.h"
#include "utils/builtins.h"
+#include "utils/catcache.h"
#include "utils/fmgroids.h"
#include "utils/syscache.h"
#include "utils/timestamp.h"
+/*
+ * Removing a role grant - or the admin option on it - might recurse to
+ * dependent grants. We use these values to reason about what would need to
+ * be done in such cases.
+ *
+ * RRG_NOOP indicates a grant that would not need to be altered by the
+ * operation.
+ *
+ * RRG_REMOVE_ADMIN_OPTION indicates a grant that would need to have
+ * admin_option set to false by the operation.
+ *
+ * RRG_DELETE_GRANT indicates a grant that would need to be removed entirely
+ * by the operation.
+ */
+typedef enum
+{
+ RRG_NOOP,
+ RRG_REMOVE_ADMIN_OPTION,
+ RRG_DELETE_GRANT
+} RevokeRoleGrantAction;
+
/* Potentially set by pg_upgrade_support functions */
Oid binary_upgrade_next_pg_authid_oid = InvalidOid;
@@ -54,7 +76,22 @@ static void AddRoleMems(const char *rolename, Oid roleid,
Oid grantorId, bool admin_opt);
static void DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
- bool admin_opt);
+ Oid grantorId, bool admin_opt, DropBehavior behavior);
+static Oid check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId,
+ bool is_grant);
+static RevokeRoleGrantAction *initialize_revoke_actions(CatCList *memlist);
+static bool plan_single_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions,
+ Oid member, Oid grantor,
+ bool revoke_admin_option_only,
+ DropBehavior behavior);
+static void plan_member_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions, Oid member);
+static void plan_recursive_revoke(CatCList *memlist,
+ RevokeRoleGrantAction *actions,
+ int index,
+ bool revoke_admin_option_only,
+ DropBehavior behavior);
/* Check if current user has createrole privileges */
@@ -449,7 +486,7 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
AddRoleMems(oldrolename, oldroleid,
thisrole_list,
thisrole_oidlist,
- GetUserId(), false);
+ InvalidOid, false);
ReleaseSysCache(oldroletup);
}
@@ -461,10 +498,10 @@ CreateRole(ParseState *pstate, CreateRoleStmt *stmt)
*/
AddRoleMems(stmt->role, roleid,
adminmembers, roleSpecsToIds(adminmembers),
- GetUserId(), true);
+ InvalidOid, true);
AddRoleMems(stmt->role, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- GetUserId(), false);
+ InvalidOid, false);
/* Post creation hook for new role */
InvokeObjectPostCreateHook(AuthIdRelationId, roleid, 0);
@@ -624,7 +661,8 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
* To mess with a superuser or replication role in any way you gotta be
* superuser. We also insist on superuser to change the BYPASSRLS
* property. Otherwise, if you don't have createrole, you're only allowed
- * to change your own password.
+ * to (1) change your own password or (2) add members to a role for which
+ * you have ADMIN OPTION.
*/
if (authform->rolsuper || dissuper)
{
@@ -649,12 +687,25 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
}
else if (!have_createrole_privilege())
{
- /* check the rest */
+ /* things you certainly can't do without CREATEROLE */
if (dinherit || dcreaterole || dcreatedb || dcanlogin || dconnlimit ||
- drolemembers || dvalidUntil || !dpassword || roleid != GetUserId())
+ dvalidUntil)
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("permission denied")));
+
+ /* without CREATEROLE, can only change your own password */
+ if (dpassword && roleid != GetUserId())
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have CREATEROLE privilege to change another user's password")));
+
+ /* without CREATEROLE, can only add members to roles you admin */
+ if (drolemembers && !is_admin_of_role(GetUserId(), roleid))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("must have admin option on role \"%s\" to add members",
+ rolename)));
}
/* Convert validuntil to internal form */
@@ -805,11 +856,11 @@ AlterRole(ParseState *pstate, AlterRoleStmt *stmt)
if (stmt->action == +1) /* add members to role */
AddRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- GetUserId(), false);
+ InvalidOid, false);
else if (stmt->action == -1) /* drop members from role */
DelRoleMems(rolename, roleid,
rolemembers, roleSpecsToIds(rolemembers),
- false);
+ InvalidOid, false, DROP_RESTRICT);
}
/*
@@ -1296,7 +1347,7 @@ GrantRole(GrantRoleStmt *stmt)
if (stmt->grantor)
grantor = get_rolespec_oid(stmt->grantor, false);
else
- grantor = GetUserId();
+ grantor = InvalidOid;
grantee_ids = roleSpecsToIds(stmt->grantee_roles);
@@ -1330,7 +1381,7 @@ GrantRole(GrantRoleStmt *stmt)
else
DelRoleMems(rolename, roleid,
stmt->grantee_roles, grantee_ids,
- stmt->admin_opt);
+ grantor, stmt->admin_opt, stmt->behavior);
}
/*
@@ -1431,7 +1482,7 @@ roleSpecsToIds(List *memberNames)
* roleid: OID of role to add to
* memberSpecs: list of RoleSpec of roles to add (used only for error messages)
* memberIds: OIDs of roles to add
- * grantorId: who is granting the membership
+ * grantorId: who is granting the membership (InvalidOid if not set explicitly)
* admin_opt: granting admin option?
*/
static void
@@ -1443,6 +1494,7 @@ AddRoleMems(const char *rolename, Oid roleid,
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
+ Oid currentUserId = GetUserId();
Assert(list_length(memberSpecs) == list_length(memberIds));
@@ -1464,7 +1516,7 @@ AddRoleMems(const char *rolename, Oid roleid,
else
{
if (!have_createrole_privilege() &&
- !is_admin_of_role(grantorId, roleid))
+ !is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
@@ -1483,29 +1535,25 @@ AddRoleMems(const char *rolename, Oid roleid,
ereport(ERROR,
errmsg("role \"%s\" cannot have explicit members", rolename));
- /*
- * The role membership grantor of record has little significance at
- * present. Nonetheless, inasmuch as users might look to it for a crude
- * audit trail, let only superusers impute the grant to a third party.
- */
- if (grantorId != GetUserId() && !superuser())
- ereport(ERROR,
- (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
- errmsg("must be superuser to set grantor")));
+ /* Validate grantor (and resolve implicit grantor if not specified). */
+ grantorId = check_role_grantor(currentUserId, roleid, grantorId, true);
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
+ /*
+ * Only allow changes to this role by one backend at a time, so that we
+ * can check integrity constraints like the lack of circular ADMIN OPTION
+ * grants without fear of race conditions.
+ */
+ LockSharedObject(AuthIdRelationId, roleid, 0,
+ ShareUpdateExclusiveLock);
+
+ /* Preliminary sanity checks. */
forboth(specitem, memberSpecs, iditem, memberIds)
{
RoleSpec *memberRole = lfirst_node(RoleSpec, specitem);
Oid memberid = lfirst_oid(iditem);
- HeapTuple authmem_tuple;
- HeapTuple tuple;
- Datum new_record[Natts_pg_auth_members] = {0};
- bool new_record_nulls[Natts_pg_auth_members] = {0};
- bool new_record_repl[Natts_pg_auth_members] = {0};
- Form_pg_auth_members authmem_form;
/*
* pg_database_owner is never a role member. Lifting this restriction
@@ -1543,14 +1591,94 @@ AddRoleMems(const char *rolename, Oid roleid,
(errcode(ERRCODE_INVALID_GRANT_OPERATION),
errmsg("role \"%s\" is a member of role \"%s\"",
rolename, get_rolespec_name(memberRole))));
+ }
+
+ /*
+ * Disallow attempts to grant ADMIN OPTION back to a user who granted it
+ * to you, similar to what check_circularity does for ACLs. We want the
+ * chains of grants to remain acyclic, so that it's always possible to use
+ * REVOKE .. CASCADE to clean up all grants that depend on the one being
+ * revoked.
+ *
+ * NB: This check might look redundant with the check for membership loops
+ * above, but it isn't. That's checking for role-member loop (e.g. A is a
+ * member of B and B is a member of A) while this is checking for a
+ * member-grantor loop (e.g. A gave ADMIN OPTION to X to B and now B, who
+ * has no other source of ADMIN OPTION on X, tries to give ADMIN OPTION on
+ * X back to A).
+ */
+ if (admin_opt && grantorId != BOOTSTRAP_SUPERUSERID)
+ {
+ CatCList *memlist;
+ RevokeRoleGrantAction *actions;
+ int i;
+
+ /* Get the list of members for this role. */
+ memlist = SearchSysCacheList1(AUTHMEMROLEMEM,
+ ObjectIdGetDatum(roleid));
+
+ /*
+ * Figure out what would happen if we removed all existing grants to
+ * every role to which we've been asked to make a new grant.
+ */
+ actions = initialize_revoke_actions(memlist);
+ foreach(iditem, memberIds)
+ {
+ Oid memberid = lfirst_oid(iditem);
+
+ if (memberid == BOOTSTRAP_SUPERUSERID)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_GRANT_OPERATION),
+ errmsg("admin option cannot be granted back to your own grantor")));
+ plan_member_revoke(memlist, actions, memberid);
+ }
+
+ /*
+ * If the result would be that the grantor role would no longer have
+ * the ability to perform the grant, then the proposed grant would
+ * create a circularity.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (actions[i] == RRG_NOOP &&
+ authmem_form->member == grantorId &&
+ authmem_form->admin_option)
+ break;
+ }
+ if (i >= memlist->n_members)
+ ereport(ERROR,
+ (errcode(ERRCODE_INVALID_GRANT_OPERATION),
+ errmsg("admin option cannot be granted back to your own grantor")));
+
+ ReleaseSysCacheList(memlist);
+ }
+
+ /* Now perform the catalog updates. */
+ forboth(specitem, memberSpecs, iditem, memberIds)
+ {
+ RoleSpec *memberRole = lfirst_node(RoleSpec, specitem);
+ Oid memberid = lfirst_oid(iditem);
+ HeapTuple authmem_tuple;
+ HeapTuple tuple;
+ Datum new_record[Natts_pg_auth_members] = {0};
+ bool new_record_nulls[Natts_pg_auth_members] = {0};
+ bool new_record_repl[Natts_pg_auth_members] = {0};
+ Form_pg_auth_members authmem_form;
/*
* Check if entry for this role/member already exists; if so, give
* warning unless we are adding admin option.
*/
- authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
+ authmem_tuple = SearchSysCache3(AUTHMEMROLEMEM,
ObjectIdGetDatum(roleid),
- ObjectIdGetDatum(memberid));
+ ObjectIdGetDatum(memberid),
+ ObjectIdGetDatum(grantorId));
if (!HeapTupleIsValid(authmem_tuple))
{
authmem_form = NULL;
@@ -1562,8 +1690,9 @@ AddRoleMems(const char *rolename, Oid roleid,
if (!admin_opt || authmem_form->admin_option)
{
ereport(NOTICE,
- (errmsg("role \"%s\" is already a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
+ (errmsg("role \"%s\" has already been granted membership in role \"%s\" by role \"%s\"",
+ get_rolespec_name(memberRole), rolename,
+ GetUserNameFromId(grantorId, false))));
ReleaseSysCache(authmem_tuple);
continue;
}
@@ -1577,28 +1706,12 @@ AddRoleMems(const char *rolename, Oid roleid,
if (HeapTupleIsValid(authmem_tuple))
{
- new_record_repl[Anum_pg_auth_members_grantor - 1] = true;
new_record_repl[Anum_pg_auth_members_admin_option - 1] = true;
tuple = heap_modify_tuple(authmem_tuple, pg_authmem_dsc,
new_record,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
- if (authmem_form->grantor != grantorId)
- {
- Oid *oldmembers = palloc(sizeof(Oid));
- Oid *newmembers = palloc(sizeof(Oid));
-
- /* updateAclDependencies wants to pfree array inputs */
- oldmembers[0] = authmem_form->grantor;
- newmembers[0] = grantorId;
-
- updateAclDependencies(AuthMemRelationId, authmem_form->oid,
- 0, InvalidOid,
- 1, oldmembers,
- 1, newmembers);
- }
-
ReleaseSysCache(authmem_tuple);
}
else
@@ -1637,17 +1750,23 @@ AddRoleMems(const char *rolename, Oid roleid,
* roleid: OID of role to del from
* memberSpecs: list of RoleSpec of roles to del (used only for error messages)
* memberIds: OIDs of roles to del
+ * grantorId: who is revoking the membership
* admin_opt: remove admin option only?
+ * behavior: RESTRICT or CASCADE behavior for recursive removal
*/
static void
DelRoleMems(const char *rolename, Oid roleid,
List *memberSpecs, List *memberIds,
- bool admin_opt)
+ Oid grantorId, bool admin_opt, DropBehavior behavior)
{
Relation pg_authmem_rel;
TupleDesc pg_authmem_dsc;
ListCell *specitem;
ListCell *iditem;
+ Oid currentUserId = GetUserId();
+ CatCList *memlist;
+ RevokeRoleGrantAction *actions;
+ int i;
Assert(list_length(memberSpecs) == list_length(memberIds));
@@ -1669,40 +1788,69 @@ DelRoleMems(const char *rolename, Oid roleid,
else
{
if (!have_createrole_privilege() &&
- !is_admin_of_role(GetUserId(), roleid))
+ !is_admin_of_role(currentUserId, roleid))
ereport(ERROR,
(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
errmsg("must have admin option on role \"%s\"",
rolename)));
}
+ /* Validate grantor (and resolve implicit grantor if not specified). */
+ grantorId = check_role_grantor(currentUserId, roleid, grantorId, false);
+
pg_authmem_rel = table_open(AuthMemRelationId, RowExclusiveLock);
pg_authmem_dsc = RelationGetDescr(pg_authmem_rel);
+ /*
+ * Only allow changes to this role by one backend at a time, so that we
+ * can check for things like dependent privileges without fear of race
+ * conditions.
+ */
+ LockSharedObject(AuthIdRelationId, roleid, 0,
+ ShareUpdateExclusiveLock);
+
+ memlist = SearchSysCacheList1(AUTHMEMROLEMEM, ObjectIdGetDatum(roleid));
+ actions = initialize_revoke_actions(memlist);
+
+ /*
+ * We may need to recurse to dependent privileges if DROP_CASCADE was
+ * specified, or refuse to perform the operation if dependent privileges
+ * exist and DROP_RESTRICT was specified. plan_single_revoke() will figure
+ * out what to do with each catalog tuple.
+ */
forboth(specitem, memberSpecs, iditem, memberIds)
{
RoleSpec *memberRole = lfirst(specitem);
Oid memberid = lfirst_oid(iditem);
- HeapTuple authmem_tuple;
- Form_pg_auth_members authmem_form;
- /*
- * Find entry for this role/member
- */
- authmem_tuple = SearchSysCache2(AUTHMEMROLEMEM,
- ObjectIdGetDatum(roleid),
- ObjectIdGetDatum(memberid));
- if (!HeapTupleIsValid(authmem_tuple))
+ if (!plan_single_revoke(memlist, actions, memberid, grantorId,
+ admin_opt, behavior))
{
ereport(WARNING,
- (errmsg("role \"%s\" is not a member of role \"%s\"",
- get_rolespec_name(memberRole), rolename)));
+ (errmsg("role \"%s\" has not been granted membership in role \"%s\" by role \"%s\"",
+ get_rolespec_name(memberRole), rolename,
+ GetUserNameFromId(grantorId, false))));
continue;
}
+ }
+ /*
+ * We now know what to do with each catalog tuple: it should either be
+ * left alone, deleted, or just have the admin_option flag cleared.
+ * Perform the appropriate action in each case.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ if (actions[i] == RRG_NOOP)
+ continue;
+
+ authmem_tuple = &memlist->members[i]->tuple;
authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
- if (!admin_opt)
+ if (actions[i] == RRG_DELETE_GRANT)
{
/*
* Remove the entry altogether, after first removing its
@@ -1729,15 +1877,298 @@ DelRoleMems(const char *rolename, Oid roleid,
new_record_nulls, new_record_repl);
CatalogTupleUpdate(pg_authmem_rel, &tuple->t_self, tuple);
}
-
- ReleaseSysCache(authmem_tuple);
-
- /* CCI after each change, in case there are duplicates in list */
- CommandCounterIncrement();
}
+ ReleaseSysCacheList(memlist);
+
/*
* Close pg_authmem, but keep lock till commit.
*/
table_close(pg_authmem_rel, NoLock);
}
+
+/*
+ * Sanity-check, or infer, the grantor for a GRANT or REVOKE statement
+ * targeting a role.
+ *
+ * The grantor must always be either a role with ADMIN OPTION on the role in
+ * which membership is being granted, or the bootstrap superuser. This is
+ * similar to the restriction enforced by select_best_grantor, except that
+ * roles don't have owners, so we regard the bootstrap superuser as the
+ * implicit owner.
+ *
+ * If the grantor was not explicitly specified by the user, grantorId should
+ * be passed as InvalidOid, and this function will infer the user to be
+ * recorded as the grantor. In many cases, this will be the current user, but
+ * things get more complicated when the current user doesn't possess ADMIN
+ * OPTION on the role but rather relies on having CREATEROLE privileges, or
+ * on inheriting the privileges of a role which does have ADMIN OPTION. See
+ * below for details.
+ *
+ * If the grantor was specified by the user, then it must be a user that
+ * can legally be recorded as the grantor, as per the rule stated above.
+ * This is an integrity constraint, not a permissions check, and thus even
+ * superusers are subject to this restriction. However, there is also a
+ * permissions check: to specify a role as the grantor, the current user
+ * must possess the privileges of that role. Superusers will always pass
+ * this check, but for non-superusers it may lead to an error.
+ *
+ * The return value is the OID to be regarded as the grantor when executing
+ * the operation.
+ */
+static Oid
+check_role_grantor(Oid currentUserId, Oid roleid, Oid grantorId, bool is_grant)
+{
+ /* If the grantor ID was not specified, pick one to use. */
+ if (!OidIsValid(grantorId))
+ {
+ /*
+ * Grants where the grantor is recorded as the bootstrap superuser do
+ * not depend on any other existing grants, so always default to this
+ * interpretation when possible.
+ */
+ if (has_createrole_privilege(currentUserId))
+ return BOOTSTRAP_SUPERUSERID;
+
+ /*
+ * Otherwise, the grantor must either have ADMIN OPTION on the role or
+ * inherit the privileges of a role which does. In the former case,
+ * record the grantor as the current user; in the latter, pick one of
+ * the roles that is "most directly" inherited by the current role
+ * (i.e. fewest "hops").
+ *
+ * (We shouldn't fail to find a best grantor, because we've already
+ * established that the current user has permission to perform the
+ * operation.)
+ */
+ grantorId = select_best_admin(currentUserId, roleid);
+ if (!OidIsValid(grantorId))
+ elog(ERROR, "no possible grantors");
+ return grantorId;
+ }
+
+ /*
+ * If an explicit grantor is specified, it must be a role whose privileges
+ * the current user possesses.
+ *
+ * It should also be a role that has ADMIN OPTION on the target role, but
+ * we check this condition only in case of GRANT. For REVOKE, no matching
+ * grant should exist anyway, but if it somehow does, let the user get rid
+ * of it.
+ */
+ if (is_grant)
+ {
+ if (!has_privs_of_role(currentUserId, grantorId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to grant privileges as role \"%s\"",
+ GetUserNameFromId(grantorId, false))));
+
+ if (grantorId != BOOTSTRAP_SUPERUSERID &&
+ select_best_admin(grantorId, roleid) != grantorId)
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("grantor must have ADMIN OPTION on \"%s\"",
+ GetUserNameFromId(roleid, false))));
+ }
+ else
+ {
+ if (!has_privs_of_role(currentUserId, grantorId))
+ ereport(ERROR,
+ (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+ errmsg("permission denied to revoke privileges granted by role \"%s\"",
+ GetUserNameFromId(grantorId, false))));
+ }
+
+ /*
+ * If a grantor was specified explicitly, always attribute the grant to
+ * that role (unless we error out above).
+ */
+ return grantorId;
+}
+
+/*
+ * Initialize an array of RevokeRoleGrantAction objects.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * This constructs an array indicating that no actions are to be performed;
+ * that is, every element is initially RRG_NOOP.
+ */
+static RevokeRoleGrantAction *
+initialize_revoke_actions(CatCList *memlist)
+{
+ RevokeRoleGrantAction *result;
+ int i;
+
+ if (memlist->n_members == 0)
+ return NULL;
+
+ result = palloc(sizeof(RevokeRoleGrantAction) * memlist->n_members);
+ for (i = 0; i < memlist->n_members; i++)
+ result[i] = RRG_NOOP;
+ return result;
+}
+
+/*
+ * Figure out what we would need to do in order to revoke a grant, or just the
+ * admin option on a grant, given that there might be dependent privileges.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * Whatever actions prove to be necessary will be signalled by updating
+ * 'actions'.
+ *
+ * If behavior is DROP_RESTRICT, an error will occur if there are dependent
+ * role membership grants; if DROP_CASCADE, those grants will be scheduled
+ * for deletion.
+ *
+ * The return value is true if the matching grant was found in the list,
+ * and false if not.
+ */
+static bool
+plan_single_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ Oid member, Oid grantor, bool revoke_admin_option_only,
+ DropBehavior behavior)
+{
+ int i;
+
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (authmem_form->member == member &&
+ authmem_form->grantor == grantor)
+ {
+ plan_recursive_revoke(memlist, actions, i,
+ revoke_admin_option_only, behavior);
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/*
+ * Figure out what we would need to do in order to revoke all grants to
+ * a given member, given that there might be dependent privileges.
+ *
+ * 'memlist' should be a list of all grants for the target role.
+ *
+ * Whatever actions prove to be necessary will be signalled by updating
+ * 'actions'.
+ */
+static void
+plan_member_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ Oid member)
+{
+ int i;
+
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+
+ authmem_tuple = &memlist->members[i]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ if (authmem_form->member == member)
+ plan_recursive_revoke(memlist, actions, i, false, DROP_CASCADE);
+ }
+}
+
+/*
+ * Workhorse for figuring out recursive revocation of role grants.
+ *
+ * This is similar to what recursive_revoke() does for ACLs.
+ */
+static void
+plan_recursive_revoke(CatCList *memlist, RevokeRoleGrantAction *actions,
+ int index,
+ bool revoke_admin_option_only, DropBehavior behavior)
+{
+ bool would_still_have_admin_option = false;
+ HeapTuple authmem_tuple;
+ Form_pg_auth_members authmem_form;
+ int i;
+
+ /* If it's already been done, we can just return. */
+ if (actions[index] == RRG_DELETE_GRANT)
+ return;
+ if (actions[index] == RRG_REMOVE_ADMIN_OPTION &&
+ revoke_admin_option_only)
+ return;
+
+ /* Locate tuple data. */
+ authmem_tuple = &memlist->members[index]->tuple;
+ authmem_form = (Form_pg_auth_members) GETSTRUCT(authmem_tuple);
+
+ /*
+ * If the existing tuple does not have admin_option set, then we do not
+ * need to recurse. If we're just supposed to clear that bit we don't need
+ * to do anything at all; if we're supposed to remove the grant, we need
+ * to do something, but only to the tuple, and not any others.
+ */
+ if (!revoke_admin_option_only)
+ {
+ actions[index] = RRG_DELETE_GRANT;
+ if (!authmem_form->admin_option)
+ return;
+ }
+ else
+ {
+ if (!authmem_form->admin_option)
+ return;
+ actions[index] = RRG_REMOVE_ADMIN_OPTION;
+ }
+
+ /* Determine whether the member would still have ADMIN OPTION. */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple am_cascade_tuple;
+ Form_pg_auth_members am_cascade_form;
+
+ am_cascade_tuple = &memlist->members[i]->tuple;
+ am_cascade_form = (Form_pg_auth_members) GETSTRUCT(am_cascade_tuple);
+
+ if (am_cascade_form->member == authmem_form->member &&
+ am_cascade_form->admin_option && actions[i] == RRG_NOOP)
+ {
+ would_still_have_admin_option = true;
+ break;
+ }
+ }
+
+ /* If the member would still have ADMIN OPTION, we need not recurse. */
+ if (would_still_have_admin_option)
+ return;
+
+ /*
+ * Recurse to grants that are not yet slated for deletion which have this
+ * member as the grantor.
+ */
+ for (i = 0; i < memlist->n_members; ++i)
+ {
+ HeapTuple am_cascade_tuple;
+ Form_pg_auth_members am_cascade_form;
+
+ am_cascade_tuple = &memlist->members[i]->tuple;
+ am_cascade_form = (Form_pg_auth_members) GETSTRUCT(am_cascade_tuple);
+
+ if (am_cascade_form->grantor == authmem_form->member &&
+ actions[i] != RRG_DELETE_GRANT)
+ {
+ if (behavior == DROP_RESTRICT)
+ ereport(ERROR,
+ (errcode(ERRCODE_DEPENDENT_OBJECTS_STILL_EXIST),
+ errmsg("dependent privileges exist"),
+ errhint("Use CASCADE to revoke them too.")));
+
+ plan_recursive_revoke(memlist, actions, i, false, behavior);
+ }
+ }
+}
diff --git a/src/backend/parser/gram.y b/src/backend/parser/gram.y
index f9037761f9..c8bd66dd54 100644
--- a/src/backend/parser/gram.y
+++ b/src/backend/parser/gram.y
@@ -7870,6 +7870,7 @@ RevokeRoleStmt:
n->admin_opt = false;
n->granted_roles = $2;
n->grantee_roles = $4;
+ n->grantor = $5;
n->behavior = $6;
$$ = (Node *) n;
}
@@ -7881,6 +7882,7 @@ RevokeRoleStmt:
n->admin_opt = true;
n->granted_roles = $5;
n->grantee_roles = $7;
+ n->grantor = $8;
n->behavior = $9;
$$ = (Node *) n;
}
diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c
index 6fa58dd8eb..3e045da31f 100644
--- a/src/backend/utils/adt/acl.c
+++ b/src/backend/utils/adt/acl.c
@@ -4791,9 +4791,7 @@ has_rolinherit(Oid roleid)
* Get a list of roles that the specified roleid is a member of
*
* Type ROLERECURSE_PRIVS recurses only through roles that have rolinherit
- * set, while ROLERECURSE_MEMBERS recurses through all roles. This sets
- * *is_admin==true if and only if role "roleid" has an ADMIN OPTION membership
- * in role "admin_of".
+ * set, while ROLERECURSE_MEMBERS recurses through all roles.
*
* Since indirect membership testing is relatively expensive, we cache
* a list of memberships. Hence, the result is only guaranteed good until
@@ -4801,10 +4799,15 @@ has_rolinherit(Oid roleid)
*
* For the benefit of select_best_grantor, the result is defined to be
* in breadth-first order, ie, closer relationships earlier.
+ *
+ * If admin_of is not InvalidOid, this function sets *admin_role, either
+ * to the OID of the first role in the result list that directly possesses
+ * ADMIN OPTION on the role corresponding to admin_of, or to InvalidOid if
+ * there is no such role.
*/
static List *
roles_is_member_of(Oid roleid, enum RoleRecurseType type,
- Oid admin_of, bool *is_admin)
+ Oid admin_of, Oid *admin_role)
{
Oid dba;
List *roles_list;
@@ -4812,7 +4815,9 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
List *new_cached_roles;
MemoryContext oldctx;
- Assert(OidIsValid(admin_of) == PointerIsValid(is_admin));
+ Assert(OidIsValid(admin_of) == PointerIsValid(admin_role));
+ if (admin_role != NULL)
+ *admin_role = InvalidOid;
/* If cache is valid and ADMIN OPTION not sought, just return the list */
if (cached_role[type] == roleid && !OidIsValid(admin_of) &&
@@ -4873,8 +4878,8 @@ roles_is_member_of(Oid roleid, enum RoleRecurseType type,
*/
if (otherid == admin_of &&
((Form_pg_auth_members) GETSTRUCT(tup))->admin_option &&
- OidIsValid(admin_of))
- *is_admin = true;
+ OidIsValid(admin_of) && !OidIsValid(*admin_role))
+ *admin_role = memberid;
/*
* Even though there shouldn't be any loops in the membership
@@ -5014,7 +5019,7 @@ is_member_of_role_nosuper(Oid member, Oid role)
bool
is_admin_of_role(Oid member, Oid role)
{
- bool result = false;
+ Oid admin_role;
if (superuser_arg(member))
return true;
@@ -5023,8 +5028,30 @@ is_admin_of_role(Oid member, Oid role)
if (member == role)
return false;
- (void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &result);
- return result;
+ (void) roles_is_member_of(member, ROLERECURSE_MEMBERS, role, &admin_role);
+ return OidIsValid(admin_role);
+}
+
+/*
+ * Find a role whose privileges "member" inherits which has ADMIN OPTION
+ * on "role", ignoring super-userness.
+ *
+ * There might be more than one such role; prefer one which involves fewer
+ * hops. That is, if member has ADMIN OPTION, prefer that over all other
+ * options; if not, prefer a role from which member inherits more directly
+ * over more indirect inheritance.
+ */
+Oid
+select_best_admin(Oid member, Oid role)
+{
+ Oid admin_role;
+
+ /* By policy, a role cannot have WITH ADMIN OPTION on itself. */
+ if (member == role)
+ return InvalidOid;
+
+ (void) roles_is_member_of(member, ROLERECURSE_PRIVS, role, &admin_role);
+ return admin_role;
}
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 1912b12146..eec644ec84 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -213,22 +213,22 @@ static const struct cachedesc cacheinfo[] = {
},
{AuthMemRelationId, /* AUTHMEMMEMROLE */
AuthMemMemRoleIndexId,
- 2,
+ 3,
{
Anum_pg_auth_members_member,
Anum_pg_auth_members_roleid,
- 0,
+ Anum_pg_auth_members_grantor,
0
},
8
},
{AuthMemRelationId, /* AUTHMEMROLEMEM */
AuthMemRoleMemIndexId,
- 2,
+ 3,
{
Anum_pg_auth_members_roleid,
Anum_pg_auth_members_member,
- 0,
+ Anum_pg_auth_members_grantor,
0
},
8
diff --git a/src/bin/pg_dump/pg_dumpall.c b/src/bin/pg_dump/pg_dumpall.c
index 26d3d53809..e8a2bfa6bd 100644
--- a/src/bin/pg_dump/pg_dumpall.c
+++ b/src/bin/pg_dump/pg_dumpall.c
@@ -21,6 +21,7 @@
#include "catalog/pg_authid_d.h"
#include "common/connect.h"
#include "common/file_utils.h"
+#include "common/hashfn.h"
#include "common/logging.h"
#include "common/string.h"
#include "dumputils.h"
@@ -31,6 +32,28 @@
/* version string we expect back from pg_dump */
#define PGDUMP_VERSIONSTR "pg_dump (PostgreSQL) " PG_VERSION "\n"
+static uint32 hash_string_pointer(char *s);
+
+typedef struct
+{
+ uint32 status;
+ uint32 hashval;
+ char *rolename;
+} RoleNameEntry;
+
+#define SH_PREFIX rolename
+#define SH_ELEMENT_TYPE RoleNameEntry
+#define SH_KEY_TYPE char *
+#define SH_KEY rolename
+#define SH_HASH_KEY(tb, key) hash_string_pointer(key)
+#define SH_EQUAL(tb, a, b) (strcmp(a, b) == 0)
+#define SH_STORE_HASH
+#define SH_GET_HASH(tb, a) (a)->hashval
+#define SH_SCOPE static inline
+#define SH_RAW_ALLOCATOR pg_malloc0
+#define SH_DECLARE
+#define SH_DEFINE
+#include "lib/simplehash.h"
static void help(void);
@@ -925,45 +948,150 @@ dumpRoleMembership(PGconn *conn)
{
PQExpBuffer buf = createPQExpBuffer();
PGresult *res;
- int i;
+ int start = 0,
+ end,
+ total;
+ bool dump_grantors;
- printfPQExpBuffer(buf, "SELECT ur.rolname AS roleid, "
+ /*
+ * Previous versions of PostgreSQL didn't used to track the grantor very
+ * carefully in the backend, and the grantor could be any user even if
+ * they didn't have ADMIN OPTION on the role, or a user that no longer
+ * existed. To avoid dump and restore failures, don't dump the grantor
+ * when talking to an old server version.
+ */
+ dump_grantors = (PQserverVersion(conn) >= 160000);
+
+ /* Generate and execute query. */
+ printfPQExpBuffer(buf, "SELECT ur.rolname AS role, "
"um.rolname AS member, "
- "a.admin_option, "
- "ug.rolname AS grantor "
+ "ug.oid AS grantorid, "
+ "ug.rolname AS grantor, "
+ "a.admin_option "
"FROM pg_auth_members a "
"LEFT JOIN %s ur on ur.oid = a.roleid "
"LEFT JOIN %s um on um.oid = a.member "
"LEFT JOIN %s ug on ug.oid = a.grantor "
"WHERE NOT (ur.rolname ~ '^pg_' AND um.rolname ~ '^pg_')"
- "ORDER BY 1,2,3", role_catalog, role_catalog, role_catalog);
+ "ORDER BY 1,2,4", role_catalog, role_catalog, role_catalog);
res = executeQuery(conn, buf->data);
if (PQntuples(res) > 0)
fprintf(OPF, "--\n-- Role memberships\n--\n\n");
- for (i = 0; i < PQntuples(res); i++)
+ /*
+ * We can't dump these GRANT commands in arbitary order, because a role
+ * that is named as a grantor must already have ADMIN OPTION on the
+ * role for which it is granting permissions, except for the boostrap
+ * superuser, who can always be named as the grantor.
+ *
+ * We handle this by considering these grants role by role. For each role,
+ * we initially consider the only allowable grantor to be the boostrap
+ * superuser. Every time we grant ADMIN OPTION on the role to some user,
+ * that user also becomes an allowable grantor. We make repeated passes
+ * over the grants for the role, each time dumping those whose grantors
+ * are allowable and which we haven't done yet. Eventually this should
+ * let us dump all the grants.
+ */
+ total = PQntuples(res);
+ while (start < total)
{
- char *roleid = PQgetvalue(res, i, 0);
- char *member = PQgetvalue(res, i, 1);
- char *option = PQgetvalue(res, i, 2);
+ char *role = PQgetvalue(res, start, 0);
+ int i;
+ bool *done;
+ int remaining;
+ int prev_remaining = 0;
+ rolename_hash *ht;
+
+ /* All memberships for a single role should be adjacent. */
+ for (end = start; end < total; ++end)
+ {
+ char *otherrole;
+
+ otherrole = PQgetvalue(res, end, 0);
+ if (strcmp(role, otherrole) != 0)
+ break;
+ }
- fprintf(OPF, "GRANT %s", fmtId(roleid));
- fprintf(OPF, " TO %s", fmtId(member));
- if (*option == 't')
- fprintf(OPF, " WITH ADMIN OPTION");
+ role = PQgetvalue(res, start, 0);
+ remaining = end - start;
+ done = pg_malloc0(remaining * sizeof(bool));
+ ht = rolename_create(remaining, NULL);
/*
- * We don't track the grantor very carefully in the backend, so cope
- * with the possibility that it has been dropped.
+ * Make repeated passses over the grants for this role until all have
+ * been dumped.
*/
- if (!PQgetisnull(res, i, 3))
+ while (remaining > 0)
{
- char *grantor = PQgetvalue(res, i, 3);
+ /*
+ * We should make progress on every iteration, because a notional
+ * graph whose vertices are grants and whose edges point from
+ * grantors to members should be connected and acyclic. If we fail
+ * to make progress, either we or the server have messed up.
+ */
+ if (remaining == prev_remaining)
+ {
+ pg_log_error("could not find a legal dump ordering for memberships in role \"%s\"",
+ role);
+ PQfinish(conn);
+ exit_nicely(1);
+ }
- fprintf(OPF, " GRANTED BY %s", fmtId(grantor));
+ /* Make one pass over the grants for this role. */
+ for (i = start; i < end; ++i)
+ {
+ char *member;
+ char *admin_option;
+ char *grantorid;
+ char *grantor;
+ bool found;
+
+ /* If we already did this grant, don't do it again. */
+ if (done[i - start])
+ continue;
+
+ member = PQgetvalue(res, i, 1);
+ grantorid = PQgetvalue(res, i, 2);
+ grantor = PQgetvalue(res, i, 3);
+ admin_option = PQgetvalue(res, i, 4);
+
+ /*
+ * If we're not dumping grantors or if the grantor is the
+ * bootstrap superuser, it's fine to dump this now. Otherwise,
+ * it's got to be someone who has already been granted ADMIN
+ * OPTION.
+ */
+ if (dump_grantors &&
+ atooid(grantorid) != BOOTSTRAP_SUPERUSERID &&
+ rolename_lookup(ht, grantor) == NULL)
+ continue;
+
+ /* Remember that we did this so that we don't do it again. */
+ done[i - start] = true;
+ --remaining;
+
+ /*
+ * If ADMIN OPTION is being granted, remember that grants
+ * listing this member as the grantor can now be dumped.
+ */
+ if (*admin_option == 't')
+ rolename_insert(ht, member, &found);
+
+ /* Generate the actual GRANT statement. */
+ fprintf(OPF, "GRANT %s", fmtId(role));
+ fprintf(OPF, " TO %s", fmtId(member));
+ if (*admin_option == 't')
+ fprintf(OPF, " WITH ADMIN OPTION");
+ if (dump_grantors)
+ fprintf(OPF, " GRANTED BY %s", fmtId(grantor));
+ fprintf(OPF, ";\n");
+ }
}
- fprintf(OPF, ";\n");
+
+ rolename_destroy(ht);
+ pg_free(done);
+ start = end;
}
PQclear(res);
@@ -1748,3 +1876,14 @@ dumpTimestamp(const char *msg)
if (strftime(buf, sizeof(buf), PGDUMP_STRFTIME_FMT, localtime(&now)) != 0)
fprintf(OPF, "-- %s %s\n\n", msg, buf);
}
+
+/*
+ * Helper function for rolenamehash hash table.
+ */
+static uint32
+hash_string_pointer(char *s)
+{
+ unsigned char *ss = (unsigned char *) s;
+
+ return hash_bytes(ss, strlen(s));
+}
diff --git a/src/include/catalog/pg_auth_members.h b/src/include/catalog/pg_auth_members.h
index c9d7697730..e57ec4f810 100644
--- a/src/include/catalog/pg_auth_members.h
+++ b/src/include/catalog/pg_auth_members.h
@@ -44,8 +44,8 @@ CATALOG(pg_auth_members,1261,AuthMemRelationId) BKI_SHARED_RELATION BKI_ROWTYPE_
typedef FormData_pg_auth_members *Form_pg_auth_members;
DECLARE_UNIQUE_INDEX_PKEY(pg_auth_members_oid_index, 9385, AuthMemOidIndexId, on pg_auth_members using btree(oid oid_ops));
-DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops));
-DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_role_member_index, 2694, AuthMemRoleMemIndexId, on pg_auth_members using btree(roleid oid_ops, member oid_ops, grantor oid_ops));
+DECLARE_UNIQUE_INDEX(pg_auth_members_member_role_index, 2695, AuthMemMemRoleIndexId, on pg_auth_members using btree(member oid_ops, roleid oid_ops, grantor oid_ops));
DECLARE_INDEX(pg_auth_members_grantor_index, 9384, AuthMemGrantorIndexId, on pg_auth_members using btree(grantor oid_ops));
#endif /* PG_AUTH_MEMBERS_H */
diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h
index 48f7d72add..3d6411197c 100644
--- a/src/include/utils/acl.h
+++ b/src/include/utils/acl.h
@@ -212,6 +212,7 @@ extern bool has_privs_of_role(Oid member, Oid role);
extern bool is_member_of_role(Oid member, Oid role);
extern bool is_member_of_role_nosuper(Oid member, Oid role);
extern bool is_admin_of_role(Oid member, Oid role);
+extern Oid select_best_admin(Oid member, Oid role);
extern void check_is_member_of_role(Oid member, Oid role);
extern Oid get_role_oid(const char *rolename, bool missing_ok);
extern Oid get_role_oid_or_public(const char *rolename);
diff --git a/src/test/regress/expected/create_role.out b/src/test/regress/expected/create_role.out
index c2465d0f49..4e67d72760 100644
--- a/src/test/regress/expected/create_role.out
+++ b/src/test/regress/expected/create_role.out
@@ -103,21 +103,9 @@ ERROR: role "regress_nosuch_recursive" does not exist
DROP ROLE regress_nosuch_admin_recursive;
ERROR: role "regress_nosuch_admin_recursive" does not exist
DROP ROLE regress_plainrole;
--- fail, can't drop regress_createrole yet, due to outstanding grants
-DROP ROLE regress_createrole;
-ERROR: role "regress_createrole" cannot be dropped because some objects depend on it
-DETAIL: privileges for membership of role regress_read_all_data in role pg_read_all_data
-privileges for membership of role regress_write_all_data in role pg_write_all_data
-privileges for membership of role regress_monitor in role pg_monitor
-privileges for membership of role regress_read_all_settings in role pg_read_all_settings
-privileges for membership of role regress_read_all_stats in role pg_read_all_stats
-privileges for membership of role regress_stat_scan_tables in role pg_stat_scan_tables
-privileges for membership of role regress_read_server_files in role pg_read_server_files
-privileges for membership of role regress_write_server_files in role pg_write_server_files
-privileges for membership of role regress_execute_server_program in role pg_execute_server_program
-privileges for membership of role regress_signal_backend in role pg_signal_backend
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
+DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -137,8 +125,6 @@ DROP ROLE regress_read_server_files;
DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
--- ok, dropped the other roles first so this is ok now
-DROP ROLE regress_createrole;
-- fail, role still owns database objects
DROP ROLE regress_tenant;
ERROR: role "regress_tenant" cannot be dropped because some objects depend on it
diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out
index 65b4a22ebc..0154a09262 100644
--- a/src/test/regress/expected/privileges.out
+++ b/src/test/regress/expected/privileges.out
@@ -33,6 +33,54 @@ CREATE USER regress_priv_user8;
CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- circular ADMIN OPTION grants should be disallowed
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION GRANTED BY regress_priv_user2;
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION GRANTED BY regress_priv_user3;
+ERROR: admin option cannot be granted back to your own grantor
+-- need CASCADE to revoke grant or admin option if dependent grants exist
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2; -- fail
+ERROR: dependent privileges exist
+HINT: Use CASCADE to revoke them too.
+REVOKE regress_priv_user1 FROM regress_priv_user2; -- fail
+ERROR: dependent privileges exist
+HINT: Use CASCADE to revoke them too.
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ member | admin_option
+--------------------+--------------
+ regress_priv_user2 | t
+ regress_priv_user3 | t
+(2 rows)
+
+BEGIN;
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ member | admin_option
+--------------------+--------------
+ regress_priv_user2 | f
+(1 row)
+
+ROLLBACK;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ member | admin_option
+--------+--------------
+(0 rows)
+
+-- inferred grantor must be a role with ADMIN OPTION
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user2 TO regress_priv_user3;
+SET ROLE regress_priv_user3;
+GRANT regress_priv_user1 TO regress_priv_user4;
+SELECT grantor::regrole FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole and member = 'regress_priv_user4'::regrole;
+ grantor
+--------------------
+ regress_priv_user2
+(1 row)
+
+RESET ROLE;
+REVOKE regress_priv_user2 FROM regress_priv_user3;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
@@ -68,15 +116,17 @@ CREATE USER regress_priv_user5;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
+GRANT regress_priv_user9 TO regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
GRANT pg_read_all_settings TO regress_priv_user9 WITH ADMIN OPTION;
SET SESSION AUTHORIZATION regress_priv_user9;
GRANT pg_read_all_settings TO regress_priv_user10;
SET SESSION AUTHORIZATION regress_priv_user8;
-REVOKE pg_read_all_settings FROM regress_priv_user10;
+REVOKE pg_read_all_settings FROM regress_priv_user10 GRANTED BY regress_priv_user9;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user9;
REVOKE pg_read_all_settings FROM regress_priv_user9;
RESET SESSION AUTHORIZATION;
+REVOKE regress_priv_user9 FROM regress_priv_user8;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
SET ROLE pg_read_all_settings;
@@ -87,11 +137,20 @@ DROP USER regress_priv_user10;
DROP USER regress_priv_user9;
DROP USER regress_priv_user8;
CREATE GROUP regress_priv_group1;
-CREATE GROUP regress_priv_group2 WITH USER regress_priv_user1, regress_priv_user2;
+CREATE GROUP regress_priv_group2 WITH ADMIN regress_priv_user1 USER regress_priv_user2;
ALTER GROUP regress_priv_group1 ADD USER regress_priv_user4;
+GRANT regress_priv_group2 TO regress_priv_user2 GRANTED BY regress_priv_user1;
+SET SESSION AUTHORIZATION regress_priv_user1;
+ALTER GROUP regress_priv_group2 ADD USER regress_priv_user2;
+NOTICE: role "regress_priv_user2" has already been granted membership in role "regress_priv_group2" by role "regress_priv_user1"
ALTER GROUP regress_priv_group2 ADD USER regress_priv_user2; -- duplicate
-NOTICE: role "regress_priv_user2" is already a member of role "regress_priv_group2"
+NOTICE: role "regress_priv_user2" has already been granted membership in role "regress_priv_group2" by role "regress_priv_user1"
+ALTER GROUP regress_priv_group2 DROP USER regress_priv_user2;
+ALTER USER regress_priv_user2 PASSWORD 'verysecret'; -- not permitted
+ERROR: must have CREATEROLE privilege to change another user's password
+RESET SESSION AUTHORIZATION;
ALTER GROUP regress_priv_group2 DROP USER regress_priv_user2;
+REVOKE ADMIN OPTION FOR regress_priv_group2 FROM regress_priv_user1;
GRANT regress_priv_group2 TO regress_priv_user4 WITH ADMIN OPTION;
-- prepare non-leakproof function for later
CREATE FUNCTION leak(integer,integer) RETURNS boolean
@@ -99,9 +158,13 @@ CREATE FUNCTION leak(integer,integer) RETURNS boolean
LANGUAGE internal IMMUTABLE STRICT; -- but deliberately not LEAKPROOF
ALTER FUNCTION leak(integer,integer) OWNER TO regress_priv_user1;
-- test owner privileges
+GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY regress_priv_role; -- error, doesn't have ADMIN OPTION
+ERROR: grantor must have ADMIN OPTION on "regress_priv_role"
GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY CURRENT_ROLE;
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY foo; -- error
-REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- error
+ERROR: role "foo" does not exist
+REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- warning, noop
+WARNING: role "regress_priv_user1" has not been granted membership in role "regress_priv_role" by role "regress_priv_user2"
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_USER;
REVOKE regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_ROLE;
DROP ROLE regress_priv_role;
@@ -1746,7 +1809,7 @@ SET SESSION AUTHORIZATION regress_priv_user1;
GRANT regress_priv_group2 TO regress_priv_user5; -- fails: no ADMIN OPTION
ERROR: must have admin option on role "regress_priv_group2"
SELECT dogrant_ok(); -- ok: SECURITY DEFINER conveys ADMIN
-NOTICE: role "regress_priv_user5" is already a member of role "regress_priv_group2"
+NOTICE: role "regress_priv_user5" has already been granted membership in role "regress_priv_group2" by role "regress_priv_user4"
dogrant_ok
------------
diff --git a/src/test/regress/sql/create_role.sql b/src/test/regress/sql/create_role.sql
index b696628238..292dc08797 100644
--- a/src/test/regress/sql/create_role.sql
+++ b/src/test/regress/sql/create_role.sql
@@ -98,11 +98,9 @@ DROP ROLE regress_nosuch_recursive;
DROP ROLE regress_nosuch_admin_recursive;
DROP ROLE regress_plainrole;
--- fail, can't drop regress_createrole yet, due to outstanding grants
-DROP ROLE regress_createrole;
-
-- ok, should be able to drop non-superuser roles we created
DROP ROLE regress_createdb;
+DROP ROLE regress_createrole;
DROP ROLE regress_login;
DROP ROLE regress_inherit;
DROP ROLE regress_connection_limit;
@@ -123,9 +121,6 @@ DROP ROLE regress_write_server_files;
DROP ROLE regress_execute_server_program;
DROP ROLE regress_signal_backend;
--- ok, dropped the other roles first so this is ok now
-DROP ROLE regress_createrole;
-
-- fail, role still owns database objects
DROP ROLE regress_tenant;
diff --git a/src/test/regress/sql/privileges.sql b/src/test/regress/sql/privileges.sql
index 66834e32a7..b4ef20f738 100644
--- a/src/test/regress/sql/privileges.sql
+++ b/src/test/regress/sql/privileges.sql
@@ -37,6 +37,32 @@ CREATE USER regress_priv_user9;
CREATE USER regress_priv_user10;
CREATE ROLE regress_priv_role;
+-- circular ADMIN OPTION grants should be disallowed
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user1 TO regress_priv_user3 WITH ADMIN OPTION GRANTED BY regress_priv_user2;
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION GRANTED BY regress_priv_user3;
+
+-- need CASCADE to revoke grant or admin option if dependent grants exist
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2; -- fail
+REVOKE regress_priv_user1 FROM regress_priv_user2; -- fail
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+BEGIN;
+REVOKE ADMIN OPTION FOR regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+ROLLBACK;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
+SELECT member::regrole, admin_option FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole;
+
+-- inferred grantor must be a role with ADMIN OPTION
+GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
+GRANT regress_priv_user2 TO regress_priv_user3;
+SET ROLE regress_priv_user3;
+GRANT regress_priv_user1 TO regress_priv_user4;
+SELECT grantor::regrole FROM pg_auth_members WHERE roleid = 'regress_priv_user1'::regrole and member = 'regress_priv_user4'::regrole;
+RESET ROLE;
+REVOKE regress_priv_user2 FROM regress_priv_user3;
+REVOKE regress_priv_user1 FROM regress_priv_user2 CASCADE;
+
-- test GRANTED BY with DROP OWNED and REASSIGN OWNED
GRANT regress_priv_user1 TO regress_priv_user2 WITH ADMIN OPTION;
GRANT regress_priv_user1 TO regress_priv_user3 GRANTED BY regress_priv_user2;
@@ -67,6 +93,7 @@ CREATE USER regress_priv_user5;
GRANT pg_read_all_data TO regress_priv_user6;
GRANT pg_write_all_data TO regress_priv_user7;
GRANT pg_read_all_settings TO regress_priv_user8 WITH ADMIN OPTION;
+GRANT regress_priv_user9 TO regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
GRANT pg_read_all_settings TO regress_priv_user9 WITH ADMIN OPTION;
@@ -75,11 +102,12 @@ SET SESSION AUTHORIZATION regress_priv_user9;
GRANT pg_read_all_settings TO regress_priv_user10;
SET SESSION AUTHORIZATION regress_priv_user8;
-REVOKE pg_read_all_settings FROM regress_priv_user10;
+REVOKE pg_read_all_settings FROM regress_priv_user10 GRANTED BY regress_priv_user9;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user9;
REVOKE pg_read_all_settings FROM regress_priv_user9;
RESET SESSION AUTHORIZATION;
+REVOKE regress_priv_user9 FROM regress_priv_user8;
REVOKE ADMIN OPTION FOR pg_read_all_settings FROM regress_priv_user8;
SET SESSION AUTHORIZATION regress_priv_user8;
@@ -94,12 +122,19 @@ DROP USER regress_priv_user9;
DROP USER regress_priv_user8;
CREATE GROUP regress_priv_group1;
-CREATE GROUP regress_priv_group2 WITH USER regress_priv_user1, regress_priv_user2;
+CREATE GROUP regress_priv_group2 WITH ADMIN regress_priv_user1 USER regress_priv_user2;
ALTER GROUP regress_priv_group1 ADD USER regress_priv_user4;
+GRANT regress_priv_group2 TO regress_priv_user2 GRANTED BY regress_priv_user1;
+SET SESSION AUTHORIZATION regress_priv_user1;
+ALTER GROUP regress_priv_group2 ADD USER regress_priv_user2;
ALTER GROUP regress_priv_group2 ADD USER regress_priv_user2; -- duplicate
ALTER GROUP regress_priv_group2 DROP USER regress_priv_user2;
+ALTER USER regress_priv_user2 PASSWORD 'verysecret'; -- not permitted
+RESET SESSION AUTHORIZATION;
+ALTER GROUP regress_priv_group2 DROP USER regress_priv_user2;
+REVOKE ADMIN OPTION FOR regress_priv_group2 FROM regress_priv_user1;
GRANT regress_priv_group2 TO regress_priv_user4 WITH ADMIN OPTION;
-- prepare non-leakproof function for later
@@ -110,9 +145,10 @@ ALTER FUNCTION leak(integer,integer) OWNER TO regress_priv_user1;
-- test owner privileges
+GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY regress_priv_role; -- error, doesn't have ADMIN OPTION
GRANT regress_priv_role TO regress_priv_user1 WITH ADMIN OPTION GRANTED BY CURRENT_ROLE;
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY foo; -- error
-REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- error
+REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY regress_priv_user2; -- warning, noop
REVOKE ADMIN OPTION FOR regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_USER;
REVOKE regress_priv_role FROM regress_priv_user1 GRANTED BY CURRENT_ROLE;
DROP ROLE regress_priv_role;
--
2.24.3 (Apple Git-128)
On Wed, Aug 10, 2022 at 4:28 PM Robert Haas <robertmhaas@gmail.com> wrote:
Well, CI isn't happy with this, and for good reason:
CI is happier with this version, so I've committed 0001. If no major
problems emerge, I'll proceed with 0002 as well.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Thu, Aug 18, 2022 at 1:26 PM Robert Haas <robertmhaas@gmail.com> wrote:
On Wed, Aug 10, 2022 at 4:28 PM Robert Haas <robertmhaas@gmail.com> wrote:
Well, CI isn't happy with this, and for good reason:
CI is happier with this version, so I've committed 0001. If no major
problems emerge, I'll proceed with 0002 as well.
Done.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Mon, 2022-08-22 at 11:47 -0400, Robert Haas wrote:
On Thu, Aug 18, 2022 at 1:26 PM Robert Haas <robertmhaas@gmail.com>
wrote:On Wed, Aug 10, 2022 at 4:28 PM Robert Haas <robertmhaas@gmail.com>
wrote:Well, CI isn't happy with this, and for good reason:
CI is happier with this version, so I've committed 0001. If no
major
problems emerge, I'll proceed with 0002 as well.Done.
It's still on the CF, so I took a look.
There's still some weirdness around superusers:
1. "GRANTED BY current_user" differs from not specifying "GRANTED BY"
at all.
a. With GRANTED BY current_user, weird because current_user is a
superuser:
CREATE USER su1 SUPERUSER;
CREATE ROLE u1;
CREATE ROLE u2;
\c - su1
GRANT u2 TO u1 GRANTED BY current_user;
ERROR: grantor must have ADMIN OPTION on "u2"
b. Without GRANTED BY:
CREATE USER su1 SUPERUSER;
CREATE ROLE u1;
CREATE ROLE u2;
\c - su1
GRANT u2 TO u1;
-- grantor is bootstrap superuser
2. Grantor can depend on the path to get there:
a. Already superuser:
CREATE USER su1 SUPERUSER;
CREATE ROLE u1;
CREATE ROLE u2;
GRANT u2 TO su1 WITH ADMIN OPTION;
\c - su1
GRANT u2 TO u1;
-- grantor is bootstrap superuser
b. Becomes superuser after GRANT:
CREATE USER su1;
CREATE ROLE u1;
CREATE ROLE u2;
GRANT u2 TO su1 WITH ADMIN OPTION;
\c - su1
GRANT u2 TO u1;
\c - bootstrap_superuser
ALTER ROLE su1 SUPERUSER;
-- grantor is su1
3. Another case where "GRANTED BY current_user" differs from no
"GRANTED BY" at all, with slightly different consequences:
a. GRANTED BY current_user, throws error:
CREATE USER su1 SUPERUSER;
CREATE ROLE u1;
CREATE ROLE u2;
GRANT u2 TO su1 WITH ADMIN OPTION;
\c - su1
GRANT u2 TO u1 GRANTED BY current_user;
-- grantor is su1
\c - bootstrap_superuser
REVOKE ADMIN OPTION FOR u2 FROM su1;
ERROR: dependent privileges exist
b. No GRANTED BY, no error:
CREATE USER su1 SUPERUSER;
CREATE ROLE u1;
CREATE ROLE u2;
GRANT u2 TO su1 WITH ADMIN OPTION;
\c - su1
GRANT u2 TO u1;
-- grantor is bootstrap superuser
\c - boostrap_superuser
REVOKE ADMIN OPTION FOR u2 FROM su1;
We seem to be trying very hard to satisfy two things that seem
impossible to satisfy:
i. "ALTER ROLE ... NOSUPERUSER" must always succeed, and probably
execute quickly, too.
ii. We want to maintain catalog invariants that are based, in part,
on roles having superuser privileges or not.
The hacks we are using to try to make this work are just that: hacks.
And it's all to satisfy a fairly rare case: removing superuser
privileges and expecting the catalogs to be consistent.
I think we'd be better off without these hacks. I'm not sure exactly
how, but the benefit doesn't seem to be worth the cost. Some
alternative ideas:
* Have a "safe" version of removing superuser that can error or
cascade, and an "unsafe" version that always succeeds but might leave
inconsistent catalogs.
* Ignore the problems with removing superuser, but issue a WARNING
* Superusers would auto-grant themselves the privileges that a normal
user would need to do something before doing it. For instance, if a
superuser did "GRANT u2 TO u1", it would first automatically issue a
"GRANT u2 TO current_user WITH ADMIN OPTION GRANTED BY
bootstrap_superuser", then do the grant normally. If the superuser
privileges are removed, then the catalogs would still be consistent.
This is a new idea and I didn't think it through very carefully, but
might be an interesting approach.
Also, it would be nice to have REASSIGN OWNED work with grants, perhaps
by adding a "WITH[OUT] GRANT" or something.
Regards,
Jeff Davis
Thanks for having a look.
On Thu, Sep 1, 2022 at 4:34 PM Jeff Davis <pgsql@j-davis.com> wrote:
There's still some weirdness around superusers:
1. "GRANTED BY current_user" differs from not specifying "GRANTED BY"
at all.
Yes. I figured that, when GRANTED BY is not specified, it is OK to
infer a valid grantor, but if it is specified, it does not seem right
to infer a grantor other than the one specified. Admittedly, this case
is without precedent elsewhere in the system, because nobody has made
GRANTED BY work for other object types, outside of trivial cases.
Still, it seems like the right behavior to me.
2. Grantor can depend on the path to get there:
a. Already superuser:
CREATE USER su1 SUPERUSER;
CREATE ROLE u1;
CREATE ROLE u2;
GRANT u2 TO su1 WITH ADMIN OPTION;
\c - su1
GRANT u2 TO u1;
-- grantor is bootstrap superuserb. Becomes superuser after GRANT:
CREATE USER su1;
CREATE ROLE u1;
CREATE ROLE u2;
GRANT u2 TO su1 WITH ADMIN OPTION;
\c - su1
GRANT u2 TO u1;
\c - bootstrap_superuser
ALTER ROLE su1 SUPERUSER;
-- grantor is su1
This also seems correct to me, and here I believe you could construct
similar examples with other object types. We infer the grantor based
on the state of the system at the time the grant was performed. We
can't change our mind later even if things have changed that would
cause us to make a different inference. In the case of a table, for
example, consider:
create role p1;
create role p2;
create role a;
create table t1 (a int);
create role b;
grant select on table t1 to p1 with grant option;
grant select on table t1 to p2 with grant option;
grant p1 to a;
set session authorization a;
grant select on table t1 to b;
At this point, b has SELECT permission on table t1 and the grantor of
record is p1. But if you had done "GRANT p2 TO a" then the grantor of
record would be p2 rather than p1. And you can still "REVOKE p1 FROM
a;" and then "GRANT p2 to a;". As in your example, doing so won't
change the grantor recorded for the grant already made.
3. Another case where "GRANTED BY current_user" differs from no
"GRANTED BY" at all, with slightly different consequences:
It's extremely difficult for me to imagine what other behavior would
be sane here. In this example, the inferred best grantor is different
from the current user, so forcing the grantor to be the current user
changes the behavior. There are only two ways that anything different
can happen: either we'd have to change the algorithm for inferring the
best grantor, or we'd have to be willing to disregard the user's
explicit specification that the grantor be the current user rather
than somebody else.
As to the first, the algorithm being used to select the best grantor
here is analogous to the one we use for privileges on other object
types, such as tables, namely, we prefer to create a grant that is not
dependent on some other grant, rather than one that is. Maybe that's
the best policy and maybe it isn't, but I can't see it being
reasonable to have one policy for grants on tables, functions, etc.
and another policy for grants on roles.
As to the second, this is somewhat similar to the case you already
raised in your example #1. However, in that case, the
explicitly-specified grantor wasn't valid, so the grant failed. I
don't think it's right to allow inference in the presence of an
explicit specification, but if the consensus was that we really ought
to make that case succeed, I suppose we could. Here, however, the
explicitly-specified grantor *is a legal grantor*. I think it would be
extremely surprising if we just ignored that and selected some other
valid grantor instead.
We seem to be trying very hard to satisfy two things that seem
impossible to satisfy:i. "ALTER ROLE ... NOSUPERUSER" must always succeed, and probably
execute quickly, too.
ii. We want to maintain catalog invariants that are based, in part,
on roles having superuser privileges or not.The hacks we are using to try to make this work are just that: hacks.
And it's all to satisfy a fairly rare case: removing superuser
privileges and expecting the catalogs to be consistent.
I guess I don't really agree with that view of it. The primary purpose
of the patch was to make the handing of role grants consistent with
the handling of grants on other object types. I did extend the
existing functionality, because the GRANTED BY <whoever> clause works
for role grants and does not work for other grants. However, that also
worked for role grants before these patches, whereas it's never worked
for other object types. So I chose to restrict that functionality as
little as possible, and basically make it work, rather than removing
it completely, which would have been the most consistent with what we
do elsewhere.
When you view this in the context of how other types of grants work,
ALTER ROLE ... NOSUPERUSER isn't as much of a special case. Just as we
want ALTER ROLE ... NOSUPERUSER to succeed quickly, we also insist
that REVOKE role1 FROM role2 to succeed quickly. It isn't allowed to
fail due to the existence of dependent privileges, because there
aren't allowed to be any dependent privileges. GRANT role1 TO role2
doesn't really give role2 the privileges of role1; what it does is
allow role2 to act on behalf of role1. Similarly, ALTER ROLE ...
SUPERUSER lets the target role act on behalf of any user at all,
including the bootstrap superuser. In either case, actions are
attributed to the user on behalf of whom they were performed, not the
user who actually typed the command.
As another example, consider a superuser (the bootstrap superuser or
any other one) who executes GRANT SELECT ON some_random_table TO
some_random_user. Who will be recorded as the grantor? The answer is
that the table owner will be recorded as the grantor, because the
table owner is the one who actually has permission to perform the
operation. The superuser doesn't, except by virtue of their ability to
act on behalf of any other user in the system. In most cases, that's
just an academic distinction, because the question is only whether or
not the operation can be performed, and not who has to perform it. But
grants are different: it matters who does it, and when someone uses
superuser powers or other special privileges to perform an operation,
we have to ask on whose behalf they are acting.
I think we'd be better off without these hacks. I'm not sure exactly
how, but the benefit doesn't seem to be worth the cost. Some
alternative ideas:* Have a "safe" version of removing superuser that can error or
cascade, and an "unsafe" version that always succeeds but might leave
inconsistent catalogs.
* Ignore the problems with removing superuser, but issue a WARNING
I don't like either of these. I think the fact that we have strong
integrity constraints around who can be recorded as the grantor of a
privilege is a good thing, and, again, the purpose of this patch was
to bring role grants up to the level of other parts of the system.
* Superusers would auto-grant themselves the privileges that a normal
user would need to do something before doing it. For instance, if a
superuser did "GRANT u2 TO u1", it would first automatically issue a
"GRANT u2 TO current_user WITH ADMIN OPTION GRANTED BY
bootstrap_superuser", then do the grant normally. If the superuser
privileges are removed, then the catalogs would still be consistent.
This is a new idea and I didn't think it through very carefully, but
might be an interesting approach.
If we did this, we ought to do it for all object types, so that if a
superuser grants privileges on a table they don't own, they implicitly
grant themselves those privileges with grant option and then grant
them to the requested recipient. I doubt that behavior change would be
popular, and I bet somebody would complain about the SQL standard or
something, but it seems more theoretically sound than the previous two
ideas, because it doesn't just throw the idea of integrity constraints
out the window.
Also, it would be nice to have REASSIGN OWNED work with grants, perhaps
by adding a "WITH[OUT] GRANT" or something.
I thought about this, too. It's a bit tricky. Right now, DROP OWNED
drops grants, but REASSIGN OWNED doesn't change their owner. On first
glance, this seems inconsistent: either grants are a kind of object
and DROP OWNED and REASSIGN OWNED ought to apply to them like anything
else, or they are not a type of object and neither command should
touch them. However, there's a pretty significant difference between
(1) a table and (2) a grant of privileges on a table. Ownership on the
table itself can be freely changed to any role in the system at any
time. We rewrite the table's ACL on the fly to preserve the invariants
about who can be listed as the grantor. But for the grant of
privileges on the table, we can't freely change the grantor of record
to an arbitrary user at any time: the set of valid grantors is
constrained.
What might be useful is a command that says "OK, for every existing
grant that is attributed to user A, change the recorded grantor to
user B, if that's allowable, for the others, do nothing". Or maybe
there's some possible idea where we try to somehow make B into a valid
grantor, but it's not clear to me what the algorithm would be.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Fri, 2022-09-02 at 09:30 -0400, Robert Haas wrote:
Thanks for having a look.
Thanks for doing the work.
Yes. I figured that, when GRANTED BY is not specified, it is OK to
infer a valid grantor
The spec is clear that the grantor should be either the current user or
the current role. We also have a concept of INHERIT, which allows us to
choose a role we're a member of if the current one does not suffice.
But to choose a different role (the bootstrap superuser) even when the
current (super) user role *does* suffice seems like an outright
violation of both the spec and the principle of least surprise.
set session authorization a;
grant select on table t1 to b;At this point, b has SELECT permission on table t1 and the grantor of
record is p1
That's because "a" does not have permision to grant select on t1, so
INHERIT kicks in to implicitly "SET ROLE p1". What keeps INHERIT sane
is that it only kicks in when required (i.e. it would otherwise result
in failure).
But in the case I raised, the current user is an entirely valid
grantor, so it doesn't make sense to me to infer a different grantor.
As to the first, the algorithm being used to select the best grantor
here is analogous to the one we use for privileges on other object
types, such as tables, namely, we prefer to create a grant that is
not
dependent on some other grant, rather than one that is.
I don't quite follow. It seems like we're conflating a policy based on
INHERIT with the policy around grants by superusers.
In the case of role membership and INHERIT, our current behavior seems
wise (and closer to the standard): to prefer a grantor that is closer
to the current user/role, and therefore less dependent on other grants.
But for the new policy around superusers, the current superuser is a
completely valid grantor, and we instead preferring the bootstrap
superuser. That doesn't seem consistent or wise to me.
The primary purpose
of the patch was to make the handing of role grants consistent with
the handling of grants on other object types.
I certainly don't want to pin every weird thing about our privilege
system on you just because you're the last one to touch it. But your
changes did extend the behavior, and create some new analogous
behavior, so it seems like a reasonable time to discuss whether those
extensions are in the right direction.
When you view this in the context of how other types of grants work,
ALTER ROLE ... NOSUPERUSER isn't as much of a special case. Just as
we
want ALTER ROLE ... NOSUPERUSER to succeed quickly, we also insist
that REVOKE role1 FROM role2 to succeed quickly. It isn't allowed to
fail due to the existence of dependent privileges, because there
aren't allowed to be any dependent privileges.
create user u1;
create user u2;
create user u3;
grant u2 to u1 with admin option;
\c - u1
grant u2 to u3;
\c - bootstrap_superuser
revoke u2 from u1;
ERROR: dependent privileges exist
But
grants are different: it matters who does it, and when someone uses
superuser powers or other special privileges to perform an operation,
we have to ask on whose behalf they are acting.
If superusers merely act on behalf of others, then:
1. Why can the bootstrap superuser be a grantor?
2. Why can non-bootstrap superusers specify themselves in GRANTED BY
if they are not suitable grantors?
I think the fact that we have strong
integrity constraints around who can be recorded as the grantor of a
privilege is a good thing, and, again, the purpose of this patch was
to bring role grants up to the level of other parts of the system.
I like integrity constriants, too. But it feels like we're recording
the wrong information (losing the actual grantor) because it's easier
to keep it "consistent", which doesn't necessarily seem like a win.
And the whole reason we are jumping through all of these hoops is
because we want to allow the removal of superuser privileges quickly
without the possibility of failure. In other words, we don't have time
to do the work of cascading to dependent objects, or erroring when we
find them. I'm not entirely sure I agree that's a hard requirement,
because dropping a superuser can fail. But even if it is a requirement,
are we even meeting it if we preserve the grants that the former
superuser created? I'd like to know more about this requirement, and
whether we are still meeting it, and whether there are alternatives.
It just feels like this edge case requirement about dropping superuser
privileges is driving the whole design, and that feels wrong to me.
* Superusers would auto-grant themselves the privileges that a
normal
user would need to do something before doing it. For instance, if a
superuser did "GRANT u2 TO u1", it would first automatically issue
a
"GRANT u2 TO current_user WITH ADMIN OPTION GRANTED BY
bootstrap_superuser", then do the grant normally.
...
it seems more theoretically sound than the previous two
ideas, because it doesn't just throw the idea of integrity
constraints
out the window.
Perhaps it's worth considering further. Would be a separate patch, of
course.
Also, it would be nice to have REASSIGN OWNED work with grants,
perhaps
by adding a "WITH[OUT] GRANT" or something.
...
What might be useful is a command that says "OK, for every existing
grant that is attributed to user A, change the recorded grantor to
user B, if that's allowable, for the others, do nothing". Or maybe
there's some possible idea where we try to somehow make B into a
valid
grantor, but it's not clear to me what the algorithm would be.
I was thinking that if the new grantor is not allowable, and "WITH
GRANT" (or whatever) was specified, then it would throw an error.
Regards,
Jeff Davis
On Fri, Sep 2, 2022 at 6:01 PM Jeff Davis <pgsql@j-davis.com> wrote:
Yes. I figured that, when GRANTED BY is not specified, it is OK to
infer a valid grantorThe spec is clear that the grantor should be either the current user or
the current role. We also have a concept of INHERIT, which allows us to
choose a role we're a member of if the current one does not suffice.But to choose a different role (the bootstrap superuser) even when the
current (super) user role *does* suffice seems like an outright
violation of both the spec and the principle of least surprise.
I don't think that the current superuser role suffices. For non-role
objects, privileges originate in the table owner and can then be
granted to others. Roles don't have an explicit owner, so I treated
the bootstrap superuser as the implicit owner of every role. Perhaps
there is some other way we could go here - e.g. it's been proposed by
multiple people that maybe roles should have owners - but I do not
think it is viable to regard the owner of a role as being anyone who
happens to be a superuser right at the moment. To some extent that's
related to your concern about whether ALTER USER .. NOSUPERUSER should
be fast and immune to failure, but I also think that it is a good idea
to have all of the privileges originating from a single owner. That
ensures, for example, that anyone who can act as the object owner can
revoke any privilege, which wouldn't necessarily be true if the object
had multiple owners. Now if all of the owners are themselves
superusers who all have the power to become any of the other owners
then perhaps it wouldn't end up mattering too much, but it doesn't
seem like a good idea to rely on that. In fact, part of my goal here
is to get to a world where there's less need to rely on superuser
powers to do system administration. I also just think it's less
confusing if objects have single owners rather than nebulous groups of
owners.
set session authorization a;
grant select on table t1 to b;At this point, b has SELECT permission on table t1 and the grantor of
record is p1That's because "a" does not have permision to grant select on t1, so
INHERIT kicks in to implicitly "SET ROLE p1". What keeps INHERIT sane
is that it only kicks in when required (i.e. it would otherwise result
in failure).But in the case I raised, the current user is an entirely valid
grantor, so it doesn't make sense to me to infer a different grantor.
See above, but also, see the first stanza of select_best_grantor(). If
alice is a table owner, and grants permissions to bob WITH GRANT
OPTION, and bob is a superuser and grants permissions on the table,
the grantor will be alice, not bob.
As to the first, the algorithm being used to select the best grantor
here is analogous to the one we use for privileges on other object
types, such as tables, namely, we prefer to create a grant that is
not
dependent on some other grant, rather than one that is.I don't quite follow. It seems like we're conflating a policy based on
INHERIT with the policy around grants by superusers.In the case of role membership and INHERIT, our current behavior seems
wise (and closer to the standard): to prefer a grantor that is closer
to the current user/role, and therefore less dependent on other grants.But for the new policy around superusers, the current superuser is a
completely valid grantor, and we instead preferring the bootstrap
superuser. That doesn't seem consistent or wise to me.
I hope that the above comments on treating the bootstrap superuser as
the object owner explain why it works this way.
I certainly don't want to pin every weird thing about our privilege
system on you just because you're the last one to touch it. But your
changes did extend the behavior, and create some new analogous
behavior, so it seems like a reasonable time to discuss whether those
extensions are in the right direction.
Sure.
When you view this in the context of how other types of grants work,
ALTER ROLE ... NOSUPERUSER isn't as much of a special case. Just as
we
want ALTER ROLE ... NOSUPERUSER to succeed quickly, we also insist
that REVOKE role1 FROM role2 to succeed quickly. It isn't allowed to
fail due to the existence of dependent privileges, because there
aren't allowed to be any dependent privileges.create user u1;
create user u2;
create user u3;
grant u2 to u1 with admin option;
\c - u1
grant u2 to u3;
\c - bootstrap_superuser
revoke u2 from u1;
ERROR: dependent privileges exist
Hmm, I stand corrected. I was thinking of a case in which the grant
was used to perform an action on behalf of an inherited role. Here the
grant from u2 to u3 is performed as u1 and attributed to u1.
And the whole reason we are jumping through all of these hoops is
because we want to allow the removal of superuser privileges quickly
without the possibility of failure. In other words, we don't have time
to do the work of cascading to dependent objects, or erroring when we
find them. I'm not entirely sure I agree that's a hard requirement,
because dropping a superuser can fail. But even if it is a requirement,
are we even meeting it if we preserve the grants that the former
superuser created? I'd like to know more about this requirement, and
whether we are still meeting it, and whether there are alternatives.It just feels like this edge case requirement about dropping superuser
privileges is driving the whole design, and that feels wrong to me.
I'm struggling to figure out how to reply to this exactly. I do agree
that the way ALTER ROLE .. [NO]SUPERUSER thing works is something of a
wart, and if we were designing SQL from scratch all over again in
2022, I think it's reasonably likely that a lot of things would end up
working quite a bit differently than they actually do. But, at the
same time, it also seems to me that (1) the way ALTER ROLE ..
[NO]SUPERUSER works is pretty firmly entrenched at this point and we
can't easily get away with changing it; (2) I don't really see an easy
way of changing it that wouldn't cause more problems than it solves;
and (3) it all seems relatively unrelated to this patch.
Like, the logic to infer the grantor in check_role_grantor() and
select_best_admin() is intended to be, and as far as I know actually
is, an exact clone of the logic in select_best_grantor(). It is
different only in that we regard the bootstrap superuser as the object
owner because there is no other owner stored in the catalogs; and in
that we check CREATEROLE permission rather than SUPERUSER permission.
Everything else is the same. To be unhappy with the patch, you have to
think either that (1) treating the bootstrap superuser as the owner of
every role is the wrong idea or (2) that role grants should not choose
an implicit grantor in the same way that other types of grants do or
(3) that the code has a bug.
If you don't think any of those things but believe that the way we've
made superusers interact with the grant system is lame in general, I
somewhat agree, but if we came up with some new paradigm for how it
should work, we'd have to explain why it was sufficiently better than
the status quo to justify breaking backward compatibility, and I think
that would be a hard argument to make. The current system feels kind
of old-fashioned and awkward, but it's self-consistent on its terms
and I bet a lot of people are relying on it to keep working. And I
think if we were going to replace it with something that feels fresh
and modern, focusing on the behavior of ALTER ROLE .. [NO]SUPERUSER
would be the wrong place to start. That, to me, seems like it's a
*mostly* a consequence of much broader design choices, like:
- Having hard-coded superuser checks in many places instead of making
everything a capability.
- Having potentially any number of superusers, instead of just one root user.
- Having granted privileges depend on the grantor continuing to hold
the granted privilege, instead of existing independently.
If I were designing a privilege system for a new piece of software
that didn't need to comply with the SQL standard, I think I'd throw at
least some and maybe all of those things right out the window. But I
designed a system that had to work within that set of assumptions, I
think I'd make it work pretty much the way it actually does.
What might be useful is a command that says "OK, for every existing
grant that is attributed to user A, change the recorded grantor to
user B, if that's allowable, for the others, do nothing". Or maybe
there's some possible idea where we try to somehow make B into a
valid
grantor, but it's not clear to me what the algorithm would be.I was thinking that if the new grantor is not allowable, and "WITH
GRANT" (or whatever) was specified, then it would throw an error.
That could be done too, but then every grant attributed to the target
role would have to be validly reattributable to the same new grantor.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Tue, 2022-09-06 at 13:15 -0400, Robert Haas wrote:
Like, the logic to infer the grantor in check_role_grantor() and
select_best_admin() is intended to be, and as far as I know actually
is, an exact clone of the logic in select_best_grantor(). It is
different only in that we regard the bootstrap superuser as the
object
owner because there is no other owner stored in the catalogs; and in
that we check CREATEROLE permission rather than SUPERUSER permission.
There's at least one other difference: if you specify "GRANTED BY su1"
for a table grant, it still selects the table owner as the grantor;
whereas if you specify "GRANTED BY su1" for a role grant, it selects
"su1".
grant all privileges on schema public to public;
create user su1 superuser;
create user u1;
create user u2;
create user aa;
grant u2 to su1 with admin option;
\c - aa
create table t_aa(i int);
grant all privileges on t_aa to su1 with grant option;
\c - su1
grant select on t_aa to u1 granted by su1;
-- grantor aa
select relname, relacl from pg_class where relname='t_aa';
grant u2 to u1 granted by su1; -- grantor su1
-- grantor su1
select grantor::regrole from pg_auth_members
where member='u1'::regrole;
[ If you run the same example but where su1 is not a superuser, then
both select "su1" as the grantor because that's the only valid grantor
that can be inferred. ]
Now that I understand the underlying philosophy better, and I've
experimented with more cases, I propose the following grantor inference
behavior which I believe is in the spirit of your changes:
* Let the granting user be the one specified in the GRANTED BY clause
if it exists; otherwise the current user. In other words, omitting
GRANTED BY is the same as specifying "GRANTED BY current_user".
* If the granting user has privileges to be the grantor (ADMIN OPTION
for roles, GRANT OPTION for other objects) then the granting user is
the grantor.
* Else if the granting user inherits from a user with the privileges
to be the grantor, then it selects a role with the fewest inheritance
hops as the grantor.
* Else if the current user is any superuser:
- If the grant is a role grant, it selects the bootstrap superuser
as the grantor.
- Else the object owner is the grantor.
* Else error (or if an error would break important backwards
compatibility, silently make it work like before or perhaps issue a
WARNING).
In other words, try to issue the grant normally if at all possible, and
play the superuser card as a last resort. I believe that will lead to
the fewest surprising cases, and make them easiest to explain, because
superuser-ness doesn't influence the outcome in as many cases.
It cements the idea that the bootstrap superuser is the "real"
superuser, and must always remain so, and that all other superusers are
temporary stand-ins (kind of but not quite the same as inheritance).
And it leaves the ugliness that we lose the information about the
"real" grantor when we play the superuser card, but, as I say above,
that would be a last resort.
The proposal would be a slight behavior change from v15 in the
following case:
grant all privileges on schema public to public;
create user su1 superuser;
create user u1;
create user aa;
\c - aa
create table t_aa(i int);
grant all privileges on t_aa to su1 with grant option;
\c - su1
grant select on t_aa to u1 granted by su1;
-- grantor "aa" in v15, grantor "su1" after my proposal
select relname, relacl from pg_class where relname='t_aa';
Another change in behavior would be that the bootstrap superuser could
be the grantor for table privileges, if the bootstrap superuser has
WITH GRANT OPTION privileges.
But those seems minor to me.
Regards,
Jeff Davis
On Tue, 2022-09-06 at 16:26 -0700, Jeff Davis wrote:
In other words, omitting
GRANTED BY is the same as specifying "GRANTED BY current_user".
Let me correct this thinko to distinguish between specifying GRANTED BY
and not:
* Let the granting user be the one specified in the GRANTED BY clause
if it exists; otherwise the current user.
* If the granting user has privileges to be the grantor (ADMIN OPTION
for roles, GRANT OPTION for other objects) then the granting user is
the grantor.
* Else if GRANTED BY was *not* specified, infer the grantor:
- If the granting user inherits from a role with the privileges
to be the grantor, then it selects a role with the fewest inheritance
hops as the grantor.
- Else if the current user is any superuser, the grantor is the top
"owner" (bootstrap superuser for roles; object owner for other objects)
* Else error (or if an error would break important backwards
compatibility, silently make it work like before and perhaps issue a
WARNING).
The basic idea is to use superuser privileges as a last resort in order
to maximize the cases that work normally (independent of superuser-
ness).
Regards,
Jeff Davis
On Tue, Sep 6, 2022 at 7:26 PM Jeff Davis <pgsql@j-davis.com> wrote:
There's at least one other difference: if you specify "GRANTED BY su1"
for a table grant, it still selects the table owner as the grantor;
whereas if you specify "GRANTED BY su1" for a role grant, it selects
"su1".
Right. Personally, I'm inclined to view that as a defect in the
"GRANTED BY whoever" implementation for other object types, and I
think it should be resolved by making other object types error out if
the user explicitly mentioned in the "GRANTED BY" clause isn't a valid
grantor. It also seems possible to view it as a defect in the new
implementation, and argue that inference should always be performed
starting at the named user. I find that a POLA violation, but someone
could disagree.
Parenthetically, I think we should also fix GRANTED BY for other
object types so that it actually works, but that is a bit of headache
because it doesn't seem like that code is relying as heavily on common
infrastructure as some things, so I believe it's actually a fair
amount of work to make that happen.
In other words, try to issue the grant normally if at all possible, and
play the superuser card as a last resort. I believe that will lead to
the fewest surprising cases, and make them easiest to explain, because
superuser-ness doesn't influence the outcome in as many cases.
It seems to me that this policy would reverse select_best_grantor()'s
decision about whether we should prefer to rely on superuser
privileges or on privileges actually granted to the current user. I
think either behavior is defensible, but the existing precedent is to
prefer relying on superuser privileges. Like you, I found that a bit
weird when I realized that's what it was doing, but it does have some
advantages. In particular, it means that the privileges granted by a
superuser don't depend on any other grants, which is something that a
user might value.
Now that is not to say that we couldn't decide that
select_best_grantor() got it wrong and choose to break backward
compatibility in order to fix it ... but I'm not even convinced that
the alternative behavior you propose is clearly better, let alone that
it's enough better to justify changing things. However, I don't
personally have a strong preference about it one way or the other; if
there's a strong consensus to change it, so be it.
--
Robert Haas
EDB: http://www.enterprisedb.com
On Wed, 2022-09-07 at 09:39 -0400, Robert Haas wrote:
Now that is not to say that we couldn't decide that
select_best_grantor() got it wrong and choose to break backward
compatibility in order to fix it ... but I'm not even convinced that
the alternative behavior you propose is clearly better, let alone
that
it's enough better to justify changing things.
OK. I suppose the best path forward is to just try to improve the
ability to administer the system without relying as much on superusers,
which will allow us to safely ignore some of the weirdness caused by
superusers issuing grants.
Regards,
Jeff Davis
On Wed, Sep 7, 2022 at 10:56 AM Jeff Davis <pgsql@j-davis.com> wrote:
OK. I suppose the best path forward is to just try to improve the
ability to administer the system without relying as much on superusers,
which will allow us to safely ignore some of the weirdness caused by
superusers issuing grants.
Yeah, and I think we might not even be that far away from making that
happen. There are still a few thorny design issues to work out, I
believe, and there's also some complexity that is introduced by the
fact that different people want different things. For example, last
release cycle, I believed that the NOINHERIT behavior was a weird wart
that probably nobody cared about. That turned out to be false, really
false.
What I *personally* want most as an alternative to superuser is an
account that inherits all the privileges of the other accounts that it
manages, which might not be all the accounts on the system, and which
can also SET ROLE to those accounts. If you're logged into such an
account, you can do many of the things a superuser can do and in the
same ways that a superuser can do them. For example, if you've got
some pg_dump output, you could probably restore the dump using such an
account and privilege restoration would work, provided that the
required accounts exist and that they're among the accounts managed by
your account.
However, I think that other people want different things. For example,
I think that Joshua Brindle mentioned wanting to have a user-creation
bot that should be able to make new accounts but not access them in
any way, and I think Stephen Frost was interested in semantics where
you could make accounts and be able to SET ROLE into them but not
inherit their privileges. Or maybe they were both proposing the same
thing: not quite sure. Anyway, it will perhaps turn out to be
impossible to give everybody 100% of everything they would like, but
I'm thinking about a few ideas that might enable us to cater to a few
different scenarios - and I'm hopeful that it will be possible to
propose something in time for inclusion in v16, but my ideas aren't
quite well enough formulated yet to make a concrete proposal just yet,
and when I do make such a proposal I want to do it on a new thread for
better visibility.
In the meantime, I think that what has already been committed is
clearly a step in the right direction. The patch which is the subject
of this thread has basically brought the role grant code up to the
level of other object types. I don't think it's an overstatement to
say that the previous state of affairs was that this feature just
didn't work properly and no one had cared enough to bother fixing it.
That always makes discussions about future enhancements harder. The
patch to add grant-level control to the INHERIT option also seems to
me to be a step in the right direction, since, at least IMHO, it is
really hard to reason about behavior when the heritability of a
particular grant is a property of the grantee rather than something
which can be controlled by the grantor, or the system. If we can reach
agreement on some of the other things that I have proposed,
specifically sorting out the issues under discussion on the
"has_privs_of_role vs. is_member_of_role, redux" thread and adding the
new capability discussed on the "allowing for control over SET ROLE"
thread, I think will be a further, useful step.
--
Robert Haas
EDB: http://www.enterprisedb.com
Robert Haas <robertmhaas@gmail.com> writes:
On Thu, Aug 18, 2022 at 1:26 PM Robert Haas <robertmhaas@gmail.com> wrote:
CI is happier with this version, so I've committed 0001. If no major
problems emerge, I'll proceed with 0002 as well.
Done.
Shouldn't the CF entry [1]https://commitfest.postgresql.org/39/3745/ be closed as committed?
regards, tom lane