Thread: Test if a database has any privilege granted to public

Test if a database has any privilege granted to public

From
Bryn Llewellyn
Date:
I want to adopt a rule that no database in my cluster has any privilege granted to public. It suits me best to encapsulate the test as a boolean function thus:

function mgr.db_has_priv_granted_to_public(db in name)

where "mgr" is a convenient schema for various admin utilities. I have implemented the function. And preliminary tests haven't shown that it doesn't work.

I created two databases like this, using a session that I authorized as a non-bootstrap superuser called "yugabyte":

create database d1;
create database d2;

grant  all on database d1 to yugabyte;
revoke all on database d2 from yugabyte;

grant  all on database d1 to public;
revoke all on database d2 from public;

And I tested the function like this:

select
  datname,
  mgr.db_has_priv_granted_to_public(datname)::text as "bad?"
from pg_database
where datname in ('d1'::name, 'd2'::name)
order by 1;

It produced this result:

 datname | bad?  
---------+-------
 d1      | true
 d2      | false

Here's the function's implementation:

create type mgr.acl_t as (grantor oid, grantee oid, privilege_type text, is_grantable boolean);

create function mgr.db_has_priv_granted_to_public(db in name)
  returns boolean
  set search_path = pg_catalog, pg_temp
  language sql
as $body$
  with
    c1 as
      (
        select (aclexplode(datacl)::text)::mgr.acl_t as v1 from pg_database where datname = db
      ),
    c2 as
      (
        select (c1.v1).grantee as grantee from c1
      )
  select exists(select 1 from c2 where grantee = 0::oid);
$body$;

The design of the user-defined type was inspired by "\df aclexplode". And the typecast to "text" and thence to my "acl_t" works around the error "cannot cast type record to acl_t". I want only to access the "grantee" field of the "aclitem" value.

My code feels very obscure and verbose. Having said this, searching the PG doc, and doing general Internet searches didn't bring any inspiration. Rather, all that I found was this:

The default ACL type in Postgres (aclitem) produces rather cryptic output that is hard to understand.

And indeed, "pg_database.datacl::text" produces output like this:

{=CTc/yugabyte,yugabyte=CTc/yugabyte}

Sure enough, the PG doc does say "An empty grantee field in an aclitem stands for PUBLIC." But it would be a pain to parse that text and deduce the presence of the empty field that I can see with my human eyes.

However, I don't want to mess around with a third party extension to meet a goal that is so simply stated.

Am I missing something? Is there a better way to implement my function?


Re: Test if a database has any privilege granted to public

From
Ron
Date:
Off-topic, but you don't need all those text casts.

On 12/14/22 23:44, Bryn Llewellyn wrote:
I want to adopt a rule that no database in my cluster has any privilege granted to public. It suits me best to encapsulate the test as a boolean function thus:

function mgr.db_has_priv_granted_to_public(db in name)

where "mgr" is a convenient schema for various admin utilities. I have implemented the function. And preliminary tests haven't shown that it doesn't work.

I created two databases like this, using a session that I authorized as a non-bootstrap superuser called "yugabyte":

create database d1;
create database d2;

grant  all on database d1 to yugabyte;
revoke all on database d2 from yugabyte;

grant  all on database d1 to public;
revoke all on database d2 from public;

And I tested the function like this:

select
  datname,
  mgr.db_has_priv_granted_to_public(datname)::text as "bad?"
from pg_database
where datname in ('d1'::name, 'd2'::name)
order by 1;

It produced this result:

 datname | bad?  
---------+-------
 d1      | true
 d2      | false

Here's the function's implementation:

create type mgr.acl_t as (grantor oid, grantee oid, privilege_type text, is_grantable boolean);

create function mgr.db_has_priv_granted_to_public(db in name)
  returns boolean
  set search_path = pg_catalog, pg_temp
  language sql
as $body$
  with
    c1 as
      (
        select (aclexplode(datacl)::text)::mgr.acl_t as v1 from pg_database where datname = db
      ),
    c2 as
      (
        select (c1.v1).grantee as grantee from c1
      )
  select exists(select 1 from c2 where grantee = 0::oid);
$body$;

The design of the user-defined type was inspired by "\df aclexplode". And the typecast to "text" and thence to my "acl_t" works around the error "cannot cast type record to acl_t". I want only to access the "grantee" field of the "aclitem" value.

My code feels very obscure and verbose. Having said this, searching the PG doc, and doing general Internet searches didn't bring any inspiration. Rather, all that I found was this:

The default ACL type in Postgres (aclitem) produces rather cryptic output that is hard to understand.

And indeed, "pg_database.datacl::text" produces output like this:

{=CTc/yugabyte,yugabyte=CTc/yugabyte}

Sure enough, the PG doc does say "An empty grantee field in an aclitem stands for PUBLIC." But it would be a pain to parse that text and deduce the presence of the empty field that I can see with my human eyes.

However, I don't want to mess around with a third party extension to meet a goal that is so simply stated.

Am I missing something? Is there a better way to implement my function?



--
Angular momentum makes the world go 'round.

Re: Test if a database has any privilege granted to public

From
Tom Lane
Date:
Ron <ronljohnsonjr@gmail.com> writes:
> Off-topic, but you don't need all those text casts.

Indeed.  Something like this ought to do it:

=# select datname from pg_database where 0::oid = any(select (aclexplode(datacl)).grantee);
  datname
------------
 template1
 template0
 regression
(3 rows)

            regards, tom lane



Re: Test if a database has any privilege granted to public

From
Bryn Llewellyn
Date:
tgl@sss.pgh.pa.us wrote:

ronljohnsonjr@gmail.com writes:

Off-topic, but you don't need all those text casts.

Indeed.  Something like this ought to do it:

select datname from pg_database where 0::oid = any(select (aclexplode(datacl)).grantee);

 datname   
------------
template1
template0
regression

Thanks, both, for the lightning-fast replies. Yes, I see it now. (I got myself confused about the requirements for using parentheses.) I should have slept on it before sending to the list.

There's still a little snag though. I created a brand-new cluster (with bootstrap superuser called "postgres"), started a session as "postgres", and did this:

create database d1;
revoke all on database d1 from postgres;
revoke all on database d1 from public;

create database d2;
revoke all on database d2 from postgres;

create database d3;

select
  datname::text                                               as name,
  case
    when datacl is null then '<NULL>'
    else                     datacl::text
  end                                                         as datacl,
  (0::oid = any(select (aclexplode(datacl)).grantee))::text   as "public has a priv"
from pg_database
where datname in ('d1', 'd2', 'd3')
order by 1;

It produced this result:

 name |     datacl     | public has a priv 
------+----------------+-------------------
 d1   | {}             | false
 d2   | {=Tc/postgres} | true
 d3   | <NULL>         | false

This seems to imply that this wording from "5.7. Privileges" (https://www.postgresql.org/docs/current/ddl-priv.html) is a little sketchy:

«
For other types of objects, the default privileges granted to PUBLIC are as follows: CONNECT and TEMPORARY (create temporary tables) privileges for databases…
»

The effect of  a NULL "datacl" is as if CONNECT and TEMPORARY have been granted to public. But even so, these privileges are not shown to have been actually granted.

In my test, I simply revoked "all" on "d2" from postgres. And this produced a not null "datacl" that did then show the documented default regime.

The following test:

create role r with login password 'p';
\c d1 r
\c d2 r
\c d3 r

Showed that "public has a priv" (as I coded it) doesn't tell the whole story because "\c d3 r" (as well as "\c d2 r") succeeds. Of course, "\c d1 r" fails.

I do see that, in a strict "legal sense", the doc that I quoted is not (quite) wrong. But to implement the test that I want robustly, I need to extend the logic thus:

select datname::text
from pg_database
where 0::oid = any(select (aclexplode(datacl)).grantee)
or datacl is null;

That's easy if you know that you need to write this. But the need to do so seems to depend on pretty arcane knowledge that, as far as I can see, isn't documented.

Anyway, my immediate requirement is solved. Thanks again!

Re: Test if a database has any privilege granted to public

From
"David G. Johnston"
Date:
On Thu, Dec 15, 2022 at 12:51 PM Bryn Llewellyn <bryn@yugabyte.com> wrote:

select datname::text
from pg_database
where 0::oid = any(select (aclexplode(datacl)).grantee)
or datacl is null;

That's easy if you know that you need to write this. But the need to do so seems to depend on pretty arcane knowledge that, as far as I can see, isn't documented.


The last paragraph of the privileges documentation says this explicitly:

If the “Access privileges” column is empty for a given object, it means the object has default privileges (that is, its privileges entry in the relevant system catalog is null). Default privileges always include all privileges for the owner, and can include some privileges for PUBLIC depending on the object type, as explained above.


Or, you know, just use the provided functions that have been programmed with knowledge of how the system works.


select has_database_privilege(0,current_database(),'CONNECT');

David J.


Re: Test if a database has any privilege granted to public

From
Bryn Llewellyn
Date:
> david.g.johnston@gmail.com wrote:
>
>> bryn@yugabyte.com wrote:
>>
>> select datname::text
>> from pg_database
>> where 0::oid = any(select (aclexplode(datacl)).grantee)
>> or datacl is null;
>>
>> That's easy if you know that you need to write this. But the need to do so seems to depend on pretty arcane
knowledgethat, as far as I can see, isn't documented. 
>
> The last paragraph of the privileges documentation says this explicitly:
>
> If the “Access privileges” column is empty for a given object, it means the object has default privileges (that is,
itsprivileges entry in the relevant system catalog is null). Default privileges always include all privileges for the
owner,and can include some privileges for PUBLIC depending on the object type, as explained above. 
>
> https://www.postgresql.org/docs/current/ddl-priv.html
>
> Or, you know, just use the provided functions that have been programmed with knowledge of how the system works.
>
> https://www.postgresql.org/docs/current/functions-info.html#FUNCTIONS-INFO-ACCESS-TABLE
>
> select has_database_privilege(0,current_database(),'CONNECT');

Ah yes, thanks. I’d missed that at the bottom of the page. I find the "aclitem" base type a bit confusing. I understand
that,as a base type, its structure is hidden. However, its text typecast, exemplified by this: 

calvin=r*w/hobbes

is conventional and rather informally doc'd. For example, what is a field? You have to guess.

There's no mention on the "Privileges" page of the "has_database_privilege()" function. Nor of "aclexplode()".

Even now, I haven't managed a linear start to finish read of the entire PG docs. And I found "has_database_privilege()"
and"aclexplode()" by Internet search rather than x-refs within the PG doc. 

The account of "has_database_privilege()" has this:

has_database_privilege ( [ user name or oid, ] database text or oid, privilege text ) → boolean

but that's the only mention of the function on the "System Information Functions and Operators" page. So nothing says
whatit means to use the (text, text) or (oid, text) overloads. 

Moreover, nothing says that "0" denotes "public". (Nor does anything that I've found say that it's the same for "0" in
thefirst field of what "aclexplode()" produces for each element of its "aclitem[]" argumemt. Internet search for
"postgresoid of public" gets no useful hits. 

But experiment shows that you can use this reserved name (in single quotes) with the same effect as "0".

I suppose that it all boils down to this:

…where
  has_database_privilege('public', datname, 'connect') or
  has_database_privilege('public', datname, 'create') or
  has_database_privilege('public', datname, 'temp');

versus this:

…where 0::oid = any(select (aclexplode(datacl)).grantee) or datacl is null;

Yes, I prefer the version that uses "has_database_privilege()" (even though it's longer) because it says more clearly
whatit means. 

Thanks!





Re: Test if a database has any privilege granted to public

From
"David G. Johnston"
Date:
On Thu, Dec 15, 2022 at 5:17 PM Bryn Llewellyn <bryn@yugabyte.com> wrote:

There's no mention on the "Privileges" page of the "has_database_privilege()" function. Nor of "aclexplode()".

Even now, I haven't managed a linear start to finish read of the entire PG docs. And I found "has_database_privilege()" and "aclexplode()" by Internet search rather than x-refs within the PG doc.


Sure, because as a typical user the implementation detail of all this is unimportant.  You interact through the GRANT/REVOKE interface.  Or find tools that present this kind of information graphically.  People just aren't asking the kinds of questions that suggest our level of documentation is insufficient.  That you've found gaps to be possibly filled in isn't surprising.  But it is also less time and effort answering your questions to help mostly just you than it is to improve the documentation to help mostly just you.
 
The account of "has_database_privilege()" has this:

has_database_privilege ( [ user name or oid, ] database text or oid, privilege text ) → boolean

but that's the only mention of the function on the "System Information Functions and Operators" page. So nothing says what it means to use the (text, text) or (oid, text) overloads.


The paragraph I note below covers all of this.  Maybe it's a bit "wall-of-text"ish but the material is present.  


But experiment shows that you can use this reserved name (in single quotes) with the same effect as "0".

Yep, as documented:

Table 9.67 lists functions that allow querying object access privileges programmatically. (See Section 5.7 for more information about privileges.) In these functions, the user whose privileges are being inquired about can be specified by name or by OID (pg_authid.oid), or if the name is given as public then the privileges of the PUBLIC pseudo-role are checked.


I'm not sure where I picked up the comment about 0 working but since "public" works and is documented that implementation detail need not be discoverable.

David J.