pax_global_header00006660000000000000000000000064152205070560014514gustar00rootroot0000000000000052 comment=3360447e0668fed2b9db6a0ae1d086533781f9bd cybertec-postgresql-pg_permissions-642eb39/000077500000000000000000000000001522050705600211145ustar00rootroot00000000000000cybertec-postgresql-pg_permissions-642eb39/.dockerignore000066400000000000000000000000651522050705600235710ustar00rootroot00000000000000.git .github results regression.diffs regression.out cybertec-postgresql-pg_permissions-642eb39/.github/000077500000000000000000000000001522050705600224545ustar00rootroot00000000000000cybertec-postgresql-pg_permissions-642eb39/.github/workflows/000077500000000000000000000000001522050705600245115ustar00rootroot00000000000000cybertec-postgresql-pg_permissions-642eb39/.github/workflows/regression.yml000066400000000000000000000015431522050705600274170ustar00rootroot00000000000000name: Build on: [push, pull_request] jobs: build: runs-on: ubuntu-latest defaults: run: shell: sh strategy: matrix: pgversion: - 19 - 18 - 17 - 16 - 15 - 14 - 13 - 12 env: PGVERSION: ${{ matrix.pgversion }} steps: - name: checkout uses: actions/checkout@v4 - name: install pg run: | sudo /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -v $PGVERSION -p -i sudo -u postgres createuser -s "$USER" - name: install extension run: | sudo -E make install - name: test run: | sudo pg_ctlcluster $PGVERSION main restart make installcheck - name: show regression diffs if: ${{ failure() }} run: | cat regression.diffs cybertec-postgresql-pg_permissions-642eb39/.gitignore000066400000000000000000000000451522050705600231030ustar00rootroot00000000000000results regression.* !regression.yml cybertec-postgresql-pg_permissions-642eb39/CHANGELOG000066400000000000000000000040171522050705600223300ustar00rootroot00000000000000Version 1.4.1, released 2026-06-29 Enhancements: - test/: a Dockerfile parameterised by PG_MAJOR plus run-tests.sh and run-installcheck.sh to build and run "make installcheck" against every supported major version in throwaway containers. Not-yet-released versions (currently 19) are pulled from the pgdg-snapshot repository. - CI: add PostgreSQL 19 to the regression matrix. - README: document all of the *_permissions views with worked examples, add a desired-vs-actual walkthrough built around permission_diffs() and the updatable views, and add a Compliance section relating the extension to common regulatory frameworks. Version 1.4, released 2025-08-04 Bugfixes: - Match a literal underscore in the "pg\_%" LIKE pattern so that schemas whose names merely start with "pg" (such as "pgabc123") are no longer mistaken for system schemas and excluded from the views. Enhancements: - Add a containerised regression test harness (in "test/") that builds the extension and runs "make installcheck" against PostgreSQL 12 through 19, pulling not-yet-released versions from the pgdg-snapshot repository. - Add PostgreSQL 19 to the continuous integration matrix. - Expand the documentation with worked examples for every "*_permissions" view, a walkthrough comparing the desired and actual state with "permission_diffs()" and the updatable views, and a section relating the extension to common compliance frameworks. Version 1.3, released 2024-06-12 Enhancements: - Support the MAINTAINprivilege new in PostgreSQL v17. Version 1.2, released 2024-02-16 Enhancements: - Add sequence to "permission_target" for ease of use. Idea by Srikanth Medikonda. Version 1.1, released 2018-12-19 Enhancements: - Add triggers to the permission views to make them updatable. Updating will issue GRANT or REVOKE statements. Patch by Antonin Houska. Bugfixes: - Rename the "permissions" column of "schema_permissions" and "database_permissions" to "permission" to match the other views. Version 1.0, released 2018-10-18 - first release cybertec-postgresql-pg_permissions-642eb39/LICENSE000066400000000000000000000017461522050705600221310ustar00rootroot00000000000000Copyright (c) 2018-2023, CYBERTEC PostgreSQL International GmbH Permission to use, copy, modify, and distribute this software and its documentation for any purpose, without fee, and without a written agreement is hereby granted, provided that the above copyright notice and this paragraph and the following two paragraphs appear in all copies. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF THE COPYRIGHT HOLDER HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE COPYRIGHT HOLDER SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS, AND CYBERTEC International GmbH HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. cybertec-postgresql-pg_permissions-642eb39/Makefile000066400000000000000000000002621522050705600225540ustar00rootroot00000000000000EXTENSION = pg_permissions DATA = pg_permissions--*.sql DOCS = README.pg_permissions REGRESS = sample PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) cybertec-postgresql-pg_permissions-642eb39/README.md000066400000000000000000000451211522050705600223760ustar00rootroot00000000000000PostgreSQL permission reports and checks ======================================== This extension allows you to review object permissions on a PostgreSQL database. Cookbook -------- First, you have to install the extension in the database: CREATE EXTENSION pg_permissions SCHEMA public; Then you need to add entries to `permission_target` that correspond to your desired permissions. Let's assume we have a schema `appschema`, and `appuser` should have `SELECT`, `UPDATE`, `DELETE` and `INSERT` permissions on all tables and views in that schema: INSERT INTO public.permission_target (role_name, permissions, object_type, schema_name) VALUES ('appuser', '{SELECT,INSERT,UPDATE,DELETE}', 'TABLE', 'appschema'); INSERT INTO public.permission_target (role_name, permissions, object_type, schema_name) VALUES ('appuser', '{SELECT,INSERT,UPDATE,DELETE}', 'VIEW', 'appschema'); Of course, the user will need the `USAGE` privilege on the schema: INSERT INTO public.permission_target (role_name, permissions, object_type, schema_name) VALUES ('appuser', '{USAGE}', 'SCHEMA', 'appschema'); The user also needs `USAGE` privileges on the `appseq` sequence in that schema: INSERT INTO public.permission_target (role_name, permissions, object_type, schema_name, object_name) VALUES ('appuser', '{USAGE}', 'SEQUENCE', 'appschema', 'appseq'); Now we can review which permissions are missing and which additional permissions are granted: SELECT * FROM public.permission_diffs(); missing | role_name | object_type | schema_name | object_name | column_name | permission ---------+-----------+-------------+-------------+-------------+-------------+------------ f | laurenz | VIEW | appschema | appview | | SELECT t | appuser | TABLE | appschema | apptable | | DELETE (2 rows) That means that `appuser` is missing the `DELETE` privilege on `appschema.apptable` which should be granted, while user `laurenz` has the additional `SELECT` privilege on `appschema.appview` (`missing` is `FALSE`). To review the actual permissions on an object, we can use the `*_permissions` views: SELECT * FROM schema_permissions WHERE role_name = 'appuser' AND schema_name = 'appschema'; object_type | role_name | schema_name | object_name | column_name | permissions | granted -------------+-----------+-------------+-------------+-------------+-------------+--------- SCHEMA | appuser | appschema | | | USAGE | t SCHEMA | appuser | appschema | | | CREATE | f (2 rows) Usage ----- ### Views ### The extension provides a number of views: - `database_permissions`: permissions granted on the current database - `schema_permissions`: permissions granted on schemas - `table_permissions`: permissions granted on tables - `view_permissions`: permissions granted on views - `column_permissions`: permissions granted on table and view columns - `function_permissions`: permissions granted on functions - `sequence_permissions`: permissions granted on sequences - `all_permissions`: permissions on all objects (`UNION` of the above) All views share the same columns: - `object_type`: the kind of object (`TABLE`, `VIEW`, `COLUMN`, `SEQUENCE`, `FUNCTION`, `SCHEMA` or `DATABASE`) - `role_name`: the role the permission applies to - `schema_name`, `object_name`, `column_name`: identify the object - `permission`: the privilege in question (`SELECT`, `USAGE`, `EXECUTE`, ...) - `granted`: whether the privilege is currently in effect (`t`) or not (`f`) A column is NULL if it has no meaning for the current view; for example, `column_name` is only set in `column_permissions`, and `schema_name` is always NULL in `database_permissions`. These views can be used to examine the currently granted permissions on database objects. The `granted` column of these views can be updated, which causes the appropriate `GRANT` or `REVOKE` command to be executed. **Note:** Superusers are not shown in the views, as they automatically have all permissions. #### Examples #### The examples in this section assume the following objects and grants: CREATE ROLE appuser LOGIN; CREATE ROLE reporting LOGIN; CREATE ROLE auditor LOGIN; CREATE SCHEMA appschema; CREATE TABLE appschema.invoice ( id integer PRIMARY KEY, customer text NOT NULL, amount numeric NOT NULL ); CREATE VIEW appschema.invoice_v AS SELECT id, customer FROM appschema.invoice; CREATE SEQUENCE appschema.invoice_id_seq; CREATE FUNCTION appschema.total() RETURNS numeric LANGUAGE sql AS 'SELECT sum(amount) FROM appschema.invoice'; GRANT USAGE ON SCHEMA appschema TO appuser, reporting, auditor; GRANT SELECT, INSERT, UPDATE, DELETE ON appschema.invoice TO appuser; GRANT SELECT ON appschema.invoice TO reporting; GRANT SELECT ON appschema.invoice_v TO reporting; GRANT SELECT (customer) ON appschema.invoice TO auditor; GRANT USAGE ON SEQUENCE appschema.invoice_id_seq TO appuser; REVOKE EXECUTE ON FUNCTION appschema.total() FROM PUBLIC; GRANT EXECUTE ON FUNCTION appschema.total() TO reporting; `schema_permissions` shows the `USAGE` and `CREATE` privileges on schemas: SELECT object_type, role_name, schema_name, permission, granted FROM schema_permissions WHERE schema_name = 'appschema' AND role_name IN ('appuser', 'reporting'); object_type | role_name | schema_name | permission | granted -------------+-----------+-------------+------------+--------- SCHEMA | appuser | appschema | USAGE | t SCHEMA | appuser | appschema | CREATE | f SCHEMA | reporting | appschema | USAGE | t SCHEMA | reporting | appschema | CREATE | f `table_permissions` lists every table privilege for each role; `granted` tells you which ones are actually in effect: SELECT role_name, object_name, permission, granted FROM table_permissions WHERE schema_name = 'appschema' AND role_name IN ('appuser', 'reporting'); role_name | object_name | permission | granted -----------+-------------+------------+--------- appuser | invoice | SELECT | t appuser | invoice | INSERT | t appuser | invoice | UPDATE | t appuser | invoice | DELETE | t appuser | invoice | TRUNCATE | f appuser | invoice | REFERENCES | f appuser | invoice | TRIGGER | f reporting | invoice | SELECT | t reporting | invoice | INSERT | f reporting | invoice | UPDATE | f reporting | invoice | DELETE | f reporting | invoice | TRUNCATE | f reporting | invoice | REFERENCES | f reporting | invoice | TRIGGER | f `view_permissions` is the same, but for views: SELECT role_name, object_name, permission, granted FROM view_permissions WHERE schema_name = 'appschema' AND role_name = 'reporting'; role_name | object_name | permission | granted -----------+-------------+------------+--------- reporting | invoice_v | SELECT | t reporting | invoice_v | INSERT | f reporting | invoice_v | UPDATE | f reporting | invoice_v | DELETE | f reporting | invoice_v | TRUNCATE | f reporting | invoice_v | REFERENCES | f reporting | invoice_v | TRIGGER | f `column_permissions` reports column level privileges. Note that `granted` is only `t` when the privilege is granted *on the column itself*; a privilege held at the table level is not repeated here (use `table_permissions` for that). Here `auditor` may read only the `customer` column: SELECT role_name, object_name, column_name, permission, granted FROM column_permissions WHERE schema_name = 'appschema' AND object_name = 'invoice' AND role_name = 'auditor'; role_name | object_name | column_name | permission | granted -----------+-------------+-------------+------------+--------- auditor | invoice | amount | SELECT | f auditor | invoice | amount | INSERT | f auditor | invoice | amount | UPDATE | f auditor | invoice | amount | REFERENCES | f auditor | invoice | customer | SELECT | t auditor | invoice | customer | INSERT | f auditor | invoice | customer | UPDATE | f auditor | invoice | customer | REFERENCES | f auditor | invoice | id | SELECT | f auditor | invoice | id | INSERT | f auditor | invoice | id | UPDATE | f auditor | invoice | id | REFERENCES | f `sequence_permissions` covers the `SELECT`, `USAGE` and `UPDATE` privileges on sequences: SELECT role_name, object_name, permission, granted FROM sequence_permissions WHERE schema_name = 'appschema' AND role_name IN ('appuser', 'reporting'); role_name | object_name | permission | granted -----------+----------------+------------+--------- appuser | invoice_id_seq | SELECT | f appuser | invoice_id_seq | UPDATE | f appuser | invoice_id_seq | USAGE | t reporting | invoice_id_seq | SELECT | f reporting | invoice_id_seq | UPDATE | f reporting | invoice_id_seq | USAGE | f `function_permissions` reports the `EXECUTE` privilege on functions. Keep in mind that functions are executable by `PUBLIC` by default, so `REVOKE ... FROM PUBLIC` is usually needed before per-role grants are meaningful: SELECT role_name, object_name, permission, granted FROM function_permissions WHERE schema_name = 'appschema' AND role_name IN ('appuser', 'reporting'); role_name | object_name | permission | granted -----------+-------------+------------+--------- appuser | total() | EXECUTE | f reporting | total() | EXECUTE | t `database_permissions` shows the `CREATE`, `CONNECT` and `TEMPORARY` privileges on the current database (`CONNECT` and `TEMPORARY` are granted to `PUBLIC` by default): SELECT role_name, permission, granted FROM database_permissions WHERE role_name IN ('appuser', 'reporting'); role_name | permission | granted -----------+------------+--------- appuser | CREATE | f appuser | CONNECT | t appuser | TEMPORARY | t reporting | CREATE | f reporting | CONNECT | t reporting | TEMPORARY | t `all_permissions` is the union of all of the above. Filtering on `granted` gives a concise overview of everything a set of roles can actually do: SELECT object_type, role_name, object_name, column_name, permission FROM all_permissions WHERE granted AND role_name IN ('appuser', 'reporting', 'auditor') AND coalesce(schema_name, 'appschema') = 'appschema' ORDER BY object_type, role_name, object_name, column_name, permission; object_type | role_name | object_name | column_name | permission -------------+-----------+----------------+-------------+------------ TABLE | appuser | invoice | | SELECT TABLE | appuser | invoice | | INSERT TABLE | appuser | invoice | | UPDATE TABLE | appuser | invoice | | DELETE TABLE | reporting | invoice | | SELECT VIEW | reporting | invoice_v | | SELECT COLUMN | auditor | invoice | customer | SELECT SEQUENCE | appuser | invoice_id_seq | | USAGE FUNCTION | reporting | total() | | EXECUTE SCHEMA | appuser | | | USAGE SCHEMA | auditor | | | USAGE SCHEMA | reporting | | | USAGE DATABASE | appuser | | | CONNECT DATABASE | appuser | | | TEMPORARY DATABASE | auditor | | | CONNECT DATABASE | auditor | | | TEMPORARY DATABASE | reporting | | | CONNECT DATABASE | reporting | | | TEMPORARY ### Tables ### The extension provides a table `permission_target` with which you can describe the permissions that *should* be granted on database objects. If you set a relevant column in `permission_target` to NULL (e.g., the `object_name` and `column_name` columns in a `TABLE` entry), the meaning is that the entry refers to *all* possible objects (in the example above, all tables in the schema). ### Functions ### The table function `permission_diffs()` checks the desired permissions in `permission_target` against the actually granted permissions in the views of the extension and returns a table of differences. If the first column `missing` is `TRUE`, the result is a permission that should be there but isn't; if `missing` is `FALSE`, the result row is a permission that is there even though it is not defined in `permission_target` (an extra permission). #### Comparing desired and actual state #### Suppose `appuser` should be able to read and write all tables in `appschema`, while `reporting` should only be allowed to read them. We record that as the desired state: INSERT INTO permission_target (role_name, permissions, object_type, schema_name) VALUES ('appuser', '{SELECT,INSERT,UPDATE,DELETE}', 'TABLE', 'appschema'), ('reporting', '{SELECT}', 'TABLE', 'appschema'); Over time the actual grants drifted: `appuser` never received `DELETE`, and `reporting` was accidentally given `INSERT`: GRANT SELECT, INSERT, UPDATE ON appschema.invoice TO appuser; GRANT SELECT, INSERT ON appschema.invoice TO reporting; `permission_diffs()` reports exactly these two deviations: SELECT * FROM permission_diffs() WHERE role_name IN ('appuser', 'reporting'); missing | role_name | object_type | schema_name | object_name | column_name | permission ---------+-----------+-------------+-------------+-------------+-------------+------------ t | appuser | TABLE | appschema | invoice | | DELETE f | reporting | TABLE | appschema | invoice | | INSERT The `DELETE` privilege is missing for `appuser` (`missing` is `t`), while `reporting` holds a surplus `INSERT` privilege (`missing` is `f`). Because the permission views are updatable, both can be corrected without writing `GRANT` or `REVOKE` by hand: -- grant the missing privilege UPDATE table_permissions SET granted = true WHERE role_name = 'appuser' AND schema_name = 'appschema' AND object_name = 'invoice' AND permission = 'DELETE'; -- revoke the surplus privilege UPDATE table_permissions SET granted = false WHERE role_name = 'reporting' AND schema_name = 'appschema' AND object_name = 'invoice' AND permission = 'INSERT'; Running `permission_diffs()` again now returns no rows: the actual state matches the desired state. Installation ------------ Make sure the PostgreSQL extension building infrastructure is installed. If you installed PostgreSQL with installation packages, you usually need to install the "development"-Package. Make sure that `pg_config` is on your `PATH`. Then type make install Then connect to the database where you want to run `pg_permissions` and use CREATE EXTENSION pg_permissions; To upgrade from an older version of the extension, run ALTER EXTENSION pg_permissions UPDATE; You need `CREATE` privileges on the schema where you install the extension. Note that you won't be able to upgrade the extension from version 1.2 or earlier to 1.3 or later for technical reasons (an added enumeration value for the `MAINTAIN` privilege). You will have to drop and re-create the extension to upgrade to 1.3 or later from an earlier release. Don't forget to dump the contents of `permission_target` before you do that, so that you can restore them afterwards. ### Installation without the extension building infrastructure ### This is also what Windows users will have to do because there is no extension building infrastructure for Windows. Find out where your PostgreSQL share directory is: pg_config --sharedir Then copy `pg_permissions.control` and the SQL files to the `extension` subdirectory of that directory, e.g. copy pg_permissions.control *.sql "C:\Program Files\PostgreSQL\10\share\extension" You still have to run `CREATE EXTENSION` as described above. Compliance ---------- Virtually every security and data-protection framework requires that access to data be limited to those who need it (the principle of least privilege), that this access be reviewed regularly, and that the review be auditable. `pg_permissions` directly supports those obligations: you declare the intended permissions in `permission_target`, prove the current state with the `*_permissions` views, and detect any drift with `permission_diffs()`. This makes the extension useful when demonstrating database access controls for, among others: - **SOX** (Sarbanes-Oxley): IT general controls under section 404 — evidence that access to financial data is restricted and periodically reviewed. - **PCI DSS**: requirements 7 and 8, which mandate restricting access to cardholder data on a business need-to-know basis. - **GDPR**: Article 32 (security of processing), which expects access to personal data to be limited to what is necessary. - **HIPAA**: the Security Rule's access-control standard (§ 164.312(a)) for electronic protected health information. - **ISO/IEC 27001 / 27002**: the access-control objectives (Annex A.9 / control 5.15 and following) covering formal entitlement and periodic review. - **SOC 2**: the Security and Confidentiality trust-services criteria around logical access. - **DORA**: the EU Digital Operational Resilience Act's access-management requirements for financial entities. The extension does not enforce these controls on its own; it provides the reporting and reconciliation you need to define a desired state, prove the current state, and catch any divergence between audits. Support ------- Open an [issue][issue] on GitHub if you have problems or questions. For professional support, please contact [CYBERTEC PostgreSQL International GmbH][cybertec]. [issue]: https://github.com/cybertec-postgresql/pg_permissions/issues [cybertec]: https://www.cybertec-postgresql.com/ cybertec-postgresql-pg_permissions-642eb39/README.pg_permissions000077700000000000000000000000001522050705600263062README.mdustar00rootroot00000000000000cybertec-postgresql-pg_permissions-642eb39/expected/000077500000000000000000000000001522050705600227155ustar00rootroot00000000000000cybertec-postgresql-pg_permissions-642eb39/expected/sample.out000066400000000000000000000350001522050705600247250ustar00rootroot00000000000000CREATE EXTENSION pg_permissions; /* test roles (will be removed afterwards) */ CREATE ROLE users; CREATE ROLE user1 LOGIN IN ROLE users; CREATE ROLE user2 LOGIN IN ROLE users; /* database */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('users', ARRAY['CONNECT','TEMPORARY']::perm_type[], 'DATABASE', NULL, NULL, NULL), ('user1', ARRAY['CONNECT','TEMPORARY']::perm_type[], 'DATABASE', NULL, NULL, NULL), ('user2', ARRAY['CONNECT','TEMPORARY']::perm_type[], 'DATABASE', NULL, NULL, NULL); -- this should fail INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user2', ARRAY['CREATE']::perm_type[], 'DATABASE', 'public', NULL, NULL); ERROR: new row for relation "permission_target" violates check constraint "permission_target_valid" DETAIL: Failing row contains (4, user2, {CREATE}, DATABASE, public, null, null). -- actual permissions REVOKE ALL ON DATABASE contrib_regression FROM PUBLIC; GRANT CONNECT, TEMPORARY ON DATABASE contrib_regression TO users; GRANT CREATE ON DATABASE contrib_regression TO user2; -- too much /* schema */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('users', ARRAY['USAGE']::perm_type[], 'SCHEMA', 'appschema', NULL, NULL), ('user1', ARRAY['USAGE','CREATE']::perm_type[], 'SCHEMA', 'appschema', NULL, NULL), ('user2', ARRAY['USAGE']::perm_type[], 'SCHEMA', 'appschema', NULL, NULL); -- this should fail INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user2', ARRAY['CREATE']::perm_type[], 'SCHEMA', 'appschema', 'sometable', NULL); ERROR: new row for relation "permission_target" violates check constraint "permission_target_valid" DETAIL: Failing row contains (8, user2, {CREATE}, SCHEMA, appschema, sometable, null). -- actual permissions CREATE SCHEMA appschema; GRANT USAGE ON SCHEMA appschema TO PUBLIC; -- missing CREATE for user1 GRANT CREATE ON SCHEMA appschema TO user2; -- too much CREATE SCHEMA pgabc123; GRANT USAGE ON SCHEMA pgabc123 TO user1; /* table */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user1', ARRAY['SELECT','INSERT','UPDATE','DELETE']::perm_type[], 'TABLE', 'appschema', NULL, NULL), ('user2', ARRAY['SELECT']::perm_type[], 'TABLE', 'appschema', NULL, NULL), ('user1', ARRAY['SELECT']::perm_type[], 'TABLE', 'pgabc213', 'sometable', NULL); -- this should fail INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user2', ARRAY['INSERT']::perm_type[], 'TABLE', 'appschema', 'apptable', 'acolumn'); ERROR: new row for relation "permission_target" violates check constraint "permission_target_valid" DETAIL: Failing row contains (12, user2, {INSERT}, TABLE, appschema, apptable, acolumn). -- actual permissions CREATE TABLE appschema.apptable ( id integer PRIMARY KEY, val text NOT NULL, created timestamp with time zone NOT NULL DEFAULT current_timestamp ); CREATE TABLE appschema.apptable2 ( id integer PRIMARY KEY, val text NOT NULL, created timestamp with time zone NOT NULL DEFAULT current_timestamp ); -- missing all permissions on this one CREATE TABLE pgabc123.sometable ( id integer PRIMARY KEY, val text NOT NULL, created timestamp with time zone NOT NULL DEFAULT current_timestamp ); GRANT SELECT, INSERT, UPDATE ON appschema.apptable TO user1; -- missing DELETE GRANT SELECT, INSERT ON appschema.apptable TO user2; -- extra privilege INSERT GRANT SELECT ON pgabc123.sometable TO user1; /* column */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user1', ARRAY['SELECT','INSERT','UPDATE','REFERENCES']::perm_type[], 'COLUMN', 'appschema', 'apptable2', 'val'); -- this should fail INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user2', ARRAY['DELETE']::perm_type[], 'COLUMN', 'appschema', 'apptable2', 'val'); ERROR: new row for relation "permission_target" violates check constraint "permission_target_valid" DETAIL: Failing row contains (14, user2, {DELETE}, COLUMN, appschema, apptable2, val). -- actual permissions -- missing REFERENCES for user1 on apptable2.val GRANT UPDATE (val) ON appschema.apptable2 TO user2; -- extra privilege UPDATE /* view */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user1', ARRAY['SELECT','INSERT','UPDATE','DELETE']::perm_type[], 'VIEW', 'appschema', 'appview', NULL), ('user2', ARRAY['SELECT']::perm_type[], 'VIEW', 'appschema', 'appview', NULL); -- actual permissions CREATE VIEW appschema.appview AS SELECT id, val FROM appschema.apptable; GRANT SELECT ON appschema.appview TO users; -- extra permission to "users" GRANT INSERT, DELETE ON appschema.appview TO user1; -- missing UPDATE /* sequence */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('users', ARRAY['USAGE']::perm_type[], 'SEQUENCE', 'appschema', 'appseq', NULL), ('user1', ARRAY['USAGE','SELECT']::perm_type[], 'SEQUENCE', 'appschema', 'appseq', NULL), ('user2', ARRAY['USAGE']::perm_type[], 'SEQUENCE', 'appschema', 'appseq', NULL); -- actual permissions CREATE SEQUENCE appschema.appseq; GRANT USAGE ON SEQUENCE appschema.appseq TO users; -- missing SELECT for user1 GRANT UPDATE ON SEQUENCE appschema.appseq TO user2; -- extra permission UPDATE /* function */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user1', ARRAY['EXECUTE']::perm_type[], 'FUNCTION', 'appschema', 'appfun(integer)', NULL), ('user2', ARRAY['EXECUTE']::perm_type[], 'FUNCTION', 'appschema', 'appfun(integer)', NULL); -- this should fail INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('users', ARRAY['UPDATE']::perm_type[], 'FUNCTION', 'appschema', 'appfun(integer)', NULL); ERROR: new row for relation "permission_target" violates check constraint "permission_target_valid" DETAIL: Failing row contains (22, users, {UPDATE}, FUNCTION, appschema, appfun(integer), null). -- actual permissions CREATE FUNCTION appschema.appfun(i integer) RETURNS integer LANGUAGE sql IMMUTABLE AS 'SELECT i + 2'; -- extra permission for "users" /* report all permissions */ SELECT object_type, role_name, schema_name, object_name, column_name, permission FROM all_permissions WHERE granted AND role_name IN ('users', 'user1', 'user2') AND coalesce(schema_name, 'appschema') IN ('appschema', 'pgabc123') ORDER BY object_type, role_name, schema_name, object_name, column_name, permission; object_type | role_name | schema_name | object_name | column_name | permission -------------+-----------+-------------+-----------------+-------------+------------ TABLE | user1 | appschema | apptable | | SELECT TABLE | user1 | appschema | apptable | | INSERT TABLE | user1 | appschema | apptable | | UPDATE TABLE | user1 | pgabc123 | sometable | | SELECT TABLE | user2 | appschema | apptable | | SELECT TABLE | user2 | appschema | apptable | | INSERT VIEW | user1 | appschema | appview | | SELECT VIEW | user1 | appschema | appview | | INSERT VIEW | user1 | appschema | appview | | DELETE VIEW | user2 | appschema | appview | | SELECT VIEW | users | appschema | appview | | SELECT COLUMN | user2 | appschema | apptable2 | val | UPDATE SEQUENCE | user1 | appschema | appseq | | USAGE SEQUENCE | user2 | appschema | appseq | | UPDATE SEQUENCE | user2 | appschema | appseq | | USAGE SEQUENCE | users | appschema | appseq | | USAGE FUNCTION | user1 | appschema | appfun(integer) | | EXECUTE FUNCTION | user2 | appschema | appfun(integer) | | EXECUTE FUNCTION | users | appschema | appfun(integer) | | EXECUTE SCHEMA | user1 | appschema | | | USAGE SCHEMA | user1 | pgabc123 | | | USAGE SCHEMA | user2 | appschema | | | USAGE SCHEMA | user2 | appschema | | | CREATE SCHEMA | users | appschema | | | USAGE DATABASE | user1 | | | | CONNECT DATABASE | user1 | | | | TEMPORARY DATABASE | user2 | | | | CREATE DATABASE | user2 | | | | CONNECT DATABASE | user2 | | | | TEMPORARY DATABASE | users | | | | CONNECT DATABASE | users | | | | TEMPORARY (31 rows) /* report differences */ SELECT * FROM permission_diffs() WHERE role_name IN ('users', 'user1', 'user2') ORDER BY object_type, schema_name, object_name, column_name, role_name, permission, missing; missing | role_name | object_type | schema_name | object_name | column_name | permission ---------+-----------+-------------+-------------+-----------------+-------------+------------ t | user1 | TABLE | appschema | apptable | | DELETE f | user2 | TABLE | appschema | apptable | | INSERT t | user1 | TABLE | appschema | apptable2 | | SELECT t | user1 | TABLE | appschema | apptable2 | | INSERT t | user1 | TABLE | appschema | apptable2 | | UPDATE t | user1 | TABLE | appschema | apptable2 | | DELETE t | user2 | TABLE | appschema | apptable2 | | SELECT t | user1 | VIEW | appschema | appview | | UPDATE f | users | VIEW | appschema | appview | | SELECT t | user1 | COLUMN | appschema | apptable2 | val | SELECT t | user1 | COLUMN | appschema | apptable2 | val | INSERT t | user1 | COLUMN | appschema | apptable2 | val | UPDATE t | user1 | COLUMN | appschema | apptable2 | val | REFERENCES f | user2 | COLUMN | appschema | apptable2 | val | UPDATE t | user1 | SEQUENCE | appschema | appseq | | SELECT f | user2 | SEQUENCE | appschema | appseq | | UPDATE f | users | FUNCTION | appschema | appfun(integer) | | EXECUTE t | user1 | SCHEMA | appschema | | | CREATE f | user2 | SCHEMA | appschema | | | CREATE f | user2 | DATABASE | | | | CREATE (20 rows) /* fix some of the differences */ UPDATE column_permissions SET granted = TRUE WHERE role_name = 'user1' AND schema_name = 'appschema' AND object_name = 'apptable2' AND column_name = 'val' AND permission = 'REFERENCES'; UPDATE all_permissions SET granted = FALSE WHERE object_type = 'TABLE' AND role_name = 'user2' AND schema_name = 'appschema' AND object_name = 'apptable' AND permission = 'INSERT'; /* check the fixed permissions */ SELECT * FROM permission_diffs() WHERE role_name IN ('users', 'user1', 'user2') ORDER BY object_type, schema_name, object_name, column_name, role_name, permission, missing; missing | role_name | object_type | schema_name | object_name | column_name | permission ---------+-----------+-------------+-------------+-----------------+-------------+------------ t | user1 | TABLE | appschema | apptable | | DELETE t | user1 | TABLE | appschema | apptable2 | | SELECT t | user1 | TABLE | appschema | apptable2 | | INSERT t | user1 | TABLE | appschema | apptable2 | | UPDATE t | user1 | TABLE | appschema | apptable2 | | DELETE t | user2 | TABLE | appschema | apptable2 | | SELECT t | user1 | VIEW | appschema | appview | | UPDATE f | users | VIEW | appschema | appview | | SELECT t | user1 | COLUMN | appschema | apptable2 | val | SELECT t | user1 | COLUMN | appschema | apptable2 | val | INSERT t | user1 | COLUMN | appschema | apptable2 | val | UPDATE f | user2 | COLUMN | appschema | apptable2 | val | UPDATE t | user1 | SEQUENCE | appschema | appseq | | SELECT f | user2 | SEQUENCE | appschema | appseq | | UPDATE f | users | FUNCTION | appschema | appfun(integer) | | EXECUTE t | user1 | SCHEMA | appschema | | | CREATE f | user2 | SCHEMA | appschema | | | CREATE f | user2 | DATABASE | | | | CREATE (18 rows) /* clean up */ DROP FUNCTION appschema.appfun(integer); DROP VIEW appschema.appview; DROP SEQUENCE appschema.appseq; DROP TABLE appschema.apptable; DROP TABLE appschema.apptable2; DROP TABLE pgabc123.sometable; DROP SCHEMA appschema; DROP SCHEMA pgabc123; REVOKE ALL ON DATABASE contrib_regression FROM user1, user2, users; DROP ROLE user1; DROP ROLE user2; DROP ROLE users; cybertec-postgresql-pg_permissions-642eb39/pg_permissions--1.0--1.1.sql000066400000000000000000000140411522050705600256210ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pg_permissions UPDATE" to load this file. \quit /* rename some view columns to match the other views */ DROP VIEW all_permissions; DROP VIEW schema_permissions; DROP VIEW database_permissions; CREATE VIEW schema_permissions AS SELECT obj_type 'SCHEMA' AS object_type, r.rolname AS role_name, n.nspname AS schema_name, NULL::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_schema_privilege(r.oid, n.oid, p.perm) AS granted FROM pg_catalog.pg_namespace AS n CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('USAGE'), ('CREATE')) AS p(perm) WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg_%' AND NOT r.rolsuper; GRANT SELECT ON schema_permissions TO PUBLIC; CREATE VIEW database_permissions AS WITH list AS (SELECT unnest AS perm FROM unnest ('{"CREATE", "CONNECT", "TEMPORARY"}'::text[])) SELECT obj_type 'DATABASE' AS object_type, r.rolname AS role_name, NULL::name AS schema_name, NULL::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_database_privilege(r.oid, d.oid, p.perm) AS granted FROM pg_catalog.pg_database AS d CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('CREATE'), ('CONNECT'), ('TEMPORARY')) AS p(perm) WHERE d.datname = current_database() AND NOT r.rolsuper; GRANT SELECT ON database_permissions TO PUBLIC; CREATE VIEW all_permissions AS SELECT * FROM table_permissions UNION ALL SELECT * FROM view_permissions UNION ALL SELECT * FROM column_permissions UNION ALL SELECT * FROM sequence_permissions UNION ALL SELECT * FROM function_permissions UNION ALL SELECT * FROM schema_permissions UNION ALL SELECT * FROM database_permissions; GRANT SELECT ON all_permissions TO PUBLIC; /* update trigers for the views */ CREATE FUNCTION permissions_trigger_func() RETURNS TRIGGER LANGUAGE plpgsql AS $$ DECLARE db_name text; cmd text; BEGIN IF NEW.object_type <> OLD.object_type OR NEW.role_name <> OLD.role_name OR NEW.schema_name <> OLD.schema_name OR NEW.object_name <> OLD.object_name OR NEW.column_name <> OLD.column_name OR NEW.permission <> OLD.permission THEN RAISE 'Only the "granted" column may be updated'; END IF; -- Is there anything to do at all? IF NEW.granted = OLD.granted THEN RETURN NEW; END IF; IF OLD.object_type IN ('TABLE', 'VIEW') THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON %s.%s TO %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON %s.%s FROM %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'COLUMN' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s(%s) ON %s.%s TO %s', OLD.permission, OLD.column_name, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s(%s) ON %s.%s FROM %s', OLD.permission, OLD.column_name, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'SEQUENCE' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON SEQUENCE %s.%s TO %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON SEQUENCE %s.%s FROM %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'FUNCTION' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON FUNCTION %s.%s TO %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON FUNCTION %s.%s FROM %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'SCHEMA' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON SCHEMA %s TO %s', OLD.permission, OLD.schema_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON SCHEMA %s FROM %s', OLD.permission, OLD.schema_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'DATABASE' THEN db_name := pg_catalog.current_database(); IF NOT OLD.granted THEN cmd := format('GRANT %s ON DATABASE %s TO %s', OLD.permission, db_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON DATABASE %s FROM %s', OLD.permission, db_name, OLD.role_name); END IF; ELSE RAISE 'Unrecognized object type: %', OLD.object_type; END IF; EXECUTE cmd; RETURN NEW; END; $$; CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON table_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON column_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON view_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON sequence_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON function_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON schema_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON database_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON all_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); cybertec-postgresql-pg_permissions-642eb39/pg_permissions--1.0.sql000066400000000000000000000240641522050705600252550ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pg_permissions" to load this file. \quit /* types */ CREATE TYPE perm_type AS ENUM ( 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'USAGE', 'CREATE', 'EXECUTE', 'CONNECT', 'TEMPORARY' ); CREATE TYPE obj_type AS ENUM ( 'TABLE', 'VIEW', 'COLUMN', 'SEQUENCE', 'FUNCTION', 'SCHEMA', 'DATABASE' ); /* views for the actual permissions */ CREATE VIEW table_permissions AS SELECT obj_type 'TABLE' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES (TEXT 'SELECT'), ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND t.relkind = 'r' AND NOT r.rolsuper; GRANT SELECT ON table_permissions TO PUBLIC; CREATE VIEW view_permissions AS SELECT obj_type 'VIEW' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES (TEXT 'SELECT'), ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND t.relkind = 'v' AND NOT r.rolsuper; GRANT SELECT ON view_permissions TO PUBLIC; CREATE VIEW column_permissions AS SELECT obj_type 'COLUMN' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, c.attname AS column_name, p.perm::perm_type AS permission, has_column_privilege(r.oid, t.oid, c.attnum, p.perm) AND NOT has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t JOIN pg_catalog.pg_attribute AS c ON t.oid = c.attrelid CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('SELECT'), ('INSERT'), ('UPDATE'), ('REFERENCES')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND c.attnum > 0 AND NOT c.attisdropped AND t.relkind IN ('r', 'v') AND NOT r.rolsuper; GRANT SELECT ON column_permissions TO PUBLIC; CREATE VIEW sequence_permissions AS SELECT obj_type 'SEQUENCE' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_sequence_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('SELECT'), ('USAGE'), ('UPDATE')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND t.relkind = 'S' AND NOT r.rolsuper; GRANT SELECT ON sequence_permissions TO PUBLIC; CREATE VIEW function_permissions AS SELECT obj_type 'FUNCTION' AS object_type, r.rolname AS role_name, f.pronamespace::regnamespace::name AS schema_name, regexp_replace(f.oid::regprocedure::text, '^((("[^"]*")|([^"][^.]*))\.)?', '') AS object_name, NULL::name AS column_name, perm_type 'EXECUTE' AS permission, has_function_privilege(r.oid, f.oid, 'EXECUTE') AS granted FROM pg_catalog.pg_proc f CROSS JOIN pg_catalog.pg_roles AS r WHERE f.pronamespace::regnamespace::name <> 'information_schema' AND f.pronamespace::regnamespace::name NOT LIKE 'pg_%' AND NOT r.rolsuper; GRANT SELECT ON function_permissions TO PUBLIC; CREATE VIEW schema_permissions AS SELECT obj_type 'SCHEMA' AS object_type, r.rolname AS role_name, n.nspname AS schema_name, NULL::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permissions, has_schema_privilege(r.oid, n.oid, p.perm) AS granted FROM pg_catalog.pg_namespace AS n CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('USAGE'), ('CREATE')) AS p(perm) WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg_%' AND NOT r.rolsuper; GRANT SELECT ON schema_permissions TO PUBLIC; CREATE VIEW database_permissions AS WITH list AS (SELECT unnest AS perm FROM unnest ('{"CREATE", "CONNECT", "TEMPORARY"}'::text[])) SELECT obj_type 'DATABASE' AS object_type, r.rolname AS role_name, NULL::name AS schema_name, NULL::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permissions, has_database_privilege(r.oid, d.oid, p.perm) AS granted FROM pg_catalog.pg_database AS d CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('CREATE'), ('CONNECT'), ('TEMPORARY')) AS p(perm) WHERE d.datname = current_database() AND NOT r.rolsuper; GRANT SELECT ON database_permissions TO PUBLIC; CREATE VIEW all_permissions AS SELECT * FROM table_permissions UNION ALL SELECT * FROM view_permissions UNION ALL SELECT * FROM column_permissions UNION ALL SELECT * FROM sequence_permissions UNION ALL SELECT * FROM function_permissions UNION ALL SELECT * FROM schema_permissions UNION ALL SELECT * FROM database_permissions; GRANT SELECT ON all_permissions TO PUBLIC; /* table for the targeted permissions */ CREATE TABLE permission_target ( id int4 PRIMARY KEY, role_name name NOT NULL, permissions perm_type[] NOT NULL, object_type obj_type NOT NULL, schema_name name, object_name text, column_name name, CONSTRAINT permission_target_valid CHECK (CASE WHEN object_type = 'DATABASE' THEN schema_name IS NULL AND object_name IS NULL AND column_name IS NULL AND ARRAY['CONNECT','CREATE','TEMPORARY']::perm_type[] @> permissions WHEN object_type = 'SCHEMA' THEN object_name IS NULL AND column_name IS NULL AND ARRAY['CREATE','USAGE']::perm_type[] @> permissions WHEN object_type IN ('TABLE', 'VIEW') THEN column_name IS NULL AND ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER']::perm_type[] @> permissions WHEN object_type = 'SEQUENCE' THEN column_name IS NULL AND ARRAY['SELECT','USAGE','UPDATE']::perm_type[] @> permissions WHEN object_type = 'FUNCTION' THEN column_name IS NULL AND ARRAY['EXECUTE']::perm_type[] @> permissions WHEN object_type = 'COLUMN' THEN ARRAY['SELECT','INSERT','UPDATE','REFERENCES']::perm_type[] @> permissions END) ); GRANT SELECT, INSERT, UPDATE, DELETE ON permission_target TO PUBLIC; SELECT pg_catalog.pg_extension_config_dump('permission_target', ''); CREATE FUNCTION permission_diffs() RETURNS TABLE ( missing boolean, role_name name, object_type obj_type, schema_name name, object_name text, column_name name, permission perm_type ) LANGUAGE plpgsql SET search_path FROM CURRENT AS $$DECLARE typ obj_type; r name; ar name; s name; a_s name; o text; ao text; so name; aso name; p perm_type; ap perm_type; g boolean; ag boolean; BEGIN /* temporary receptacle for reports */ CREATE TEMPORARY TABLE findings ( missing boolean, role_name name, object_type obj_type, schema_name name, object_name text, column_name name, permission perm_type ) ON COMMIT DROP; /* loop through all entries in "permission_target" */ FOR r, p, typ, s, o, so IN SELECT pt.role_name, p.permission, pt.object_type, pt.schema_name, pt.object_name, pt.column_name FROM permission_target AS pt CROSS JOIN LATERAL unnest(pt.permissions) AS p(permission) LOOP /* find all matching object permissions */ FOR ar, ap, a_s, ao, aso, ag IN SELECT apm.role_name, apm.permission, apm.schema_name, apm.object_name, apm.column_name, apm.granted FROM all_permissions AS apm WHERE apm.object_type = typ AND (apm.schema_name = s OR s IS NULL) AND (apm.object_name = o OR o IS NULL) AND (apm.column_name = so OR so IS NULL) LOOP IF ar = r AND ap = p AND NOT ag THEN /* permission not granted that should be */ INSERT INTO findings (missing, role_name, object_type, schema_name, object_name, column_name, permission) VALUES (TRUE, r, typ, a_s, ao, aso, ap); END IF; IF (ar <> r OR ap <> p) AND ag THEN /* different permission found, check if there is a matching rule */ IF NOT EXISTS ( SELECT 1 FROM permission_target AS pt WHERE pt.role_name = ar AND pt.permissions @> ARRAY[ap]::perm_type[] AND pt.object_type = typ AND (pt.schema_name IS NULL OR pt.schema_name = a_s) AND (pt.object_name IS NULL OR pt.object_name = ao) AND (pt.column_name IS NULL OR pt.column_name = aso) ) THEN /* extra permission found, report */ INSERT INTO findings (missing, role_name, object_type, schema_name, object_name, column_name, permission) VALUES (FALSE, ar, typ, a_s, ao, aso, ap); END IF; END IF; END LOOP; END LOOP; RETURN QUERY SELECT DISTINCT * FROM findings; END;$$; cybertec-postgresql-pg_permissions-642eb39/pg_permissions--1.1--1.2.sql000066400000000000000000000007161522050705600256270ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pg_permissions UPDATE" to load this file. \quit CREATE SEQUENCE permission_target_id_seq OWNED BY permission_target.id; ALTER TABLE permission_target ALTER id SET DEFAULT nextval('permission_target_id_seq'::regclass); GRANT USAGE ON SEQUENCE permission_target_id_seq TO PUBLIC; SELECT pg_catalog.pg_extension_config_dump('permission_target_id_seq', ''); cybertec-postgresql-pg_permissions-642eb39/pg_permissions--1.1.sql000066400000000000000000000343351522050705600252600ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pg_permissions" to load this file. \quit /* types */ CREATE TYPE perm_type AS ENUM ( 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'USAGE', 'CREATE', 'EXECUTE', 'CONNECT', 'TEMPORARY' ); CREATE TYPE obj_type AS ENUM ( 'TABLE', 'VIEW', 'COLUMN', 'SEQUENCE', 'FUNCTION', 'SCHEMA', 'DATABASE' ); /* views for the actual permissions */ CREATE VIEW table_permissions AS SELECT obj_type 'TABLE' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES (TEXT 'SELECT'), ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND t.relkind = 'r' AND NOT r.rolsuper; GRANT SELECT ON table_permissions TO PUBLIC; CREATE VIEW view_permissions AS SELECT obj_type 'VIEW' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES (TEXT 'SELECT'), ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND t.relkind = 'v' AND NOT r.rolsuper; GRANT SELECT ON view_permissions TO PUBLIC; CREATE VIEW column_permissions AS SELECT obj_type 'COLUMN' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, c.attname AS column_name, p.perm::perm_type AS permission, has_column_privilege(r.oid, t.oid, c.attnum, p.perm) AND NOT has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t JOIN pg_catalog.pg_attribute AS c ON t.oid = c.attrelid CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('SELECT'), ('INSERT'), ('UPDATE'), ('REFERENCES')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND c.attnum > 0 AND NOT c.attisdropped AND t.relkind IN ('r', 'v') AND NOT r.rolsuper; GRANT SELECT ON column_permissions TO PUBLIC; CREATE VIEW sequence_permissions AS SELECT obj_type 'SEQUENCE' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_sequence_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('SELECT'), ('USAGE'), ('UPDATE')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND t.relkind = 'S' AND NOT r.rolsuper; GRANT SELECT ON sequence_permissions TO PUBLIC; CREATE VIEW function_permissions AS SELECT obj_type 'FUNCTION' AS object_type, r.rolname AS role_name, f.pronamespace::regnamespace::name AS schema_name, regexp_replace(f.oid::regprocedure::text, '^((("[^"]*")|([^"][^.]*))\.)?', '') AS object_name, NULL::name AS column_name, perm_type 'EXECUTE' AS permission, has_function_privilege(r.oid, f.oid, 'EXECUTE') AS granted FROM pg_catalog.pg_proc f CROSS JOIN pg_catalog.pg_roles AS r WHERE f.pronamespace::regnamespace::name <> 'information_schema' AND f.pronamespace::regnamespace::name NOT LIKE 'pg_%' AND NOT r.rolsuper; GRANT SELECT ON function_permissions TO PUBLIC; CREATE VIEW schema_permissions AS SELECT obj_type 'SCHEMA' AS object_type, r.rolname AS role_name, n.nspname AS schema_name, NULL::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_schema_privilege(r.oid, n.oid, p.perm) AS granted FROM pg_catalog.pg_namespace AS n CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('USAGE'), ('CREATE')) AS p(perm) WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg_%' AND NOT r.rolsuper; GRANT SELECT ON schema_permissions TO PUBLIC; CREATE VIEW database_permissions AS WITH list AS (SELECT unnest AS perm FROM unnest ('{"CREATE", "CONNECT", "TEMPORARY"}'::text[])) SELECT obj_type 'DATABASE' AS object_type, r.rolname AS role_name, NULL::name AS schema_name, NULL::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_database_privilege(r.oid, d.oid, p.perm) AS granted FROM pg_catalog.pg_database AS d CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('CREATE'), ('CONNECT'), ('TEMPORARY')) AS p(perm) WHERE d.datname = current_database() AND NOT r.rolsuper; GRANT SELECT ON database_permissions TO PUBLIC; CREATE VIEW all_permissions AS SELECT * FROM table_permissions UNION ALL SELECT * FROM view_permissions UNION ALL SELECT * FROM column_permissions UNION ALL SELECT * FROM sequence_permissions UNION ALL SELECT * FROM function_permissions UNION ALL SELECT * FROM schema_permissions UNION ALL SELECT * FROM database_permissions; GRANT SELECT ON all_permissions TO PUBLIC; /* update trigers for the views */ CREATE FUNCTION permissions_trigger_func() RETURNS TRIGGER LANGUAGE plpgsql AS $$ DECLARE db_name text; cmd text; BEGIN IF NEW.object_type <> OLD.object_type OR NEW.role_name <> OLD.role_name OR NEW.schema_name <> OLD.schema_name OR NEW.object_name <> OLD.object_name OR NEW.column_name <> OLD.column_name OR NEW.permission <> OLD.permission THEN RAISE 'Only the "granted" column may be updated'; END IF; -- Is there anything to do at all? IF NEW.granted = OLD.granted THEN RETURN NEW; END IF; IF OLD.object_type IN ('TABLE', 'VIEW') THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON %s.%s TO %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON %s.%s FROM %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'COLUMN' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s(%s) ON %s.%s TO %s', OLD.permission, OLD.column_name, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s(%s) ON %s.%s FROM %s', OLD.permission, OLD.column_name, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'SEQUENCE' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON SEQUENCE %s.%s TO %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON SEQUENCE %s.%s FROM %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'FUNCTION' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON FUNCTION %s.%s TO %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON FUNCTION %s.%s FROM %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'SCHEMA' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON SCHEMA %s TO %s', OLD.permission, OLD.schema_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON SCHEMA %s FROM %s', OLD.permission, OLD.schema_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'DATABASE' THEN db_name := pg_catalog.current_database(); IF NOT OLD.granted THEN cmd := format('GRANT %s ON DATABASE %s TO %s', OLD.permission, db_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON DATABASE %s FROM %s', OLD.permission, db_name, OLD.role_name); END IF; ELSE RAISE 'Unrecognized object type: %', OLD.object_type; END IF; EXECUTE cmd; RETURN NEW; END; $$; CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON table_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON column_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON view_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON sequence_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON function_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON schema_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON database_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON all_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); /* table for the targeted permissions */ CREATE TABLE permission_target ( id int4 PRIMARY KEY, role_name name NOT NULL, permissions perm_type[] NOT NULL, object_type obj_type NOT NULL, schema_name name, object_name text, column_name name, CONSTRAINT permission_target_valid CHECK (CASE WHEN object_type = 'DATABASE' THEN schema_name IS NULL AND object_name IS NULL AND column_name IS NULL AND ARRAY['CONNECT','CREATE','TEMPORARY']::perm_type[] @> permissions WHEN object_type = 'SCHEMA' THEN object_name IS NULL AND column_name IS NULL AND ARRAY['CREATE','USAGE']::perm_type[] @> permissions WHEN object_type IN ('TABLE', 'VIEW') THEN column_name IS NULL AND ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER']::perm_type[] @> permissions WHEN object_type = 'SEQUENCE' THEN column_name IS NULL AND ARRAY['SELECT','USAGE','UPDATE']::perm_type[] @> permissions WHEN object_type = 'FUNCTION' THEN column_name IS NULL AND ARRAY['EXECUTE']::perm_type[] @> permissions WHEN object_type = 'COLUMN' THEN ARRAY['SELECT','INSERT','UPDATE','REFERENCES']::perm_type[] @> permissions END) ); GRANT SELECT, INSERT, UPDATE, DELETE ON permission_target TO PUBLIC; SELECT pg_catalog.pg_extension_config_dump('permission_target', ''); CREATE FUNCTION permission_diffs() RETURNS TABLE ( missing boolean, role_name name, object_type obj_type, schema_name name, object_name text, column_name name, permission perm_type ) LANGUAGE plpgsql SET search_path FROM CURRENT AS $$DECLARE typ obj_type; r name; ar name; s name; a_s name; o text; ao text; so name; aso name; p perm_type; ap perm_type; g boolean; ag boolean; BEGIN /* temporary receptacle for reports */ CREATE TEMPORARY TABLE findings ( missing boolean, role_name name, object_type obj_type, schema_name name, object_name text, column_name name, permission perm_type ) ON COMMIT DROP; /* loop through all entries in "permission_target" */ FOR r, p, typ, s, o, so IN SELECT pt.role_name, p.permission, pt.object_type, pt.schema_name, pt.object_name, pt.column_name FROM permission_target AS pt CROSS JOIN LATERAL unnest(pt.permissions) AS p(permission) LOOP /* find all matching object permissions */ FOR ar, ap, a_s, ao, aso, ag IN SELECT apm.role_name, apm.permission, apm.schema_name, apm.object_name, apm.column_name, apm.granted FROM all_permissions AS apm WHERE apm.object_type = typ AND (apm.schema_name = s OR s IS NULL) AND (apm.object_name = o OR o IS NULL) AND (apm.column_name = so OR so IS NULL) LOOP IF ar = r AND ap = p AND NOT ag THEN /* permission not granted that should be */ INSERT INTO findings (missing, role_name, object_type, schema_name, object_name, column_name, permission) VALUES (TRUE, r, typ, a_s, ao, aso, ap); END IF; IF (ar <> r OR ap <> p) AND ag THEN /* different permission found, check if there is a matching rule */ IF NOT EXISTS ( SELECT 1 FROM permission_target AS pt WHERE pt.role_name = ar AND pt.permissions @> ARRAY[ap]::perm_type[] AND pt.object_type = typ AND (pt.schema_name IS NULL OR pt.schema_name = a_s) AND (pt.object_name IS NULL OR pt.object_name = ao) AND (pt.column_name IS NULL OR pt.column_name = aso) ) THEN /* extra permission found, report */ INSERT INTO findings (missing, role_name, object_type, schema_name, object_name, column_name, permission) VALUES (FALSE, ar, typ, a_s, ao, aso, ap); END IF; END IF; END LOOP; END LOOP; RETURN QUERY SELECT DISTINCT * FROM findings; END;$$; cybertec-postgresql-pg_permissions-642eb39/pg_permissions--1.2.sql000066400000000000000000000350231522050705600252540ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pg_permissions" to load this file. \quit /* types */ CREATE TYPE perm_type AS ENUM ( 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'USAGE', 'CREATE', 'EXECUTE', 'CONNECT', 'TEMPORARY' ); CREATE TYPE obj_type AS ENUM ( 'TABLE', 'VIEW', 'COLUMN', 'SEQUENCE', 'FUNCTION', 'SCHEMA', 'DATABASE' ); /* views for the actual permissions */ CREATE VIEW table_permissions AS SELECT obj_type 'TABLE' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES (TEXT 'SELECT'), ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND t.relkind = 'r' AND NOT r.rolsuper; GRANT SELECT ON table_permissions TO PUBLIC; CREATE VIEW view_permissions AS SELECT obj_type 'VIEW' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES (TEXT 'SELECT'), ('INSERT'), ('UPDATE'), ('DELETE'), ('TRUNCATE'), ('REFERENCES'), ('TRIGGER')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND t.relkind = 'v' AND NOT r.rolsuper; GRANT SELECT ON view_permissions TO PUBLIC; CREATE VIEW column_permissions AS SELECT obj_type 'COLUMN' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, c.attname AS column_name, p.perm::perm_type AS permission, has_column_privilege(r.oid, t.oid, c.attnum, p.perm) AND NOT has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t JOIN pg_catalog.pg_attribute AS c ON t.oid = c.attrelid CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('SELECT'), ('INSERT'), ('UPDATE'), ('REFERENCES')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND c.attnum > 0 AND NOT c.attisdropped AND t.relkind IN ('r', 'v') AND NOT r.rolsuper; GRANT SELECT ON column_permissions TO PUBLIC; CREATE VIEW sequence_permissions AS SELECT obj_type 'SEQUENCE' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_sequence_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('SELECT'), ('USAGE'), ('UPDATE')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND t.relkind = 'S' AND NOT r.rolsuper; GRANT SELECT ON sequence_permissions TO PUBLIC; CREATE VIEW function_permissions AS SELECT obj_type 'FUNCTION' AS object_type, r.rolname AS role_name, f.pronamespace::regnamespace::name AS schema_name, regexp_replace(f.oid::regprocedure::text, '^((("[^"]*")|([^"][^.]*))\.)?', '') AS object_name, NULL::name AS column_name, perm_type 'EXECUTE' AS permission, has_function_privilege(r.oid, f.oid, 'EXECUTE') AS granted FROM pg_catalog.pg_proc f CROSS JOIN pg_catalog.pg_roles AS r WHERE f.pronamespace::regnamespace::name <> 'information_schema' AND f.pronamespace::regnamespace::name NOT LIKE 'pg_%' AND NOT r.rolsuper; GRANT SELECT ON function_permissions TO PUBLIC; CREATE VIEW schema_permissions AS SELECT obj_type 'SCHEMA' AS object_type, r.rolname AS role_name, n.nspname AS schema_name, NULL::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_schema_privilege(r.oid, n.oid, p.perm) AS granted FROM pg_catalog.pg_namespace AS n CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('USAGE'), ('CREATE')) AS p(perm) WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg_%' AND NOT r.rolsuper; GRANT SELECT ON schema_permissions TO PUBLIC; CREATE VIEW database_permissions AS WITH list AS (SELECT unnest AS perm FROM unnest ('{"CREATE", "CONNECT", "TEMPORARY"}'::text[])) SELECT obj_type 'DATABASE' AS object_type, r.rolname AS role_name, NULL::name AS schema_name, NULL::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_database_privilege(r.oid, d.oid, p.perm) AS granted FROM pg_catalog.pg_database AS d CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('CREATE'), ('CONNECT'), ('TEMPORARY')) AS p(perm) WHERE d.datname = current_database() AND NOT r.rolsuper; GRANT SELECT ON database_permissions TO PUBLIC; CREATE VIEW all_permissions AS SELECT * FROM table_permissions UNION ALL SELECT * FROM view_permissions UNION ALL SELECT * FROM column_permissions UNION ALL SELECT * FROM sequence_permissions UNION ALL SELECT * FROM function_permissions UNION ALL SELECT * FROM schema_permissions UNION ALL SELECT * FROM database_permissions; GRANT SELECT ON all_permissions TO PUBLIC; /* update trigers for the views */ CREATE FUNCTION permissions_trigger_func() RETURNS TRIGGER LANGUAGE plpgsql AS $$ DECLARE db_name text; cmd text; BEGIN IF NEW.object_type <> OLD.object_type OR NEW.role_name <> OLD.role_name OR NEW.schema_name <> OLD.schema_name OR NEW.object_name <> OLD.object_name OR NEW.column_name <> OLD.column_name OR NEW.permission <> OLD.permission THEN RAISE 'Only the "granted" column may be updated'; END IF; -- Is there anything to do at all? IF NEW.granted = OLD.granted THEN RETURN NEW; END IF; IF OLD.object_type IN ('TABLE', 'VIEW') THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON %s.%s TO %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON %s.%s FROM %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'COLUMN' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s(%s) ON %s.%s TO %s', OLD.permission, OLD.column_name, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s(%s) ON %s.%s FROM %s', OLD.permission, OLD.column_name, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'SEQUENCE' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON SEQUENCE %s.%s TO %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON SEQUENCE %s.%s FROM %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'FUNCTION' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON FUNCTION %s.%s TO %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON FUNCTION %s.%s FROM %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'SCHEMA' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON SCHEMA %s TO %s', OLD.permission, OLD.schema_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON SCHEMA %s FROM %s', OLD.permission, OLD.schema_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'DATABASE' THEN db_name := pg_catalog.current_database(); IF NOT OLD.granted THEN cmd := format('GRANT %s ON DATABASE %s TO %s', OLD.permission, db_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON DATABASE %s FROM %s', OLD.permission, db_name, OLD.role_name); END IF; ELSE RAISE 'Unrecognized object type: %', OLD.object_type; END IF; EXECUTE cmd; RETURN NEW; END; $$; CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON table_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON column_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON view_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON sequence_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON function_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON schema_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON database_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON all_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); /* table for the targeted permissions */ CREATE TABLE permission_target ( id int4 PRIMARY KEY, role_name name NOT NULL, permissions perm_type[] NOT NULL, object_type obj_type NOT NULL, schema_name name, object_name text, column_name name, CONSTRAINT permission_target_valid CHECK (CASE WHEN object_type = 'DATABASE' THEN schema_name IS NULL AND object_name IS NULL AND column_name IS NULL AND ARRAY['CONNECT','CREATE','TEMPORARY']::perm_type[] @> permissions WHEN object_type = 'SCHEMA' THEN object_name IS NULL AND column_name IS NULL AND ARRAY['CREATE','USAGE']::perm_type[] @> permissions WHEN object_type IN ('TABLE', 'VIEW') THEN column_name IS NULL AND ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER']::perm_type[] @> permissions WHEN object_type = 'SEQUENCE' THEN column_name IS NULL AND ARRAY['SELECT','USAGE','UPDATE']::perm_type[] @> permissions WHEN object_type = 'FUNCTION' THEN column_name IS NULL AND ARRAY['EXECUTE']::perm_type[] @> permissions WHEN object_type = 'COLUMN' THEN ARRAY['SELECT','INSERT','UPDATE','REFERENCES']::perm_type[] @> permissions END) ); CREATE SEQUENCE permission_target_id_seq OWNED BY permission_target.id; ALTER TABLE permission_target ALTER id SET DEFAULT nextval('permission_target_id_seq'::regclass); GRANT SELECT, INSERT, UPDATE, DELETE ON permission_target TO PUBLIC; GRANT USAGE ON SEQUENCE permission_target_id_seq TO PUBLIC; SELECT pg_catalog.pg_extension_config_dump('permission_target', ''); SELECT pg_catalog.pg_extension_config_dump('permission_target_id_seq', ''); CREATE FUNCTION permission_diffs() RETURNS TABLE ( missing boolean, role_name name, object_type obj_type, schema_name name, object_name text, column_name name, permission perm_type ) LANGUAGE plpgsql SET search_path FROM CURRENT AS $$DECLARE typ obj_type; r name; ar name; s name; a_s name; o text; ao text; so name; aso name; p perm_type; ap perm_type; g boolean; ag boolean; BEGIN /* temporary receptacle for reports */ CREATE TEMPORARY TABLE findings ( missing boolean, role_name name, object_type obj_type, schema_name name, object_name text, column_name name, permission perm_type ) ON COMMIT DROP; /* loop through all entries in "permission_target" */ FOR r, p, typ, s, o, so IN SELECT pt.role_name, p.permission, pt.object_type, pt.schema_name, pt.object_name, pt.column_name FROM permission_target AS pt CROSS JOIN LATERAL unnest(pt.permissions) AS p(permission) LOOP /* find all matching object permissions */ FOR ar, ap, a_s, ao, aso, ag IN SELECT apm.role_name, apm.permission, apm.schema_name, apm.object_name, apm.column_name, apm.granted FROM all_permissions AS apm WHERE apm.object_type = typ AND (apm.schema_name = s OR s IS NULL) AND (apm.object_name = o OR o IS NULL) AND (apm.column_name = so OR so IS NULL) LOOP IF ar = r AND ap = p AND NOT ag THEN /* permission not granted that should be */ INSERT INTO findings (missing, role_name, object_type, schema_name, object_name, column_name, permission) VALUES (TRUE, r, typ, a_s, ao, aso, ap); END IF; IF (ar <> r OR ap <> p) AND ag THEN /* different permission found, check if there is a matching rule */ IF NOT EXISTS ( SELECT 1 FROM permission_target AS pt WHERE pt.role_name = ar AND pt.permissions @> ARRAY[ap]::perm_type[] AND pt.object_type = typ AND (pt.schema_name IS NULL OR pt.schema_name = a_s) AND (pt.object_name IS NULL OR pt.object_name = ao) AND (pt.column_name IS NULL OR pt.column_name = aso) ) THEN /* extra permission found, report */ INSERT INTO findings (missing, role_name, object_type, schema_name, object_name, column_name, permission) VALUES (FALSE, ar, typ, a_s, ao, aso, ap); END IF; END IF; END LOOP; END LOOP; RETURN QUERY SELECT DISTINCT * FROM findings; END;$$; cybertec-postgresql-pg_permissions-642eb39/pg_permissions--1.3--1.4.sql000066400000000000000000000112131522050705600256250ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "ALTER EXTENSION pg_permissions UPDATE" to load this file. \quit CREATE OR REPLACE VIEW table_permissions AS SELECT obj_type 'TABLE' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN unnest( CASE WHEN current_setting('server_version_num')::integer < 170000 THEN ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER'] ELSE ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER','MAINTAIN'] END ) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg\_%' AND t.relkind = 'r' AND NOT r.rolsuper; CREATE OR REPLACE VIEW view_permissions AS SELECT obj_type 'VIEW' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN unnest( CASE WHEN current_setting('server_version_num')::integer < 170000 THEN ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER'] ELSE ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER','MAINTAIN'] END ) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg\_%' AND t.relkind = 'v' AND NOT r.rolsuper; CREATE OR REPLACE VIEW column_permissions AS SELECT obj_type 'COLUMN' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, c.attname AS column_name, p.perm::perm_type AS permission, has_column_privilege(r.oid, t.oid, c.attnum, p.perm) AND NOT has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t JOIN pg_catalog.pg_attribute AS c ON t.oid = c.attrelid CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('SELECT'), ('INSERT'), ('UPDATE'), ('REFERENCES')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg\_%' AND c.attnum > 0 AND NOT c.attisdropped AND t.relkind IN ('r', 'v') AND NOT r.rolsuper; CREATE OR REPLACE VIEW sequence_permissions AS SELECT obj_type 'SEQUENCE' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_sequence_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('SELECT'), ('USAGE'), ('UPDATE')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg\_%' AND t.relkind = 'S' AND NOT r.rolsuper; CREATE OR REPLACE VIEW function_permissions AS SELECT obj_type 'FUNCTION' AS object_type, r.rolname AS role_name, f.pronamespace::regnamespace::name AS schema_name, regexp_replace(f.oid::regprocedure::text, '^((("[^"]*")|([^"][^.]*))\.)?', '') AS object_name, NULL::name AS column_name, perm_type 'EXECUTE' AS permission, has_function_privilege(r.oid, f.oid, 'EXECUTE') AS granted FROM pg_catalog.pg_proc f CROSS JOIN pg_catalog.pg_roles AS r WHERE f.pronamespace::regnamespace::name <> 'information_schema' AND f.pronamespace::regnamespace::name NOT LIKE 'pg\_%' AND NOT r.rolsuper; CREATE OR REPLACE VIEW schema_permissions AS SELECT obj_type 'SCHEMA' AS object_type, r.rolname AS role_name, n.nspname AS schema_name, NULL::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_schema_privilege(r.oid, n.oid, p.perm) AS granted FROM pg_catalog.pg_namespace AS n CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('USAGE'), ('CREATE')) AS p(perm) WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg\_%' AND NOT r.rolsuper; cybertec-postgresql-pg_permissions-642eb39/pg_permissions--1.3.sql000066400000000000000000000360231522050705600252560ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION pg_permissions" to load this file. \quit /* types */ CREATE TYPE perm_type AS ENUM ( 'SELECT', 'INSERT', 'UPDATE', 'DELETE', 'TRUNCATE', 'REFERENCES', 'TRIGGER', 'USAGE', 'CREATE', 'EXECUTE', 'CONNECT', 'TEMPORARY', 'MAINTAIN' ); CREATE TYPE obj_type AS ENUM ( 'TABLE', 'VIEW', 'COLUMN', 'SEQUENCE', 'FUNCTION', 'SCHEMA', 'DATABASE' ); /* views for the actual permissions */ CREATE VIEW table_permissions AS SELECT obj_type 'TABLE' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN unnest( CASE WHEN current_setting('server_version_num')::integer < 170000 THEN ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER'] ELSE ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER','MAINTAIN'] END ) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND t.relkind = 'r' AND NOT r.rolsuper; GRANT SELECT ON table_permissions TO PUBLIC; CREATE VIEW view_permissions AS SELECT obj_type 'VIEW' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN unnest( CASE WHEN current_setting('server_version_num')::integer < 170000 THEN ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER'] ELSE ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER','MAINTAIN'] END ) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND t.relkind = 'v' AND NOT r.rolsuper; GRANT SELECT ON view_permissions TO PUBLIC; CREATE VIEW column_permissions AS SELECT obj_type 'COLUMN' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, c.attname AS column_name, p.perm::perm_type AS permission, has_column_privilege(r.oid, t.oid, c.attnum, p.perm) AND NOT has_table_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t JOIN pg_catalog.pg_attribute AS c ON t.oid = c.attrelid CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('SELECT'), ('INSERT'), ('UPDATE'), ('REFERENCES')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND c.attnum > 0 AND NOT c.attisdropped AND t.relkind IN ('r', 'v') AND NOT r.rolsuper; GRANT SELECT ON column_permissions TO PUBLIC; CREATE VIEW sequence_permissions AS SELECT obj_type 'SEQUENCE' AS object_type, r.rolname AS role_name, t.relnamespace::regnamespace::name AS schema_name, t.relname::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_sequence_privilege(r.oid, t.oid, p.perm) AS granted FROM pg_catalog.pg_class AS t CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('SELECT'), ('USAGE'), ('UPDATE')) AS p(perm) WHERE t.relnamespace::regnamespace::name <> 'information_schema' AND t.relnamespace::regnamespace::name NOT LIKE 'pg_%' AND t.relkind = 'S' AND NOT r.rolsuper; GRANT SELECT ON sequence_permissions TO PUBLIC; CREATE VIEW function_permissions AS SELECT obj_type 'FUNCTION' AS object_type, r.rolname AS role_name, f.pronamespace::regnamespace::name AS schema_name, regexp_replace(f.oid::regprocedure::text, '^((("[^"]*")|([^"][^.]*))\.)?', '') AS object_name, NULL::name AS column_name, perm_type 'EXECUTE' AS permission, has_function_privilege(r.oid, f.oid, 'EXECUTE') AS granted FROM pg_catalog.pg_proc f CROSS JOIN pg_catalog.pg_roles AS r WHERE f.pronamespace::regnamespace::name <> 'information_schema' AND f.pronamespace::regnamespace::name NOT LIKE 'pg_%' AND NOT r.rolsuper; GRANT SELECT ON function_permissions TO PUBLIC; CREATE VIEW schema_permissions AS SELECT obj_type 'SCHEMA' AS object_type, r.rolname AS role_name, n.nspname AS schema_name, NULL::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_schema_privilege(r.oid, n.oid, p.perm) AS granted FROM pg_catalog.pg_namespace AS n CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('USAGE'), ('CREATE')) AS p(perm) WHERE n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg_%' AND NOT r.rolsuper; GRANT SELECT ON schema_permissions TO PUBLIC; CREATE VIEW database_permissions AS WITH list AS (SELECT unnest AS perm FROM unnest ('{"CREATE", "CONNECT", "TEMPORARY"}'::text[])) SELECT obj_type 'DATABASE' AS object_type, r.rolname AS role_name, NULL::name AS schema_name, NULL::text AS object_name, NULL::name AS column_name, p.perm::perm_type AS permission, has_database_privilege(r.oid, d.oid, p.perm) AS granted FROM pg_catalog.pg_database AS d CROSS JOIN pg_catalog.pg_roles AS r CROSS JOIN (VALUES ('CREATE'), ('CONNECT'), ('TEMPORARY')) AS p(perm) WHERE d.datname = current_database() AND NOT r.rolsuper; GRANT SELECT ON database_permissions TO PUBLIC; CREATE VIEW all_permissions AS SELECT * FROM table_permissions UNION ALL SELECT * FROM view_permissions UNION ALL SELECT * FROM column_permissions UNION ALL SELECT * FROM sequence_permissions UNION ALL SELECT * FROM function_permissions UNION ALL SELECT * FROM schema_permissions UNION ALL SELECT * FROM database_permissions; GRANT SELECT ON all_permissions TO PUBLIC; /* update trigers for the views */ CREATE FUNCTION permissions_trigger_func() RETURNS TRIGGER LANGUAGE plpgsql AS $$ DECLARE db_name text; cmd text; BEGIN IF NEW.object_type <> OLD.object_type OR NEW.role_name <> OLD.role_name OR NEW.schema_name <> OLD.schema_name OR NEW.object_name <> OLD.object_name OR NEW.column_name <> OLD.column_name OR NEW.permission <> OLD.permission THEN RAISE 'Only the "granted" column may be updated'; END IF; -- Is there anything to do at all? IF NEW.granted = OLD.granted THEN RETURN NEW; END IF; IF OLD.object_type IN ('TABLE', 'VIEW') THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON %s.%s TO %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON %s.%s FROM %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'COLUMN' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s(%s) ON %s.%s TO %s', OLD.permission, OLD.column_name, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s(%s) ON %s.%s FROM %s', OLD.permission, OLD.column_name, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'SEQUENCE' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON SEQUENCE %s.%s TO %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON SEQUENCE %s.%s FROM %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'FUNCTION' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON FUNCTION %s.%s TO %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON FUNCTION %s.%s FROM %s', OLD.permission, OLD.schema_name, OLD.object_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'SCHEMA' THEN IF NOT OLD.granted THEN cmd := format('GRANT %s ON SCHEMA %s TO %s', OLD.permission, OLD.schema_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON SCHEMA %s FROM %s', OLD.permission, OLD.schema_name, OLD.role_name); END IF; ELSIF OLD.object_type = 'DATABASE' THEN db_name := pg_catalog.current_database(); IF NOT OLD.granted THEN cmd := format('GRANT %s ON DATABASE %s TO %s', OLD.permission, db_name, OLD.role_name); ELSE cmd := format('REVOKE %s ON DATABASE %s FROM %s', OLD.permission, db_name, OLD.role_name); END IF; ELSE RAISE 'Unrecognized object type: %', OLD.object_type; END IF; EXECUTE cmd; RETURN NEW; END; $$; CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON table_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON column_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON view_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON sequence_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON function_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON schema_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON database_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); CREATE TRIGGER permissions_trigger INSTEAD OF UPDATE ON all_permissions FOR EACH ROW EXECUTE PROCEDURE permissions_trigger_func(); /* table for the targeted permissions */ CREATE TABLE permission_target ( id int4 PRIMARY KEY, role_name name NOT NULL, permissions perm_type[] NOT NULL, object_type obj_type NOT NULL, schema_name name, object_name text, column_name name, CONSTRAINT permission_target_valid CHECK (CASE WHEN object_type = 'DATABASE' THEN schema_name IS NULL AND object_name IS NULL AND column_name IS NULL AND ARRAY['CONNECT','CREATE','TEMPORARY']::perm_type[] @> permissions WHEN object_type = 'SCHEMA' THEN object_name IS NULL AND column_name IS NULL AND ARRAY['CREATE','USAGE']::perm_type[] @> permissions WHEN object_type IN ('TABLE', 'VIEW') THEN column_name IS NULL AND ARRAY['SELECT','INSERT','UPDATE','DELETE','TRUNCATE','REFERENCES','TRIGGER','MAINTAIN']::perm_type[] @> permissions WHEN object_type = 'SEQUENCE' THEN column_name IS NULL AND ARRAY['SELECT','USAGE','UPDATE']::perm_type[] @> permissions WHEN object_type = 'FUNCTION' THEN column_name IS NULL AND ARRAY['EXECUTE']::perm_type[] @> permissions WHEN object_type = 'COLUMN' THEN ARRAY['SELECT','INSERT','UPDATE','REFERENCES']::perm_type[] @> permissions END) ); CREATE SEQUENCE permission_target_id_seq OWNED BY permission_target.id; ALTER TABLE permission_target ALTER id SET DEFAULT nextval('permission_target_id_seq'::regclass); GRANT SELECT, INSERT, UPDATE, DELETE ON permission_target TO PUBLIC; GRANT USAGE ON SEQUENCE permission_target_id_seq TO PUBLIC; SELECT pg_catalog.pg_extension_config_dump('permission_target', ''); SELECT pg_catalog.pg_extension_config_dump('permission_target_id_seq', ''); CREATE FUNCTION permission_diffs() RETURNS TABLE ( missing boolean, role_name name, object_type obj_type, schema_name name, object_name text, column_name name, permission perm_type ) LANGUAGE plpgsql SET search_path FROM CURRENT AS $$DECLARE typ obj_type; r name; ar name; s name; a_s name; o text; ao text; so name; aso name; p perm_type; ap perm_type; g boolean; ag boolean; BEGIN /* temporary receptacle for reports */ CREATE TEMPORARY TABLE findings ( missing boolean, role_name name, object_type obj_type, schema_name name, object_name text, column_name name, permission perm_type ) ON COMMIT DROP; /* loop through all entries in "permission_target" */ FOR r, p, typ, s, o, so IN SELECT pt.role_name, p.permission, pt.object_type, pt.schema_name, pt.object_name, pt.column_name FROM permission_target AS pt CROSS JOIN LATERAL unnest(pt.permissions) AS p(permission) LOOP /* find all matching object permissions */ FOR ar, ap, a_s, ao, aso, ag IN SELECT apm.role_name, apm.permission, apm.schema_name, apm.object_name, apm.column_name, apm.granted FROM all_permissions AS apm WHERE apm.object_type = typ AND (apm.schema_name = s OR s IS NULL) AND (apm.object_name = o OR o IS NULL) AND (apm.column_name = so OR so IS NULL) LOOP IF ar = r AND ap = p AND NOT ag THEN /* permission not granted that should be */ INSERT INTO findings (missing, role_name, object_type, schema_name, object_name, column_name, permission) VALUES (TRUE, r, typ, a_s, ao, aso, ap); END IF; IF (ar <> r OR ap <> p) AND ag THEN /* different permission found, check if there is a matching rule */ IF NOT EXISTS ( SELECT 1 FROM permission_target AS pt WHERE pt.role_name = ar AND pt.permissions @> ARRAY[ap]::perm_type[] AND pt.object_type = typ AND (pt.schema_name IS NULL OR pt.schema_name = a_s) AND (pt.object_name IS NULL OR pt.object_name = ao) AND (pt.column_name IS NULL OR pt.column_name = aso) ) THEN /* extra permission found, report */ INSERT INTO findings (missing, role_name, object_type, schema_name, object_name, column_name, permission) VALUES (FALSE, ar, typ, a_s, ao, aso, ap); END IF; END IF; END LOOP; END LOOP; RETURN QUERY SELECT DISTINCT * FROM findings; END;$$; cybertec-postgresql-pg_permissions-642eb39/pg_permissions.control000066400000000000000000000002121522050705600255520ustar00rootroot00000000000000comment = 'view object permissions and compare them with the desired state' default_version = '1.4' relocatable = false superuser = false cybertec-postgresql-pg_permissions-642eb39/sql/000077500000000000000000000000001522050705600217135ustar00rootroot00000000000000cybertec-postgresql-pg_permissions-642eb39/sql/sample.sql000066400000000000000000000164061522050705600237240ustar00rootroot00000000000000CREATE EXTENSION pg_permissions; /* test roles (will be removed afterwards) */ CREATE ROLE users; CREATE ROLE user1 LOGIN IN ROLE users; CREATE ROLE user2 LOGIN IN ROLE users; /* database */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('users', ARRAY['CONNECT','TEMPORARY']::perm_type[], 'DATABASE', NULL, NULL, NULL), ('user1', ARRAY['CONNECT','TEMPORARY']::perm_type[], 'DATABASE', NULL, NULL, NULL), ('user2', ARRAY['CONNECT','TEMPORARY']::perm_type[], 'DATABASE', NULL, NULL, NULL); -- this should fail INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user2', ARRAY['CREATE']::perm_type[], 'DATABASE', 'public', NULL, NULL); -- actual permissions REVOKE ALL ON DATABASE contrib_regression FROM PUBLIC; GRANT CONNECT, TEMPORARY ON DATABASE contrib_regression TO users; GRANT CREATE ON DATABASE contrib_regression TO user2; -- too much /* schema */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('users', ARRAY['USAGE']::perm_type[], 'SCHEMA', 'appschema', NULL, NULL), ('user1', ARRAY['USAGE','CREATE']::perm_type[], 'SCHEMA', 'appschema', NULL, NULL), ('user2', ARRAY['USAGE']::perm_type[], 'SCHEMA', 'appschema', NULL, NULL); -- this should fail INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user2', ARRAY['CREATE']::perm_type[], 'SCHEMA', 'appschema', 'sometable', NULL); -- actual permissions CREATE SCHEMA appschema; GRANT USAGE ON SCHEMA appschema TO PUBLIC; -- missing CREATE for user1 GRANT CREATE ON SCHEMA appschema TO user2; -- too much CREATE SCHEMA pgabc123; GRANT USAGE ON SCHEMA pgabc123 TO user1; /* table */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user1', ARRAY['SELECT','INSERT','UPDATE','DELETE']::perm_type[], 'TABLE', 'appschema', NULL, NULL), ('user2', ARRAY['SELECT']::perm_type[], 'TABLE', 'appschema', NULL, NULL), ('user1', ARRAY['SELECT']::perm_type[], 'TABLE', 'pgabc213', 'sometable', NULL); -- this should fail INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user2', ARRAY['INSERT']::perm_type[], 'TABLE', 'appschema', 'apptable', 'acolumn'); -- actual permissions CREATE TABLE appschema.apptable ( id integer PRIMARY KEY, val text NOT NULL, created timestamp with time zone NOT NULL DEFAULT current_timestamp ); CREATE TABLE appschema.apptable2 ( id integer PRIMARY KEY, val text NOT NULL, created timestamp with time zone NOT NULL DEFAULT current_timestamp ); -- missing all permissions on this one CREATE TABLE pgabc123.sometable ( id integer PRIMARY KEY, val text NOT NULL, created timestamp with time zone NOT NULL DEFAULT current_timestamp ); GRANT SELECT, INSERT, UPDATE ON appschema.apptable TO user1; -- missing DELETE GRANT SELECT, INSERT ON appschema.apptable TO user2; -- extra privilege INSERT GRANT SELECT ON pgabc123.sometable TO user1; /* column */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user1', ARRAY['SELECT','INSERT','UPDATE','REFERENCES']::perm_type[], 'COLUMN', 'appschema', 'apptable2', 'val'); -- this should fail INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user2', ARRAY['DELETE']::perm_type[], 'COLUMN', 'appschema', 'apptable2', 'val'); -- actual permissions -- missing REFERENCES for user1 on apptable2.val GRANT UPDATE (val) ON appschema.apptable2 TO user2; -- extra privilege UPDATE /* view */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user1', ARRAY['SELECT','INSERT','UPDATE','DELETE']::perm_type[], 'VIEW', 'appschema', 'appview', NULL), ('user2', ARRAY['SELECT']::perm_type[], 'VIEW', 'appschema', 'appview', NULL); -- actual permissions CREATE VIEW appschema.appview AS SELECT id, val FROM appschema.apptable; GRANT SELECT ON appschema.appview TO users; -- extra permission to "users" GRANT INSERT, DELETE ON appschema.appview TO user1; -- missing UPDATE /* sequence */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('users', ARRAY['USAGE']::perm_type[], 'SEQUENCE', 'appschema', 'appseq', NULL), ('user1', ARRAY['USAGE','SELECT']::perm_type[], 'SEQUENCE', 'appschema', 'appseq', NULL), ('user2', ARRAY['USAGE']::perm_type[], 'SEQUENCE', 'appschema', 'appseq', NULL); -- actual permissions CREATE SEQUENCE appschema.appseq; GRANT USAGE ON SEQUENCE appschema.appseq TO users; -- missing SELECT for user1 GRANT UPDATE ON SEQUENCE appschema.appseq TO user2; -- extra permission UPDATE /* function */ -- desired permissions INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('user1', ARRAY['EXECUTE']::perm_type[], 'FUNCTION', 'appschema', 'appfun(integer)', NULL), ('user2', ARRAY['EXECUTE']::perm_type[], 'FUNCTION', 'appschema', 'appfun(integer)', NULL); -- this should fail INSERT INTO permission_target (role_name, permissions, object_type, schema_name, object_name, column_name) VALUES ('users', ARRAY['UPDATE']::perm_type[], 'FUNCTION', 'appschema', 'appfun(integer)', NULL); -- actual permissions CREATE FUNCTION appschema.appfun(i integer) RETURNS integer LANGUAGE sql IMMUTABLE AS 'SELECT i + 2'; -- extra permission for "users" /* report all permissions */ SELECT object_type, role_name, schema_name, object_name, column_name, permission FROM all_permissions WHERE granted AND role_name IN ('users', 'user1', 'user2') AND coalesce(schema_name, 'appschema') IN ('appschema', 'pgabc123') ORDER BY object_type, role_name, schema_name, object_name, column_name, permission; /* report differences */ SELECT * FROM permission_diffs() WHERE role_name IN ('users', 'user1', 'user2') ORDER BY object_type, schema_name, object_name, column_name, role_name, permission, missing; /* fix some of the differences */ UPDATE column_permissions SET granted = TRUE WHERE role_name = 'user1' AND schema_name = 'appschema' AND object_name = 'apptable2' AND column_name = 'val' AND permission = 'REFERENCES'; UPDATE all_permissions SET granted = FALSE WHERE object_type = 'TABLE' AND role_name = 'user2' AND schema_name = 'appschema' AND object_name = 'apptable' AND permission = 'INSERT'; /* check the fixed permissions */ SELECT * FROM permission_diffs() WHERE role_name IN ('users', 'user1', 'user2') ORDER BY object_type, schema_name, object_name, column_name, role_name, permission, missing; /* clean up */ DROP FUNCTION appschema.appfun(integer); DROP VIEW appschema.appview; DROP SEQUENCE appschema.appseq; DROP TABLE appschema.apptable; DROP TABLE appschema.apptable2; DROP TABLE pgabc123.sometable; DROP SCHEMA appschema; DROP SCHEMA pgabc123; REVOKE ALL ON DATABASE contrib_regression FROM user1, user2, users; DROP ROLE user1; DROP ROLE user2; DROP ROLE users; cybertec-postgresql-pg_permissions-642eb39/test/000077500000000000000000000000001522050705600220735ustar00rootroot00000000000000cybertec-postgresql-pg_permissions-642eb39/test/Dockerfile000066400000000000000000000042271522050705600240720ustar00rootroot00000000000000# Build and regression-test pg_permissions against a single PostgreSQL major # version. The version is selected with the PG_MAJOR build argument, e.g. # # docker build --build-arg PG_MAJOR=17 -f test/Dockerfile -t pg_permissions-test:17 . # # Packages are taken from the PostgreSQL APT repository (apt.postgresql.org). # Versions that have not been released yet (currently 19) are pulled from the # additional "-pgdg-snapshot" repository. FROM debian:bookworm-slim ARG PG_MAJOR=16 ENV PG_MAJOR=${PG_MAJOR} ENV DEBIAN_FRONTEND=noninteractive # PostgreSQL APT repository plus the build tool chain. The snapshot repository # is added unconditionally but only carries packages for not-yet-released major # versions, so it does no harm for released ones. RUN set -eux; \ apt-get update; \ apt-get install -y --no-install-recommends \ ca-certificates gnupg lsb-release wget make gcc; \ install -d /usr/share/postgresql-common/pgdg; \ wget -qO /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \ https://www.postgresql.org/media/keys/ACCC4CF8.asc; \ codename="$(lsb_release -cs)"; \ printf 'deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt %s-pgdg main %s\n' \ "$codename" "$PG_MAJOR" > /etc/apt/sources.list.d/pgdg.list; \ printf 'deb [signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt %s-pgdg-snapshot main %s\n' \ "$codename" "$PG_MAJOR" > /etc/apt/sources.list.d/pgdg-snapshot.list; \ apt-get update; \ apt-get install -y --no-install-recommends \ "postgresql-${PG_MAJOR}" "postgresql-server-dev-${PG_MAJOR}"; \ rm -rf /var/lib/apt/lists/* ENV PATH=/usr/lib/postgresql/${PG_MAJOR}/bin:$PATH WORKDIR /work COPY . /work # Install the extension into the server packages (needs root). RUN make install PG_CONFIG="/usr/lib/postgresql/${PG_MAJOR}/bin/pg_config" # The regression test must not run as root; pg_virtualenv spins up a throwaway # cluster owned by this user for the duration of the test. RUN chown -R postgres:postgres /work USER postgres CMD ["test/run-installcheck.sh"] cybertec-postgresql-pg_permissions-642eb39/test/README.md000066400000000000000000000025161522050705600233560ustar00rootroot00000000000000Containerised regression tests ============================== These files run the regression suite (`make installcheck`) against every supported PostgreSQL major version inside throwaway Docker containers, so you do not need a local PostgreSQL installation of each version. Requirements ------------ - Docker (with the daemon running and reachable by the current user) Usage ----- Run all supported versions (currently 12 through 19) from the repository root: test/run-tests.sh Or test only specific versions: test/run-tests.sh 17 18 19 The script builds one image per version and runs the suite in it, then prints a summary of which versions passed and which failed. The exit status is non-zero if any version failed. How it works ------------ `Dockerfile` takes a `PG_MAJOR` build argument and installs that major version from the PostgreSQL APT repository (apt.postgresql.org), including the `-pgdg-snapshot` repository so that not-yet-released versions (currently 19) can be tested. The extension is built and installed during the image build; the container then uses `pg_virtualenv` to spin up a temporary cluster and runs `make installcheck` against it. To build and test a single version by hand: docker build --build-arg PG_MAJOR=17 -f test/Dockerfile -t pg_permissions-test:17 . docker run --rm pg_permissions-test:17 cybertec-postgresql-pg_permissions-642eb39/test/run-installcheck.sh000077500000000000000000000011721522050705600257010ustar00rootroot00000000000000#!/bin/sh # Run the regression suite against a throwaway cluster of the major version that # was baked into this image. Executed as the container's default command; not # meant to be run directly on a developer machine (see run-tests.sh for that). set -eu cd "$(dirname "$0")/.." echo "=== regression test for PostgreSQL ${PG_MAJOR} ===" if pg_virtualenv -v "${PG_MAJOR}" make installcheck; then echo "PostgreSQL ${PG_MAJOR}: PASS" else status=$? echo "PostgreSQL ${PG_MAJOR}: FAIL" if [ -f regression.diffs ]; then echo "--- regression.diffs ---" cat regression.diffs fi exit "$status" fi cybertec-postgresql-pg_permissions-642eb39/test/run-tests.sh000077500000000000000000000017751522050705600244100ustar00rootroot00000000000000#!/bin/sh # Build a container for each requested PostgreSQL major version and run the # regression suite inside it. With no arguments every supported version is # tested; otherwise the arguments are taken as the list of versions, e.g. # # test/run-tests.sh # 12 13 14 15 16 17 18 19 # test/run-tests.sh 17 18 19 # only these # # Must be run from a checkout with Docker available. set -eu VERSIONS="${*:-12 13 14 15 16 17 18 19}" # Resolve the repository root so the script works from anywhere. cd "$(CDPATH= cd "$(dirname "$0")/.." && pwd)" passed="" failed="" for v in $VERSIONS; do echo echo "########## PostgreSQL $v ##########" if docker build --build-arg "PG_MAJOR=$v" -f test/Dockerfile -t "pg_permissions-test:$v" . \ && docker run --rm "pg_permissions-test:$v"; then passed="$passed $v" else failed="$failed $v" fi done echo echo "########## summary ##########" echo "passed:${passed:- none}" echo "failed:${failed:- none}" [ -z "$failed" ]