From 4b2dafcc0b1a579ef5daaa2728223006d1ff98e9 Mon Sep 17 00:00:00 2001 From: Tom Lane Date: Tue, 1 Jun 2004 21:49:23 +0000 Subject: [PATCH] Align GRANT/REVOKE behavior more closely with the SQL spec, per discussion of bug report #1150. Also, arrange that the object owner's irrevocable grant-option permissions are handled implicitly by the system rather than being listed in the ACL as self-granted rights (which was wrong anyway). I did not take the further step of showing these permissions in an explicit 'granted by _SYSTEM' ACL entry, as that seemed more likely to bollix up existing clients than to do anything really useful. It's still a possible future direction, though. --- doc/src/sgml/ref/grant.sgml | 64 +++- doc/src/sgml/ref/revoke.sgml | 31 +- src/backend/catalog/aclchk.c | 562 +++++++++++++++++++------------ src/backend/utils/adt/acl.c | 411 +++++++++++++++++----- src/include/utils/acl.h | 20 +- src/include/utils/errcodes.h | 4 +- src/test/regress/expected/privileges.out | 6 +- 7 files changed, 764 insertions(+), 334 deletions(-) diff --git a/doc/src/sgml/ref/grant.sgml b/doc/src/sgml/ref/grant.sgml index 59b0d8148e..25ba6e29bd 100644 --- a/doc/src/sgml/ref/grant.sgml +++ b/doc/src/sgml/ref/grant.sgml @@ -1,5 +1,5 @@ @@ -67,9 +67,10 @@ GRANT { { CREATE | USAGE } [,...] | ALL [ PRIVILEGES ] } If WITH GRANT OPTION is specified, the recipient - of the privilege may in turn grant it to others. By default this - is not allowed. Grant options can only be granted to individual - users, not to groups or PUBLIC. + of the privilege may in turn grant it to others. Without a grant + option, the recipient cannot do that. At present, grant options can + only be granted to individual users, not to groups or + PUBLIC. @@ -79,8 +80,8 @@ GRANT { { CREATE | USAGE } [,...] | ALL [ PRIVILEGES ] } however, choose to revoke some of his own privileges for safety.) The right to drop an object, or to alter its definition in any way is not described by a grantable privilege; it is inherent in the owner, - and cannot be granted or revoked. It is not possible for the owner's - grant options to be revoked, either. + and cannot be granted or revoked. The owner implicitly has all grant + options for the object, too. @@ -150,7 +151,7 @@ GRANT { { CREATE | USAGE } [,...] | ALL [ PRIVILEGES ] } RULE - Allows the creation of a rule on the table/view. (See statement.) @@ -171,7 +172,7 @@ GRANT { { CREATE | USAGE } [,...] | ALL [ PRIVILEGES ] } TRIGGER - Allows the creation of a trigger on the specified table. (See + Allows the creation of a trigger on the specified table. (See the statement.) @@ -234,7 +235,7 @@ GRANT { { CREATE | USAGE } [,...] | ALL [ PRIVILEGES ] } ALL PRIVILEGES - Grant all of the privileges applicable to the object at once. + Grant all of the available privileges at once. The PRIVILEGES key word is optional in PostgreSQL, though it is required by strict SQL. @@ -258,6 +259,20 @@ GRANT { { CREATE | USAGE } [,...] | ALL [ PRIVILEGES ] } + When a non-owner of an object attempts to GRANT privileges + on the object, the command will fail outright if the user has no + privileges whatsoever on the object. As long as some privilege is + available, the command will proceed, but it will grant only those + privileges for which the user has grant options. The GRANT ALL + PRIVILEGES forms will issue a warning message if no grant options are + held, while the other forms will issue a warning if grant options for + any of the privileges specifically named in the command are not held. + (In principle these statements apply to the object owner as well, but + since the owner is always treated as holding all grant options, the + cases can never occur.) + + + It should be noted that database superusers can access all objects regardless of object privilege settings. This is comparable to the rights of root in a Unix system. @@ -273,10 +288,10 @@ GRANT { { CREATE | USAGE } [,...] | ALL [ PRIVILEGES ] } - Currently, to grant privileges in PostgreSQL - to only a few columns, you must - create a view having the desired columns and then grant privileges - to that view. + Currently, PostgreSQL does not support + granting or revoking privileges for individual columns of a table. + One possible workaround is to create a view having just the desired + columns and then grant privileges to that view. @@ -286,9 +301,9 @@ GRANT { { CREATE | USAGE } [,...] | ALL [ PRIVILEGES ] } => \z mytable Access privileges for database "lusitania" - Schema | Name | Type | Access privileges ---------+---------+-------+----------------------------------------------------------------- - public | mytable | table | {=r/postgres,miriam=arwdRxt/postgres,"group todos=arw/postgres"} + Schema | Name | Type | Access privileges +--------+---------+-------+------------------------------------------------------------ + public | mytable | table | {miriam=arwdRxt/miriam,=r/miriam,"group todos=arw/miriam"} (1 row) The entries shown by \z are interpreted thus: @@ -331,7 +346,14 @@ and may include some privileges for PUBLIC depending on the object type, as explained above. The first GRANT or REVOKE on an object will instantiate the default privileges (producing, for example, -{=,miriam=arwdRxt}) and then modify them per the specified request. +{miriam=arwdRxt/miriam}) and then modify them per the +specified request. + + + + Notice that the owner's implicit grant options are not marked in the + access privileges display. A * will appear only when + grant options have been explicitly granted to someone. @@ -347,11 +369,17 @@ GRANT INSERT ON films TO PUBLIC; - Grant all privileges to user manuel on view kinds: + Grant all available privileges to user manuel on view + kinds: GRANT ALL PRIVILEGES ON kinds TO manuel; + + Note that while the above will indeed grant all privileges if executed by a + superuser or the owner of kinds, when executed by someone + else it will only grant those permissions for which the someone else has + grant options. diff --git a/doc/src/sgml/ref/revoke.sgml b/doc/src/sgml/ref/revoke.sgml index 7a6b664e71..096a813b7f 100644 --- a/doc/src/sgml/ref/revoke.sgml +++ b/doc/src/sgml/ref/revoke.sgml @@ -1,5 +1,5 @@ @@ -81,6 +81,7 @@ REVOKE [ GRANT OPTION FOR ] If GRANT OPTION FOR is specified, only the grant option for the privilege is revoked, not the privilege itself. + Otherwise, both the privilege and the grant option are revoked. @@ -103,7 +104,7 @@ REVOKE [ GRANT OPTION FOR ] Use 's \z command to - display the privileges granted on existing objects. See also for information about the format. @@ -114,10 +115,26 @@ REVOKE [ GRANT OPTION FOR ] C, then user A cannot revoke the privilege directly from C. Instead, user A could revoke the grant option from user B and use the CASCADE option so that the privilege is - automatically revoked from user C. + in turn revoked from user C. For another example, if both A and B + have granted the same privilege to C, A can revoke his own grant + but not B's grant, so C will still effectively have the privilege. + When a non-owner of an object attempts to REVOKE privileges + on the object, the command will fail outright if the user has no + privileges whatsoever on the object. As long as some privilege is + available, the command will proceed, but it will revoke only those + privileges for which the user has grant options. The REVOKE ALL + PRIVILEGES forms will issue a warning message if no grant options are + held, while the other forms will issue a warning if grant options for + any of the privileges specifically named in the command are not held. + (In principle these statements apply to the object owner as well, but + since the owner is always treated as holding all grant options, the + cases can never occur.) + + + If a superuser chooses to issue a GRANT or REVOKE command, the command is performed as though it were issued by the owner of the affected object. Since all privileges ultimately come @@ -140,11 +157,15 @@ REVOKE INSERT ON films FROM PUBLIC; - Revoke all privileges from user manuel on view kinds: + Revoke all privileges from user manuel on view + kinds: - + REVOKE ALL PRIVILEGES ON kinds FROM manuel; + + Note that this actually means revoke all privileges that I + granted. diff --git a/src/backend/catalog/aclchk.c b/src/backend/catalog/aclchk.c index 3aaabf1b99..de74a422b7 100644 --- a/src/backend/catalog/aclchk.c +++ b/src/backend/catalog/aclchk.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/catalog/aclchk.c,v 1.102 2004/05/28 16:37:11 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/catalog/aclchk.c,v 1.103 2004/06/01 21:49:22 tgl Exp $ * * NOTES * See acl.h. @@ -48,9 +48,6 @@ static void ExecuteGrantStmt_Namespace(GrantStmt *stmt); static const char *privilege_to_string(AclMode privilege); -static AclMode aclmask(Acl *acl, AclId userid, - AclMode mask, AclMaskHow how); - #ifdef ACLDEBUG static @@ -126,15 +123,12 @@ merge_acl_with_grant(Acl *old_acl, bool is_grant, AclItem aclitem; uint32 idtype; Acl *newer_acl; - bool grantee_is_owner = false; if (grantee->username) { aclitem.ai_grantee = get_usesysid(grantee->username); idtype = ACL_IDTYPE_UID; - - grantee_is_owner = (aclitem.ai_grantee == owner_uid); } else if (grantee->groupname) { @@ -161,19 +155,21 @@ merge_acl_with_grant(Acl *old_acl, bool is_grant, (errcode(ERRCODE_INVALID_GRANT_OPERATION), errmsg("grant options can only be granted to individual users"))); - if (!is_grant && grant_option && grantee_is_owner) - ereport(ERROR, - (errcode(ERRCODE_INVALID_GRANT_OPERATION), - errmsg("cannot revoke grant options from owner"))); - aclitem.ai_grantor = grantor_uid; + /* + * The asymmetry in the conditions here comes from the spec. In + * GRANT, the grant_option flag signals WITH GRANT OPTION, which means + * to grant both the basic privilege and its grant option. But in + * REVOKE, plain revoke revokes both the basic privilege and its + * grant option, while REVOKE GRANT OPTION revokes only the option. + */ ACLITEM_SET_PRIVS_IDTYPE(aclitem, (is_grant || !grant_option) ? privileges : ACL_NO_RIGHTS, - (grant_option || (!is_grant && !grantee_is_owner)) ? privileges : ACL_NO_RIGHTS, + (!is_grant || grant_option) ? privileges : ACL_NO_RIGHTS, idtype); - newer_acl = aclinsert3(new_acl, &aclitem, modechg, behavior); + newer_acl = aclupdate(new_acl, &aclitem, modechg, owner_uid, behavior); /* avoid memory leak when there are many grantees */ pfree(new_acl); @@ -221,12 +217,17 @@ static void ExecuteGrantStmt_Relation(GrantStmt *stmt) { AclMode privileges; + bool all_privs; ListCell *i; if (linitial_int(stmt->privileges) == ACL_ALL_RIGHTS) + { + all_privs = true; privileges = ACL_ALL_RIGHTS_RELATION; + } else { + all_privs = false; privileges = ACL_NO_RIGHTS; foreach(i, stmt->privileges) { @@ -250,6 +251,8 @@ ExecuteGrantStmt_Relation(GrantStmt *stmt) Form_pg_class pg_class_tuple; Datum aclDatum; bool isNull; + AclMode my_goptions; + AclMode this_privileges; Acl *old_acl; Acl *new_acl; AclId grantorId; @@ -269,15 +272,6 @@ ExecuteGrantStmt_Relation(GrantStmt *stmt) elog(ERROR, "cache lookup failed for relation %u", relOid); pg_class_tuple = (Form_pg_class) GETSTRUCT(tuple); - ownerId = pg_class_tuple->relowner; - grantorId = select_grantor(ownerId); - - if (stmt->is_grant - && !pg_class_ownercheck(relOid, GetUserId()) - && pg_class_aclcheck(relOid, GetUserId(), - ACL_GRANT_OPTION_FOR(privileges)) != ACLCHECK_OK) - aclcheck_error(ACLCHECK_NO_PRIV, ACL_KIND_CLASS, relvar->relname); - /* Not sensible to grant on an index */ if (pg_class_tuple->relkind == RELKIND_INDEX) ereport(ERROR, @@ -285,6 +279,69 @@ ExecuteGrantStmt_Relation(GrantStmt *stmt) errmsg("\"%s\" is an index", relvar->relname))); + /* Composite types aren't tables either */ + if (pg_class_tuple->relkind == RELKIND_COMPOSITE_TYPE) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("\"%s\" is a composite type", + relvar->relname))); + + ownerId = pg_class_tuple->relowner; + grantorId = select_grantor(ownerId); + + /* + * Must be owner or have some privilege on the object (per spec, + * any privilege will get you by here). The owner is always + * treated as having all grant options. + */ + if (pg_class_ownercheck(relOid, GetUserId())) + my_goptions = ACL_ALL_RIGHTS_RELATION; + else + { + AclMode my_rights; + + my_rights = pg_class_aclmask(relOid, + GetUserId(), + ACL_ALL_RIGHTS_RELATION | ACL_GRANT_OPTION_FOR(ACL_ALL_RIGHTS_RELATION), + ACLMASK_ALL); + if (my_rights == ACL_NO_RIGHTS) + aclcheck_error(ACLCHECK_NO_PRIV, ACL_KIND_CLASS, + relvar->relname); + my_goptions = ACL_OPTION_TO_PRIVS(my_rights); + } + + /* + * Restrict the operation to what we can actually grant or revoke, + * and issue a warning if appropriate. (For REVOKE this isn't quite + * what the spec says to do: the spec seems to want a warning only + * if no privilege bits actually change in the ACL. In practice + * that behavior seems much too noisy, as well as inconsistent with + * the GRANT case.) + */ + this_privileges = privileges & my_goptions; + if (stmt->is_grant) + { + if (this_privileges == 0) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_GRANTED), + errmsg("no privileges were granted"))); + else if (!all_privs && this_privileges != privileges) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_GRANTED), + errmsg("not all privileges were granted"))); + } + else + { + if (this_privileges == 0) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_REVOKED), + errmsg("no privileges could be revoked"))); + else if (!all_privs && this_privileges != privileges) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_REVOKED), + errmsg("not all privileges could be revoked"))); + } + /* * If there's no ACL, substitute the proper default. */ @@ -298,7 +355,7 @@ ExecuteGrantStmt_Relation(GrantStmt *stmt) new_acl = merge_acl_with_grant(old_acl, stmt->is_grant, stmt->grant_option, stmt->behavior, - stmt->grantees, privileges, + stmt->grantees, this_privileges, grantorId, ownerId); /* finished building new ACL value, now insert it */ @@ -328,12 +385,17 @@ static void ExecuteGrantStmt_Database(GrantStmt *stmt) { AclMode privileges; + bool all_privs; ListCell *i; if (linitial_int(stmt->privileges) == ACL_ALL_RIGHTS) + { + all_privs = true; privileges = ACL_ALL_RIGHTS_DATABASE; + } else { + all_privs = false; privileges = ACL_NO_RIGHTS; foreach(i, stmt->privileges) { @@ -358,6 +420,8 @@ ExecuteGrantStmt_Database(GrantStmt *stmt) Form_pg_database pg_database_tuple; Datum aclDatum; bool isNull; + AclMode my_goptions; + AclMode this_privileges; Acl *old_acl; Acl *new_acl; AclId grantorId; @@ -383,12 +447,58 @@ ExecuteGrantStmt_Database(GrantStmt *stmt) ownerId = pg_database_tuple->datdba; grantorId = select_grantor(ownerId); - if (stmt->is_grant - && !pg_database_ownercheck(HeapTupleGetOid(tuple), GetUserId()) - && pg_database_aclcheck(HeapTupleGetOid(tuple), GetUserId(), - ACL_GRANT_OPTION_FOR(privileges)) != ACLCHECK_OK) - aclcheck_error(ACLCHECK_NO_PRIV, ACL_KIND_DATABASE, - NameStr(pg_database_tuple->datname)); + /* + * Must be owner or have some privilege on the object (per spec, + * any privilege will get you by here). The owner is always + * treated as having all grant options. + */ + if (pg_database_ownercheck(HeapTupleGetOid(tuple), GetUserId())) + my_goptions = ACL_ALL_RIGHTS_DATABASE; + else + { + AclMode my_rights; + + my_rights = pg_database_aclmask(HeapTupleGetOid(tuple), + GetUserId(), + ACL_ALL_RIGHTS_DATABASE | ACL_GRANT_OPTION_FOR(ACL_ALL_RIGHTS_DATABASE), + ACLMASK_ALL); + if (my_rights == ACL_NO_RIGHTS) + aclcheck_error(ACLCHECK_NO_PRIV, ACL_KIND_DATABASE, + NameStr(pg_database_tuple->datname)); + my_goptions = ACL_OPTION_TO_PRIVS(my_rights); + } + + /* + * Restrict the operation to what we can actually grant or revoke, + * and issue a warning if appropriate. (For REVOKE this isn't quite + * what the spec says to do: the spec seems to want a warning only + * if no privilege bits actually change in the ACL. In practice + * that behavior seems much too noisy, as well as inconsistent with + * the GRANT case.) + */ + this_privileges = privileges & my_goptions; + if (stmt->is_grant) + { + if (this_privileges == 0) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_GRANTED), + errmsg("no privileges were granted"))); + else if (!all_privs && this_privileges != privileges) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_GRANTED), + errmsg("not all privileges were granted"))); + } + else + { + if (this_privileges == 0) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_REVOKED), + errmsg("no privileges could be revoked"))); + else if (!all_privs && this_privileges != privileges) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_REVOKED), + errmsg("not all privileges could be revoked"))); + } /* * If there's no ACL, substitute the proper default. @@ -403,7 +513,7 @@ ExecuteGrantStmt_Database(GrantStmt *stmt) new_acl = merge_acl_with_grant(old_acl, stmt->is_grant, stmt->grant_option, stmt->behavior, - stmt->grantees, privileges, + stmt->grantees, this_privileges, grantorId, ownerId); /* finished building new ACL value, now insert it */ @@ -433,12 +543,17 @@ static void ExecuteGrantStmt_Function(GrantStmt *stmt) { AclMode privileges; + bool all_privs; ListCell *i; if (linitial_int(stmt->privileges) == ACL_ALL_RIGHTS) + { + all_privs = true; privileges = ACL_ALL_RIGHTS_FUNCTION; + } else { + all_privs = false; privileges = ACL_NO_RIGHTS; foreach(i, stmt->privileges) { @@ -462,6 +577,8 @@ ExecuteGrantStmt_Function(GrantStmt *stmt) Form_pg_proc pg_proc_tuple; Datum aclDatum; bool isNull; + AclMode my_goptions; + AclMode this_privileges; Acl *old_acl; Acl *new_acl; AclId grantorId; @@ -484,12 +601,58 @@ ExecuteGrantStmt_Function(GrantStmt *stmt) ownerId = pg_proc_tuple->proowner; grantorId = select_grantor(ownerId); - if (stmt->is_grant - && !pg_proc_ownercheck(oid, GetUserId()) - && pg_proc_aclcheck(oid, GetUserId(), - ACL_GRANT_OPTION_FOR(privileges)) != ACLCHECK_OK) - aclcheck_error(ACLCHECK_NO_PRIV, ACL_KIND_PROC, - NameStr(pg_proc_tuple->proname)); + /* + * Must be owner or have some privilege on the object (per spec, + * any privilege will get you by here). The owner is always + * treated as having all grant options. + */ + if (pg_proc_ownercheck(oid, GetUserId())) + my_goptions = ACL_ALL_RIGHTS_FUNCTION; + else + { + AclMode my_rights; + + my_rights = pg_proc_aclmask(oid, + GetUserId(), + ACL_ALL_RIGHTS_FUNCTION | ACL_GRANT_OPTION_FOR(ACL_ALL_RIGHTS_FUNCTION), + ACLMASK_ALL); + if (my_rights == ACL_NO_RIGHTS) + aclcheck_error(ACLCHECK_NO_PRIV, ACL_KIND_PROC, + NameStr(pg_proc_tuple->proname)); + my_goptions = ACL_OPTION_TO_PRIVS(my_rights); + } + + /* + * Restrict the operation to what we can actually grant or revoke, + * and issue a warning if appropriate. (For REVOKE this isn't quite + * what the spec says to do: the spec seems to want a warning only + * if no privilege bits actually change in the ACL. In practice + * that behavior seems much too noisy, as well as inconsistent with + * the GRANT case.) + */ + this_privileges = privileges & my_goptions; + if (stmt->is_grant) + { + if (this_privileges == 0) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_GRANTED), + errmsg("no privileges were granted"))); + else if (!all_privs && this_privileges != privileges) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_GRANTED), + errmsg("not all privileges were granted"))); + } + else + { + if (this_privileges == 0) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_REVOKED), + errmsg("no privileges could be revoked"))); + else if (!all_privs && this_privileges != privileges) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_REVOKED), + errmsg("not all privileges could be revoked"))); + } /* * If there's no ACL, substitute the proper default. @@ -504,7 +667,7 @@ ExecuteGrantStmt_Function(GrantStmt *stmt) new_acl = merge_acl_with_grant(old_acl, stmt->is_grant, stmt->grant_option, stmt->behavior, - stmt->grantees, privileges, + stmt->grantees, this_privileges, grantorId, ownerId); /* finished building new ACL value, now insert it */ @@ -534,12 +697,17 @@ static void ExecuteGrantStmt_Language(GrantStmt *stmt) { AclMode privileges; + bool all_privs; ListCell *i; if (linitial_int(stmt->privileges) == ACL_ALL_RIGHTS) + { + all_privs = true; privileges = ACL_ALL_RIGHTS_LANGUAGE; + } else { + all_privs = false; privileges = ACL_NO_RIGHTS; foreach(i, stmt->privileges) { @@ -562,6 +730,8 @@ ExecuteGrantStmt_Language(GrantStmt *stmt) Form_pg_language pg_language_tuple; Datum aclDatum; bool isNull; + AclMode my_goptions; + AclMode this_privileges; Acl *old_acl; Acl *new_acl; AclId grantorId; @@ -581,6 +751,11 @@ ExecuteGrantStmt_Language(GrantStmt *stmt) errmsg("language \"%s\" does not exist", langname))); pg_language_tuple = (Form_pg_language) GETSTRUCT(tuple); + if (!pg_language_tuple->lanpltrusted) + ereport(ERROR, + (errcode(ERRCODE_WRONG_OBJECT_TYPE), + errmsg("language \"%s\" is not trusted", langname))); + /* * Note: for now, languages are treated as owned by the bootstrap * user. We should add an owner column to pg_language instead. @@ -588,17 +763,58 @@ ExecuteGrantStmt_Language(GrantStmt *stmt) ownerId = BOOTSTRAP_USESYSID; grantorId = select_grantor(ownerId); - if (stmt->is_grant - && !superuser() /* XXX no ownercheck() available */ - && pg_language_aclcheck(HeapTupleGetOid(tuple), GetUserId(), - ACL_GRANT_OPTION_FOR(privileges)) != ACLCHECK_OK) - aclcheck_error(ACLCHECK_NO_PRIV, ACL_KIND_LANGUAGE, - NameStr(pg_language_tuple->lanname)); + /* + * Must be owner or have some privilege on the object (per spec, + * any privilege will get you by here). The owner is always + * treated as having all grant options. + */ + if (superuser()) /* XXX no ownercheck() available */ + my_goptions = ACL_ALL_RIGHTS_LANGUAGE; + else + { + AclMode my_rights; + + my_rights = pg_language_aclmask(HeapTupleGetOid(tuple), + GetUserId(), + ACL_ALL_RIGHTS_LANGUAGE | ACL_GRANT_OPTION_FOR(ACL_ALL_RIGHTS_LANGUAGE), + ACLMASK_ALL); + if (my_rights == ACL_NO_RIGHTS) + aclcheck_error(ACLCHECK_NO_PRIV, ACL_KIND_LANGUAGE, + NameStr(pg_language_tuple->lanname)); + my_goptions = ACL_OPTION_TO_PRIVS(my_rights); + } - if (!pg_language_tuple->lanpltrusted) - ereport(ERROR, - (errcode(ERRCODE_WRONG_OBJECT_TYPE), - errmsg("language \"%s\" is not trusted", langname))); + /* + * Restrict the operation to what we can actually grant or revoke, + * and issue a warning if appropriate. (For REVOKE this isn't quite + * what the spec says to do: the spec seems to want a warning only + * if no privilege bits actually change in the ACL. In practice + * that behavior seems much too noisy, as well as inconsistent with + * the GRANT case.) + */ + this_privileges = privileges & my_goptions; + if (stmt->is_grant) + { + if (this_privileges == 0) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_GRANTED), + errmsg("no privileges were granted"))); + else if (!all_privs && this_privileges != privileges) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_GRANTED), + errmsg("not all privileges were granted"))); + } + else + { + if (this_privileges == 0) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_REVOKED), + errmsg("no privileges could be revoked"))); + else if (!all_privs && this_privileges != privileges) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_REVOKED), + errmsg("not all privileges could be revoked"))); + } /* * If there's no ACL, substitute the proper default. @@ -613,7 +829,7 @@ ExecuteGrantStmt_Language(GrantStmt *stmt) new_acl = merge_acl_with_grant(old_acl, stmt->is_grant, stmt->grant_option, stmt->behavior, - stmt->grantees, privileges, + stmt->grantees, this_privileges, grantorId, ownerId); /* finished building new ACL value, now insert it */ @@ -643,12 +859,17 @@ static void ExecuteGrantStmt_Namespace(GrantStmt *stmt) { AclMode privileges; + bool all_privs; ListCell *i; if (linitial_int(stmt->privileges) == ACL_ALL_RIGHTS) + { + all_privs = true; privileges = ACL_ALL_RIGHTS_NAMESPACE; + } else { + all_privs = false; privileges = ACL_NO_RIGHTS; foreach(i, stmt->privileges) { @@ -671,6 +892,8 @@ ExecuteGrantStmt_Namespace(GrantStmt *stmt) Form_pg_namespace pg_namespace_tuple; Datum aclDatum; bool isNull; + AclMode my_goptions; + AclMode this_privileges; Acl *old_acl; Acl *new_acl; AclId grantorId; @@ -693,12 +916,58 @@ ExecuteGrantStmt_Namespace(GrantStmt *stmt) ownerId = pg_namespace_tuple->nspowner; grantorId = select_grantor(ownerId); - if (stmt->is_grant - && !pg_namespace_ownercheck(HeapTupleGetOid(tuple), GetUserId()) - && pg_namespace_aclcheck(HeapTupleGetOid(tuple), GetUserId(), - ACL_GRANT_OPTION_FOR(privileges)) != ACLCHECK_OK) - aclcheck_error(ACLCHECK_NO_PRIV, ACL_KIND_NAMESPACE, - nspname); + /* + * Must be owner or have some privilege on the object (per spec, + * any privilege will get you by here). The owner is always + * treated as having all grant options. + */ + if (pg_namespace_ownercheck(HeapTupleGetOid(tuple), GetUserId())) + my_goptions = ACL_ALL_RIGHTS_NAMESPACE; + else + { + AclMode my_rights; + + my_rights = pg_namespace_aclmask(HeapTupleGetOid(tuple), + GetUserId(), + ACL_ALL_RIGHTS_NAMESPACE | ACL_GRANT_OPTION_FOR(ACL_ALL_RIGHTS_NAMESPACE), + ACLMASK_ALL); + if (my_rights == ACL_NO_RIGHTS) + aclcheck_error(ACLCHECK_NO_PRIV, ACL_KIND_NAMESPACE, + nspname); + my_goptions = ACL_OPTION_TO_PRIVS(my_rights); + } + + /* + * Restrict the operation to what we can actually grant or revoke, + * and issue a warning if appropriate. (For REVOKE this isn't quite + * what the spec says to do: the spec seems to want a warning only + * if no privilege bits actually change in the ACL. In practice + * that behavior seems much too noisy, as well as inconsistent with + * the GRANT case.) + */ + this_privileges = privileges & my_goptions; + if (stmt->is_grant) + { + if (this_privileges == 0) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_GRANTED), + errmsg("no privileges were granted"))); + else if (!all_privs && this_privileges != privileges) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_GRANTED), + errmsg("not all privileges were granted"))); + } + else + { + if (this_privileges == 0) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_REVOKED), + errmsg("no privileges could be revoked"))); + else if (!all_privs && this_privileges != privileges) + ereport(WARNING, + (errcode(ERRCODE_WARNING_PRIVILEGE_NOT_REVOKED), + errmsg("not all privileges could be revoked"))); + } /* * If there's no ACL, substitute the proper default. @@ -714,7 +983,7 @@ ExecuteGrantStmt_Namespace(GrantStmt *stmt) new_acl = merge_acl_with_grant(old_acl, stmt->is_grant, stmt->grant_option, stmt->behavior, - stmt->grantees, privileges, + stmt->grantees, this_privileges, grantorId, ownerId); /* finished building new ACL value, now insert it */ @@ -816,147 +1085,6 @@ get_groname(AclId grosysid) return name; } -/* - * Is user a member of group? - */ -static bool -in_group(AclId uid, AclId gid) -{ - bool result = false; - HeapTuple tuple; - Datum att; - bool isNull; - IdList *glist; - AclId *aidp; - int i, - num; - - tuple = SearchSysCache(GROSYSID, - ObjectIdGetDatum(gid), - 0, 0, 0); - if (HeapTupleIsValid(tuple)) - { - att = SysCacheGetAttr(GROSYSID, - tuple, - Anum_pg_group_grolist, - &isNull); - if (!isNull) - { - /* be sure the IdList is not toasted */ - glist = DatumGetIdListP(att); - /* scan it */ - num = IDLIST_NUM(glist); - aidp = IDLIST_DAT(glist); - for (i = 0; i < num; ++i) - { - if (aidp[i] == uid) - { - result = true; - break; - } - } - /* if IdList was toasted, free detoasted copy */ - if ((Pointer) glist != DatumGetPointer(att)) - pfree(glist); - } - ReleaseSysCache(tuple); - } - else - ereport(WARNING, - (errcode(ERRCODE_UNDEFINED_OBJECT), - errmsg("group with ID %u does not exist", gid))); - return result; -} - - -/* - * aclmask --- compute bitmask of all privileges held by userid. - * - * When 'how' = ACLMASK_ALL, this simply returns the privilege bits - * held by the given userid according to the given ACL list, ANDed - * with 'mask'. (The point of passing 'mask' is to let the routine - * exit early if all privileges of interest have been found.) - * - * When 'how' = ACLMASK_ANY, returns as soon as any bit in the mask - * is known true. (This lets us exit soonest in cases where the - * caller is only going to test for zero or nonzero result.) - * - * Usage patterns: - * - * To see if any of a set of privileges are held: - * if (aclmask(acl, userid, privs, ACLMASK_ANY) != 0) - * - * To see if all of a set of privileges are held: - * if (aclmask(acl, userid, privs, ACLMASK_ALL) == privs) - * - * To determine exactly which of a set of privileges are held: - * heldprivs = aclmask(acl, userid, privs, ACLMASK_ALL); - */ -static AclMode -aclmask(Acl *acl, AclId userid, AclMode mask, AclMaskHow how) -{ - AclMode result; - AclMode remaining; - AclItem *aidat; - int i, - num; - - /* - * Null ACL should not happen, since caller should have inserted - * appropriate default - */ - if (acl == NULL) - elog(ERROR, "null ACL"); - - /* Quick exit for mask == 0 */ - if (mask == 0) - return 0; - - num = ACL_NUM(acl); - aidat = ACL_DAT(acl); - - result = 0; - - /* - * Check privileges granted directly to user or to public - */ - for (i = 0; i < num; i++) - { - AclItem *aidata = &aidat[i]; - - if (ACLITEM_GET_IDTYPE(*aidata) == ACL_IDTYPE_WORLD - || (ACLITEM_GET_IDTYPE(*aidata) == ACL_IDTYPE_UID - && aidata->ai_grantee == userid)) - { - result |= (aidata->ai_privs & mask); - if ((how == ACLMASK_ALL) ? (result == mask) : (result != 0)) - return result; - } - } - - /* - * Check privileges granted via groups. We do this in a separate - * pass to minimize expensive lookups in pg_group. - */ - remaining = (mask & ~result); - for (i = 0; i < num; i++) - { - AclItem *aidata = &aidat[i]; - - if (ACLITEM_GET_IDTYPE(*aidata) == ACL_IDTYPE_GID - && (aidata->ai_privs & remaining) - && in_group(userid, aidata->ai_grantee)) - { - result |= (aidata->ai_privs & mask); - if ((how == ACLMASK_ALL) ? (result == mask) : (result != 0)) - return result; - remaining = (mask & ~result); - } - } - - return result; -} - /* * Standardized reporting of aclcheck permissions failures. @@ -1058,6 +1186,7 @@ pg_class_aclmask(Oid table_oid, AclId userid, Datum aclDatum; bool isNull; Acl *acl; + AclId ownerId; /* * Validate userid, find out if he is superuser, also get usecatupd @@ -1125,13 +1254,13 @@ pg_class_aclmask(Oid table_oid, AclId userid, /* * Normal case: get the relation's ACL from pg_class */ + ownerId = classForm->relowner; + aclDatum = SysCacheGetAttr(RELOID, tuple, Anum_pg_class_relacl, &isNull); if (isNull) { /* No ACL, so build default ACL */ - AclId ownerId = classForm->relowner; - acl = acldefault(ACL_OBJECT_RELATION, ownerId); aclDatum = (Datum) 0; } @@ -1141,7 +1270,7 @@ pg_class_aclmask(Oid table_oid, AclId userid, acl = DatumGetAclP(aclDatum); } - result = aclmask(acl, userid, mask, how); + result = aclmask(acl, userid, ownerId, mask, how); /* if we have a detoasted copy, free it */ if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) @@ -1167,6 +1296,7 @@ pg_database_aclmask(Oid db_oid, AclId userid, Datum aclDatum; bool isNull; Acl *acl; + AclId ownerId; /* Superusers bypass all permission checking. */ if (superuser_arg(userid)) @@ -1189,15 +1319,14 @@ pg_database_aclmask(Oid db_oid, AclId userid, (errcode(ERRCODE_UNDEFINED_DATABASE), errmsg("database with OID %u does not exist", db_oid))); + ownerId = ((Form_pg_database) GETSTRUCT(tuple))->datdba; + aclDatum = heap_getattr(tuple, Anum_pg_database_datacl, RelationGetDescr(pg_database), &isNull); if (isNull) { /* No ACL, so build default ACL */ - AclId ownerId; - - ownerId = ((Form_pg_database) GETSTRUCT(tuple))->datdba; acl = acldefault(ACL_OBJECT_DATABASE, ownerId); aclDatum = (Datum) 0; } @@ -1207,7 +1336,7 @@ pg_database_aclmask(Oid db_oid, AclId userid, acl = DatumGetAclP(aclDatum); } - result = aclmask(acl, userid, mask, how); + result = aclmask(acl, userid, ownerId, mask, how); /* if we have a detoasted copy, free it */ if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) @@ -1231,6 +1360,7 @@ pg_proc_aclmask(Oid proc_oid, AclId userid, Datum aclDatum; bool isNull; Acl *acl; + AclId ownerId; /* Superusers bypass all permission checking. */ if (superuser_arg(userid)) @@ -1247,14 +1377,13 @@ pg_proc_aclmask(Oid proc_oid, AclId userid, (errcode(ERRCODE_UNDEFINED_FUNCTION), errmsg("function with OID %u does not exist", proc_oid))); + ownerId = ((Form_pg_proc) GETSTRUCT(tuple))->proowner; + aclDatum = SysCacheGetAttr(PROCOID, tuple, Anum_pg_proc_proacl, &isNull); if (isNull) { /* No ACL, so build default ACL */ - AclId ownerId; - - ownerId = ((Form_pg_proc) GETSTRUCT(tuple))->proowner; acl = acldefault(ACL_OBJECT_FUNCTION, ownerId); aclDatum = (Datum) 0; } @@ -1264,7 +1393,7 @@ pg_proc_aclmask(Oid proc_oid, AclId userid, acl = DatumGetAclP(aclDatum); } - result = aclmask(acl, userid, mask, how); + result = aclmask(acl, userid, ownerId, mask, how); /* if we have a detoasted copy, free it */ if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) @@ -1287,6 +1416,7 @@ pg_language_aclmask(Oid lang_oid, AclId userid, Datum aclDatum; bool isNull; Acl *acl; + AclId ownerId; /* Superusers bypass all permission checking. */ if (superuser_arg(userid)) @@ -1303,13 +1433,15 @@ pg_language_aclmask(Oid lang_oid, AclId userid, (errcode(ERRCODE_UNDEFINED_OBJECT), errmsg("language with OID %u does not exist", lang_oid))); + /* XXX pg_language should have an owner column, but doesn't */ + ownerId = BOOTSTRAP_USESYSID; + aclDatum = SysCacheGetAttr(LANGOID, tuple, Anum_pg_language_lanacl, &isNull); if (isNull) { /* No ACL, so build default ACL */ - /* XXX pg_language should have an owner column, but doesn't */ - acl = acldefault(ACL_OBJECT_LANGUAGE, BOOTSTRAP_USESYSID); + acl = acldefault(ACL_OBJECT_LANGUAGE, ownerId); aclDatum = (Datum) 0; } else @@ -1318,7 +1450,7 @@ pg_language_aclmask(Oid lang_oid, AclId userid, acl = DatumGetAclP(aclDatum); } - result = aclmask(acl, userid, mask, how); + result = aclmask(acl, userid, ownerId, mask, how); /* if we have a detoasted copy, free it */ if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) @@ -1341,6 +1473,7 @@ pg_namespace_aclmask(Oid nsp_oid, AclId userid, Datum aclDatum; bool isNull; Acl *acl; + AclId ownerId; /* Superusers bypass all permission checking. */ if (superuser_arg(userid)) @@ -1385,14 +1518,13 @@ pg_namespace_aclmask(Oid nsp_oid, AclId userid, (errcode(ERRCODE_UNDEFINED_SCHEMA), errmsg("schema with OID %u does not exist", nsp_oid))); + ownerId = ((Form_pg_namespace) GETSTRUCT(tuple))->nspowner; + aclDatum = SysCacheGetAttr(NAMESPACEOID, tuple, Anum_pg_namespace_nspacl, &isNull); if (isNull) { /* No ACL, so build default ACL */ - AclId ownerId; - - ownerId = ((Form_pg_namespace) GETSTRUCT(tuple))->nspowner; acl = acldefault(ACL_OBJECT_NAMESPACE, ownerId); aclDatum = (Datum) 0; } @@ -1402,7 +1534,7 @@ pg_namespace_aclmask(Oid nsp_oid, AclId userid, acl = DatumGetAclP(aclDatum); } - result = aclmask(acl, userid, mask, how); + result = aclmask(acl, userid, ownerId, mask, how); /* if we have a detoasted copy, free it */ if (acl && (Pointer) acl != DatumGetPointer(aclDatum)) diff --git a/src/backend/utils/adt/acl.c b/src/backend/utils/adt/acl.c index 214bda2245..d02683245a 100644 --- a/src/backend/utils/adt/acl.c +++ b/src/backend/utils/adt/acl.c @@ -8,7 +8,7 @@ * * * IDENTIFICATION - * $PostgreSQL: pgsql/src/backend/utils/adt/acl.c,v 1.104 2004/05/07 00:24:57 tgl Exp $ + * $PostgreSQL: pgsql/src/backend/utils/adt/acl.c,v 1.105 2004/06/01 21:49:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -17,6 +17,7 @@ #include #include "catalog/namespace.h" +#include "catalog/pg_group.h" #include "catalog/pg_shadow.h" #include "catalog/pg_type.h" #include "commands/dbcommands.h" @@ -35,8 +36,11 @@ static void putid(char *p, const char *s); static Acl *allocacl(int n); static const char *aclparse(const char *s, AclItem *aip); static bool aclitem_match(const AclItem *a1, const AclItem *a2); -static Acl *recursive_revoke(Acl *acl, AclId grantee, - AclMode revoke_privs, DropBehavior behavior); +static void check_circularity(const Acl *old_acl, const AclItem *mod_aip, + AclId ownerid); +static Acl *recursive_revoke(Acl *acl, AclId grantee, AclMode revoke_privs, + AclId ownerid, DropBehavior behavior); +static bool in_group(AclId uid, AclId gid); static AclMode convert_priv_string(text *priv_type_text); @@ -554,10 +558,19 @@ acldefault(GrantObjectType objtype, AclId ownerid) aip++; } + /* + * Note that the owner's entry shows all ordinary privileges but no + * grant options. This is because his grant options come "from the + * system" and not from his own efforts. (The SQL spec says that + * the owner's rights come from a "_SYSTEM" authid.) However, we do + * consider that the owner's ordinary privileges are self-granted; + * this lets him revoke them. We implement the owner's grant options + * without any explicit "_SYSTEM"-like ACL entry, by internally + * special-casing the owner whereever we are testing grant options. + */ aip->ai_grantee = ownerid; aip->ai_grantor = ownerid; - /* owner gets default privileges with grant option */ - ACLITEM_SET_PRIVS_IDTYPE(*aip, owner_default, owner_default, + ACLITEM_SET_PRIVS_IDTYPE(*aip, owner_default, ACL_NO_RIGHTS, ACL_IDTYPE_UID); return acl; @@ -565,21 +578,31 @@ acldefault(GrantObjectType objtype, AclId ownerid) /* - * Add or replace an item in an ACL array. The result is a modified copy; - * the input object is not changed. + * Update an ACL array to add or remove specified privileges. + * + * old_acl: the input ACL array + * mod_aip: defines the privileges to be added, removed, or substituted + * modechg: ACL_MODECHG_ADD, ACL_MODECHG_DEL, or ACL_MODECHG_EQL + * ownerid: AclId of object owner + * behavior: RESTRICT or CASCADE behavior for recursive removal + * + * ownerid and behavior are only relevant when the update operation specifies + * deletion of grant options. + * + * The result is a modified copy; the input object is not changed. * * NB: caller is responsible for having detoasted the input ACL, if needed. */ Acl * -aclinsert3(const Acl *old_acl, const AclItem *mod_aip, - unsigned modechg, DropBehavior behavior) +aclupdate(const Acl *old_acl, const AclItem *mod_aip, + int modechg, AclId ownerid, DropBehavior behavior) { Acl *new_acl = NULL; AclItem *old_aip, *new_aip = NULL; - AclMode old_privs, + AclMode old_rights, old_goptions, - new_privs, + new_rights, new_goptions; int dst, num; @@ -590,10 +613,15 @@ aclinsert3(const Acl *old_acl, const AclItem *mod_aip, if (!mod_aip) { new_acl = allocacl(ACL_NUM(old_acl)); - memcpy((char *) new_acl, (char *) old_acl, ACL_SIZE(old_acl)); + memcpy(new_acl, old_acl, ACL_SIZE(old_acl)); return new_acl; } + /* If granting grant options, check for circularity */ + if (modechg != ACL_MODECHG_DEL && + ACLITEM_GET_GOPTIONS(*mod_aip) != ACL_NO_RIGHTS) + check_circularity(old_acl, mod_aip, ownerid); + num = ACL_NUM(old_acl); old_aip = ACL_DAT(old_acl); @@ -626,44 +654,39 @@ aclinsert3(const Acl *old_acl, const AclItem *mod_aip, /* initialize the new entry with no permissions */ new_aip[dst].ai_grantee = mod_aip->ai_grantee; new_aip[dst].ai_grantor = mod_aip->ai_grantor; - ACLITEM_SET_PRIVS_IDTYPE(new_aip[dst], ACL_NO_RIGHTS, ACL_NO_RIGHTS, + ACLITEM_SET_PRIVS_IDTYPE(new_aip[dst], + ACL_NO_RIGHTS, ACL_NO_RIGHTS, ACLITEM_GET_IDTYPE(*mod_aip)); num++; /* set num to the size of new_acl */ } - old_privs = ACLITEM_GET_PRIVS(new_aip[dst]); + old_rights = ACLITEM_GET_RIGHTS(new_aip[dst]); old_goptions = ACLITEM_GET_GOPTIONS(new_aip[dst]); /* apply the specified permissions change */ switch (modechg) { case ACL_MODECHG_ADD: - ACLITEM_SET_PRIVS(new_aip[dst], - old_privs | ACLITEM_GET_PRIVS(*mod_aip)); - ACLITEM_SET_GOPTIONS(new_aip[dst], - old_goptions | ACLITEM_GET_GOPTIONS(*mod_aip)); + ACLITEM_SET_RIGHTS(new_aip[dst], + old_rights | ACLITEM_GET_RIGHTS(*mod_aip)); break; case ACL_MODECHG_DEL: - ACLITEM_SET_PRIVS(new_aip[dst], - old_privs & ~ACLITEM_GET_PRIVS(*mod_aip)); - ACLITEM_SET_GOPTIONS(new_aip[dst], - old_goptions & ~ACLITEM_GET_GOPTIONS(*mod_aip)); + ACLITEM_SET_RIGHTS(new_aip[dst], + old_rights & ~ACLITEM_GET_RIGHTS(*mod_aip)); break; case ACL_MODECHG_EQL: - ACLITEM_SET_PRIVS_IDTYPE(new_aip[dst], - ACLITEM_GET_PRIVS(*mod_aip), - ACLITEM_GET_GOPTIONS(*mod_aip), - ACLITEM_GET_IDTYPE(new_aip[dst])); + ACLITEM_SET_RIGHTS(new_aip[dst], + ACLITEM_GET_RIGHTS(*mod_aip)); break; } - new_privs = ACLITEM_GET_PRIVS(new_aip[dst]); + new_rights = ACLITEM_GET_RIGHTS(new_aip[dst]); new_goptions = ACLITEM_GET_GOPTIONS(new_aip[dst]); /* * If the adjusted entry has no permissions, delete it from the list. */ - if (new_privs == ACL_NO_RIGHTS && new_goptions == ACL_NO_RIGHTS) + if (new_rights == ACL_NO_RIGHTS) { memmove(new_aip + dst, new_aip + dst + 1, @@ -676,40 +699,143 @@ aclinsert3(const Acl *old_acl, const AclItem *mod_aip, * Remove abandoned privileges (cascading revoke). Currently we * can only handle this when the grantee is a user. */ - if ((old_goptions & ~new_goptions) != 0 - && ACLITEM_GET_IDTYPE(*mod_aip) == ACL_IDTYPE_UID) + if ((old_goptions & ~new_goptions) != 0) + { + Assert(ACLITEM_GET_IDTYPE(*mod_aip) == ACL_IDTYPE_UID); new_acl = recursive_revoke(new_acl, mod_aip->ai_grantee, (old_goptions & ~new_goptions), - behavior); + ownerid, behavior); + } return new_acl; } /* + * When granting grant options, we must disallow attempts to set up circular + * chains of grant options. Suppose A (the object owner) grants B some + * privileges with grant option, and B re-grants them to C. If C could + * grant the privileges to B as well, then A would be unable to effectively + * revoke the privileges from B, since recursive_revoke would consider that + * B still has 'em from C. + * + * We check for this by recursively deleting all grant options belonging to + * the target grantee, and then seeing if the would-be grantor still has the + * grant option or not. + */ +static void +check_circularity(const Acl *old_acl, const AclItem *mod_aip, + AclId ownerid) +{ + Acl *acl; + AclItem *aip; + int i, + num; + AclMode own_privs; + + /* + * For now, grant options can only be granted to users, not groups or + * PUBLIC. Otherwise we'd have to work a bit harder here. + */ + Assert(ACLITEM_GET_IDTYPE(*mod_aip) == ACL_IDTYPE_UID); + + /* The owner always has grant options, no need to check */ + if (mod_aip->ai_grantor == ownerid) + return; + + /* Make a working copy */ + acl = allocacl(ACL_NUM(old_acl)); + memcpy(acl, old_acl, ACL_SIZE(old_acl)); + + /* Zap all grant options of target grantee, plus what depends on 'em */ +cc_restart: + num = ACL_NUM(acl); + aip = ACL_DAT(acl); + for (i = 0; i < num; i++) + { + if (ACLITEM_GET_IDTYPE(aip[i]) == ACL_IDTYPE_UID && + aip[i].ai_grantee == mod_aip->ai_grantee && + ACLITEM_GET_GOPTIONS(aip[i]) != ACL_NO_RIGHTS) + { + Acl *new_acl; + + /* We'll actually zap ordinary privs too, but no matter */ + new_acl = aclupdate(acl, &aip[i], ACL_MODECHG_DEL, + ownerid, DROP_CASCADE); + + pfree(acl); + acl = new_acl; + + goto cc_restart; + } + } + + /* Now we can compute grantor's independently-derived privileges */ + own_privs = aclmask(acl, + mod_aip->ai_grantor, + ownerid, + ACL_GRANT_OPTION_FOR(ACLITEM_GET_GOPTIONS(*mod_aip)), + ACLMASK_ALL); + own_privs = ACL_OPTION_TO_PRIVS(own_privs); + + 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"))); + + pfree(acl); +} + + +/* * Ensure that no privilege is "abandoned". A privilege is abandoned * if the user that granted the privilege loses the grant option. (So * the chain through which it was granted is broken.) Either the * abandoned privileges are revoked as well, or an error message is * printed, depending on the drop behavior option. + * + * acl: the input ACL list + * grantee: the user from whom some grant options have been revoked + * revoke_privs: the grant options being revoked + * ownerid: AclId of object owner + * behavior: RESTRICT or CASCADE behavior for recursive removal + * + * The input Acl object is pfree'd if replaced. */ static Acl * recursive_revoke(Acl *acl, AclId grantee, AclMode revoke_privs, + AclId ownerid, DropBehavior behavior) { - int i; + AclMode still_has; + AclItem *aip; + int i, + num; + + /* The owner can never truly lose grant options, so short-circuit */ + if (grantee == ownerid) + return acl; + + /* The grantee might still have the privileges via another grantor */ + still_has = aclmask(acl, grantee, ownerid, + ACL_GRANT_OPTION_FOR(revoke_privs), + ACLMASK_ALL); + revoke_privs &= ~still_has; + if (revoke_privs == ACL_NO_RIGHTS) + return acl; restart: - for (i = 0; i < ACL_NUM(acl); i++) + num = ACL_NUM(acl); + aip = ACL_DAT(acl); + for (i = 0; i < num; i++) { - AclItem *aip = ACL_DAT(acl); - if (aip[i].ai_grantor == grantee && (ACLITEM_GET_PRIVS(aip[i]) & revoke_privs) != 0) { AclItem mod_acl; + Acl *new_acl; if (behavior == DROP_RESTRICT) ereport(ERROR, @@ -724,7 +850,12 @@ restart: revoke_privs, ACLITEM_GET_IDTYPE(aip[i])); - acl = aclinsert3(acl, &mod_acl, ACL_MODECHG_DEL, behavior); + new_acl = aclupdate(acl, &mod_acl, ACL_MODECHG_DEL, + ownerid, behavior); + + pfree(acl); + acl = new_acl; + goto restart; } } @@ -734,70 +865,177 @@ restart: /* - * aclinsert (exported function) + * aclmask --- compute bitmask of all privileges held by userid. + * + * When 'how' = ACLMASK_ALL, this simply returns the privilege bits + * held by the given userid according to the given ACL list, ANDed + * with 'mask'. (The point of passing 'mask' is to let the routine + * exit early if all privileges of interest have been found.) + * + * When 'how' = ACLMASK_ANY, returns as soon as any bit in the mask + * is known true. (This lets us exit soonest in cases where the + * caller is only going to test for zero or nonzero result.) + * + * Usage patterns: + * + * To see if any of a set of privileges are held: + * if (aclmask(acl, userid, ownerid, privs, ACLMASK_ANY) != 0) + * + * To see if all of a set of privileges are held: + * if (aclmask(acl, userid, ownerid, privs, ACLMASK_ALL) == privs) + * + * To determine exactly which of a set of privileges are held: + * heldprivs = aclmask(acl, userid, ownerid, privs, ACLMASK_ALL); */ -Datum -aclinsert(PG_FUNCTION_ARGS) +AclMode +aclmask(const Acl *acl, AclId userid, AclId ownerid, + AclMode mask, AclMaskHow how) { - Acl *old_acl = PG_GETARG_ACL_P(0); - AclItem *mod_aip = PG_GETARG_ACLITEM_P(1); + AclMode result; + AclMode remaining; + AclItem *aidat; + int i, + num; - PG_RETURN_ACL_P(aclinsert3(old_acl, mod_aip, ACL_MODECHG_EQL, DROP_CASCADE)); -} + /* + * Null ACL should not happen, since caller should have inserted + * appropriate default + */ + if (acl == NULL) + elog(ERROR, "null ACL"); -Datum -aclremove(PG_FUNCTION_ARGS) -{ - Acl *old_acl = PG_GETARG_ACL_P(0); - AclItem *mod_aip = PG_GETARG_ACLITEM_P(1); - Acl *new_acl; - AclItem *old_aip, - *new_aip; - int dst, - old_num, - new_num; + /* Quick exit for mask == 0 */ + if (mask == 0) + return 0; - /* These checks for null input should be dead code, but... */ - if (!old_acl || ACL_NUM(old_acl) < 0) - old_acl = allocacl(0); - if (!mod_aip) + result = 0; + + /* Owner always implicitly has all grant options */ + if (userid == ownerid) { - new_acl = allocacl(ACL_NUM(old_acl)); - memcpy((char *) new_acl, (char *) old_acl, ACL_SIZE(old_acl)); - PG_RETURN_ACL_P(new_acl); + result = mask & ACLITEM_ALL_GOPTION_BITS; + if (result == mask) + return result; } - old_num = ACL_NUM(old_acl); - old_aip = ACL_DAT(old_acl); + num = ACL_NUM(acl); + aidat = ACL_DAT(acl); + + /* + * Check privileges granted directly to user or to public + */ + for (i = 0; i < num; i++) + { + AclItem *aidata = &aidat[i]; - /* Search for the matching entry */ - for (dst = 0; - dst < old_num && !aclitem_match(mod_aip, old_aip + dst); - ++dst) - /* continue */ ; + if (ACLITEM_GET_IDTYPE(*aidata) == ACL_IDTYPE_WORLD + || (ACLITEM_GET_IDTYPE(*aidata) == ACL_IDTYPE_UID + && aidata->ai_grantee == userid)) + { + result |= (aidata->ai_privs & mask); + if ((how == ACLMASK_ALL) ? (result == mask) : (result != 0)) + return result; + } + } - if (dst >= old_num) + /* + * Check privileges granted via groups. We do this in a separate + * pass to minimize expensive lookups in pg_group. + */ + remaining = (mask & ~result); + for (i = 0; i < num; i++) { - /* Not found, so return copy of source ACL */ - new_acl = allocacl(old_num); - memcpy((char *) new_acl, (char *) old_acl, ACL_SIZE(old_acl)); + AclItem *aidata = &aidat[i]; + + if (ACLITEM_GET_IDTYPE(*aidata) == ACL_IDTYPE_GID + && (aidata->ai_privs & remaining) + && in_group(userid, aidata->ai_grantee)) + { + result |= (aidata->ai_privs & mask); + if ((how == ACLMASK_ALL) ? (result == mask) : (result != 0)) + return result; + remaining = (mask & ~result); + } } - else + + return result; +} + + +/* + * Is user a member of group? + */ +static bool +in_group(AclId uid, AclId gid) +{ + bool result = false; + HeapTuple tuple; + Datum att; + bool isNull; + IdList *glist; + AclId *aidp; + int i, + num; + + tuple = SearchSysCache(GROSYSID, + ObjectIdGetDatum(gid), + 0, 0, 0); + if (HeapTupleIsValid(tuple)) { - new_num = old_num - 1; - new_acl = allocacl(new_num); - new_aip = ACL_DAT(new_acl); - if (dst > 0) - memcpy((char *) new_aip, - (char *) old_aip, - dst * sizeof(AclItem)); - if (dst < new_num) - memcpy((char *) (new_aip + dst), - (char *) (old_aip + dst + 1), - (new_num - dst) * sizeof(AclItem)); + att = SysCacheGetAttr(GROSYSID, + tuple, + Anum_pg_group_grolist, + &isNull); + if (!isNull) + { + /* be sure the IdList is not toasted */ + glist = DatumGetIdListP(att); + /* scan it */ + num = IDLIST_NUM(glist); + aidp = IDLIST_DAT(glist); + for (i = 0; i < num; ++i) + { + if (aidp[i] == uid) + { + result = true; + break; + } + } + /* if IdList was toasted, free detoasted copy */ + if ((Pointer) glist != DatumGetPointer(att)) + pfree(glist); + } + ReleaseSysCache(tuple); } + else + ereport(WARNING, + (errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("group with ID %u does not exist", gid))); + return result; +} + + +/* + * aclinsert (exported function) + */ +Datum +aclinsert(PG_FUNCTION_ARGS) +{ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("aclinsert is no longer supported"))); + + PG_RETURN_NULL(); /* keep compiler quiet */ +} + +Datum +aclremove(PG_FUNCTION_ARGS) +{ + ereport(ERROR, + (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), + errmsg("aclremove is no longer supported"))); - PG_RETURN_ACL_P(new_acl); + PG_RETURN_NULL(); /* keep compiler quiet */ } Datum @@ -816,8 +1054,7 @@ aclcontains(PG_FUNCTION_ARGS) if (aip->ai_grantee == aidat[i].ai_grantee && ACLITEM_GET_IDTYPE(*aip) == ACLITEM_GET_IDTYPE(aidat[i]) && aip->ai_grantor == aidat[i].ai_grantor - && (ACLITEM_GET_PRIVS(*aip) & ACLITEM_GET_PRIVS(aidat[i])) == ACLITEM_GET_PRIVS(*aip) - && (ACLITEM_GET_GOPTIONS(*aip) & ACLITEM_GET_GOPTIONS(aidat[i])) == ACLITEM_GET_GOPTIONS(*aip)) + && (ACLITEM_GET_RIGHTS(*aip) & ACLITEM_GET_RIGHTS(aidat[i])) == ACLITEM_GET_RIGHTS(*aip)) PG_RETURN_BOOL(true); } PG_RETURN_BOOL(false); diff --git a/src/include/utils/acl.h b/src/include/utils/acl.h index 59131122cb..f5ac89e257 100644 --- a/src/include/utils/acl.h +++ b/src/include/utils/acl.h @@ -7,7 +7,7 @@ * Portions Copyright (c) 1996-2003, PostgreSQL Global Development Group * Portions Copyright (c) 1994, Regents of the University of California * - * $PostgreSQL: pgsql/src/include/utils/acl.h,v 1.69 2004/05/11 17:36:13 tgl Exp $ + * $PostgreSQL: pgsql/src/include/utils/acl.h,v 1.70 2004/06/01 21:49:22 tgl Exp $ * * NOTES * An ACL array is simply an array of AclItems, representing the union @@ -63,13 +63,16 @@ typedef struct AclItem /* * The AclIdType is stored in the top two bits of the ai_privs field * of an AclItem. The middle 15 bits are the grant option markers, - * and the lower 15 bits are the actual privileges. + * and the lower 15 bits are the actual privileges. We use "rights" + * to mean the combined grant option and privilege bits fields. */ #define ACLITEM_GET_PRIVS(item) ((item).ai_privs & 0x7FFF) #define ACLITEM_GET_GOPTIONS(item) (((item).ai_privs >> 15) & 0x7FFF) +#define ACLITEM_GET_RIGHTS(item) ((item).ai_privs & 0x3FFFFFFF) #define ACLITEM_GET_IDTYPE(item) ((item).ai_privs >> 30) #define ACL_GRANT_OPTION_FOR(privs) (((AclMode) (privs) & 0x7FFF) << 15) +#define ACL_OPTION_TO_PRIVS(privs) (((AclMode) (privs) >> 15) & 0x7FFF) #define ACLITEM_SET_PRIVS(item,privs) \ ((item).ai_privs = ((item).ai_privs & ~((AclMode) 0x7FFF)) | \ @@ -77,6 +80,9 @@ typedef struct AclItem #define ACLITEM_SET_GOPTIONS(item,goptions) \ ((item).ai_privs = ((item).ai_privs & ~(((AclMode) 0x7FFF) << 15)) | \ (((AclMode) (goptions) & 0x7FFF) << 15)) +#define ACLITEM_SET_RIGHTS(item,rights) \ + ((item).ai_privs = ((item).ai_privs & ~((AclMode) 0x3FFFFFFF)) | \ + ((AclMode) (rights) & 0x3FFFFFFF)) #define ACLITEM_SET_IDTYPE(item,idtype) \ ((item).ai_privs = ((item).ai_privs & ~(((AclMode) 0x03) << 30)) | \ (((AclMode) (idtype) & 0x03) << 30)) @@ -86,6 +92,8 @@ typedef struct AclItem (((AclMode) (goption) & 0x7FFF) << 15) | \ ((AclMode) (idtype) << 30)) +#define ACLITEM_ALL_PRIV_BITS ((AclMode) 0x7FFF) +#define ACLITEM_ALL_GOPTION_BITS ((AclMode) 0x7FFF << 15) /* * Definitions for convenient access to Acl (array of AclItem) and IdList @@ -143,7 +151,7 @@ typedef ArrayType IdList; /* - * ACL modification opcodes + * ACL modification opcodes for aclupdate */ #define ACL_MODECHG_ADD 1 #define ACL_MODECHG_DEL 2 @@ -212,8 +220,10 @@ typedef enum AclObjectKind * routines used internally */ extern Acl *acldefault(GrantObjectType objtype, AclId ownerid); -extern Acl *aclinsert3(const Acl *old_acl, const AclItem *mod_aip, - unsigned modechg, DropBehavior behavior); +extern Acl *aclupdate(const Acl *old_acl, const AclItem *mod_aip, + int modechg, AclId ownerid, DropBehavior behavior); +extern AclMode aclmask(const Acl *acl, AclId userid, AclId ownerid, + AclMode mask, AclMaskHow how); /* * SQL functions (from acl.c) diff --git a/src/include/utils/errcodes.h b/src/include/utils/errcodes.h index cdee2737f0..270eb2073b 100644 --- a/src/include/utils/errcodes.h +++ b/src/include/utils/errcodes.h @@ -11,7 +11,7 @@ * * Copyright (c) 2003, PostgreSQL Global Development Group * - * $PostgreSQL: pgsql/src/include/utils/errcodes.h,v 1.11 2004/05/16 23:18:55 neilc Exp $ + * $PostgreSQL: pgsql/src/include/utils/errcodes.h,v 1.12 2004/06/01 21:49:22 tgl Exp $ * *------------------------------------------------------------------------- */ @@ -59,6 +59,8 @@ #define ERRCODE_WARNING_DYNAMIC_RESULT_SETS_RETURNED MAKE_SQLSTATE('0','1', '0','0','C') #define ERRCODE_WARNING_IMPLICIT_ZERO_BIT_PADDING MAKE_SQLSTATE('0','1', '0','0','8') #define ERRCODE_WARNING_NULL_VALUE_ELIMINATED_IN_SET_FUNCTION MAKE_SQLSTATE('0','1', '0','0','3') +#define ERRCODE_WARNING_PRIVILEGE_NOT_GRANTED MAKE_SQLSTATE('0','1', '0','0','7') +#define ERRCODE_WARNING_PRIVILEGE_NOT_REVOKED MAKE_SQLSTATE('0','1', '0','0','6') #define ERRCODE_WARNING_STRING_DATA_RIGHT_TRUNCATION MAKE_SQLSTATE('0','1', '0','0','4') #define ERRCODE_WARNING_DEPRECATED_FEATURE MAKE_SQLSTATE('0','1', 'P','0','1') diff --git a/src/test/regress/expected/privileges.out b/src/test/regress/expected/privileges.out index f5a6c039d5..83903f6b97 100644 --- a/src/test/regress/expected/privileges.out +++ b/src/test/regress/expected/privileges.out @@ -89,7 +89,7 @@ ERROR: permission denied for relation atest2 COPY atest2 FROM stdin; -- fail ERROR: permission denied for relation atest2 GRANT ALL ON atest1 TO PUBLIC; -- fail -ERROR: permission denied for relation atest1 +WARNING: no privileges were granted -- checks in subquery, both ok SELECT * FROM atest1 WHERE ( b IN ( SELECT col1 FROM atest2 ) ); a | b @@ -225,7 +225,7 @@ GRANT USAGE ON LANGUAGE c TO PUBLIC; -- fail ERROR: language "c" is not trusted SET SESSION AUTHORIZATION regressuser1; GRANT USAGE ON LANGUAGE sql TO regressuser2; -- fail -ERROR: permission denied for language sql +WARNING: no privileges were granted CREATE FUNCTION testfunc1(int) RETURNS int AS 'select 2 * $1;' LANGUAGE sql; CREATE FUNCTION testfunc2(int) RETURNS int AS 'select 3 * $1;' LANGUAGE sql; REVOKE ALL ON FUNCTION testfunc1(int), testfunc2(int) FROM PUBLIC; @@ -550,7 +550,7 @@ ERROR: grant options can only be granted to individual users SET SESSION AUTHORIZATION regressuser2; GRANT SELECT ON atest4 TO regressuser3; GRANT UPDATE ON atest4 TO regressuser3; -- fail -ERROR: permission denied for relation atest4 +WARNING: no privileges were granted SET SESSION AUTHORIZATION regressuser1; REVOKE SELECT ON atest4 FROM regressuser3; -- does nothing SELECT has_table_privilege('regressuser3', 'atest4', 'SELECT'); -- true -- 2.11.0