Skip to content

Commit 4072e0a

Browse files
committed
Add containerised tests for PG 12-19 and expand documentation
- 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.
1 parent f622556 commit 4072e0a

7 files changed

Lines changed: 427 additions & 2 deletions

File tree

.dockerignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.git
2+
.github
3+
results
4+
regression.diffs
5+
regression.out

.github/workflows/regression.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ jobs:
1313
strategy:
1414
matrix:
1515
pgversion:
16+
- 19
1617
- 18
1718
- 17
1819
- 16

README.md

Lines changed: 275 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,8 +100,18 @@ The extension provides a number of views:
100100

101101
- `all_permissions`: permissions on all objects (`UNION` of the above)
102102

103-
All views have the same columns; a column is NULL if it has no meaning
104-
for the current view.
103+
All views share the same columns:
104+
105+
- `object_type`: the kind of object (`TABLE`, `VIEW`, `COLUMN`, `SEQUENCE`,
106+
`FUNCTION`, `SCHEMA` or `DATABASE`)
107+
- `role_name`: the role the permission applies to
108+
- `schema_name`, `object_name`, `column_name`: identify the object
109+
- `permission`: the privilege in question (`SELECT`, `USAGE`, `EXECUTE`, ...)
110+
- `granted`: whether the privilege is currently in effect (`t`) or not (`f`)
111+
112+
A column is NULL if it has no meaning for the current view; for example,
113+
`column_name` is only set in `column_permissions`, and `schema_name` is
114+
always NULL in `database_permissions`.
105115

106116
These views can be used to examine the currently granted permissions on
107117
database objects.
@@ -112,6 +122,187 @@ appropriate `GRANT` or `REVOKE` command to be executed.
112122
**Note:** Superusers are not shown in the views, as they automatically have all
113123
permissions.
114124

125+
#### Examples ####
126+
127+
The examples in this section assume the following objects and grants:
128+
129+
CREATE ROLE appuser LOGIN;
130+
CREATE ROLE reporting LOGIN;
131+
CREATE ROLE auditor LOGIN;
132+
133+
CREATE SCHEMA appschema;
134+
CREATE TABLE appschema.invoice (
135+
id integer PRIMARY KEY,
136+
customer text NOT NULL,
137+
amount numeric NOT NULL
138+
);
139+
CREATE VIEW appschema.invoice_v AS SELECT id, customer FROM appschema.invoice;
140+
CREATE SEQUENCE appschema.invoice_id_seq;
141+
CREATE FUNCTION appschema.total() RETURNS numeric LANGUAGE sql
142+
AS 'SELECT sum(amount) FROM appschema.invoice';
143+
144+
GRANT USAGE ON SCHEMA appschema TO appuser, reporting, auditor;
145+
GRANT SELECT, INSERT, UPDATE, DELETE ON appschema.invoice TO appuser;
146+
GRANT SELECT ON appschema.invoice TO reporting;
147+
GRANT SELECT ON appschema.invoice_v TO reporting;
148+
GRANT SELECT (customer) ON appschema.invoice TO auditor;
149+
GRANT USAGE ON SEQUENCE appschema.invoice_id_seq TO appuser;
150+
REVOKE EXECUTE ON FUNCTION appschema.total() FROM PUBLIC;
151+
GRANT EXECUTE ON FUNCTION appschema.total() TO reporting;
152+
153+
`schema_permissions` shows the `USAGE` and `CREATE` privileges on schemas:
154+
155+
SELECT object_type, role_name, schema_name, permission, granted
156+
FROM schema_permissions
157+
WHERE schema_name = 'appschema' AND role_name IN ('appuser', 'reporting');
158+
159+
object_type | role_name | schema_name | permission | granted
160+
-------------+-----------+-------------+------------+---------
161+
SCHEMA | appuser | appschema | USAGE | t
162+
SCHEMA | appuser | appschema | CREATE | f
163+
SCHEMA | reporting | appschema | USAGE | t
164+
SCHEMA | reporting | appschema | CREATE | f
165+
166+
`table_permissions` lists every table privilege for each role; `granted` tells
167+
you which ones are actually in effect:
168+
169+
SELECT role_name, object_name, permission, granted
170+
FROM table_permissions
171+
WHERE schema_name = 'appschema' AND role_name IN ('appuser', 'reporting');
172+
173+
role_name | object_name | permission | granted
174+
-----------+-------------+------------+---------
175+
appuser | invoice | SELECT | t
176+
appuser | invoice | INSERT | t
177+
appuser | invoice | UPDATE | t
178+
appuser | invoice | DELETE | t
179+
appuser | invoice | TRUNCATE | f
180+
appuser | invoice | REFERENCES | f
181+
appuser | invoice | TRIGGER | f
182+
reporting | invoice | SELECT | t
183+
reporting | invoice | INSERT | f
184+
reporting | invoice | UPDATE | f
185+
reporting | invoice | DELETE | f
186+
reporting | invoice | TRUNCATE | f
187+
reporting | invoice | REFERENCES | f
188+
reporting | invoice | TRIGGER | f
189+
190+
`view_permissions` is the same, but for views:
191+
192+
SELECT role_name, object_name, permission, granted
193+
FROM view_permissions
194+
WHERE schema_name = 'appschema' AND role_name = 'reporting';
195+
196+
role_name | object_name | permission | granted
197+
-----------+-------------+------------+---------
198+
reporting | invoice_v | SELECT | t
199+
reporting | invoice_v | INSERT | f
200+
reporting | invoice_v | UPDATE | f
201+
reporting | invoice_v | DELETE | f
202+
reporting | invoice_v | TRUNCATE | f
203+
reporting | invoice_v | REFERENCES | f
204+
reporting | invoice_v | TRIGGER | f
205+
206+
`column_permissions` reports column level privileges. Note that `granted` is
207+
only `t` when the privilege is granted *on the column itself*; a privilege held
208+
at the table level is not repeated here (use `table_permissions` for that).
209+
Here `auditor` may read only the `customer` column:
210+
211+
SELECT role_name, object_name, column_name, permission, granted
212+
FROM column_permissions
213+
WHERE schema_name = 'appschema' AND object_name = 'invoice' AND role_name = 'auditor';
214+
215+
role_name | object_name | column_name | permission | granted
216+
-----------+-------------+-------------+------------+---------
217+
auditor | invoice | amount | SELECT | f
218+
auditor | invoice | amount | INSERT | f
219+
auditor | invoice | amount | UPDATE | f
220+
auditor | invoice | amount | REFERENCES | f
221+
auditor | invoice | customer | SELECT | t
222+
auditor | invoice | customer | INSERT | f
223+
auditor | invoice | customer | UPDATE | f
224+
auditor | invoice | customer | REFERENCES | f
225+
auditor | invoice | id | SELECT | f
226+
auditor | invoice | id | INSERT | f
227+
auditor | invoice | id | UPDATE | f
228+
auditor | invoice | id | REFERENCES | f
229+
230+
`sequence_permissions` covers the `SELECT`, `USAGE` and `UPDATE` privileges on
231+
sequences:
232+
233+
SELECT role_name, object_name, permission, granted
234+
FROM sequence_permissions
235+
WHERE schema_name = 'appschema' AND role_name IN ('appuser', 'reporting');
236+
237+
role_name | object_name | permission | granted
238+
-----------+----------------+------------+---------
239+
appuser | invoice_id_seq | SELECT | f
240+
appuser | invoice_id_seq | UPDATE | f
241+
appuser | invoice_id_seq | USAGE | t
242+
reporting | invoice_id_seq | SELECT | f
243+
reporting | invoice_id_seq | UPDATE | f
244+
reporting | invoice_id_seq | USAGE | f
245+
246+
`function_permissions` reports the `EXECUTE` privilege on functions. Keep in
247+
mind that functions are executable by `PUBLIC` by default, so `REVOKE ... FROM
248+
PUBLIC` is usually needed before per-role grants are meaningful:
249+
250+
SELECT role_name, object_name, permission, granted
251+
FROM function_permissions
252+
WHERE schema_name = 'appschema' AND role_name IN ('appuser', 'reporting');
253+
254+
role_name | object_name | permission | granted
255+
-----------+-------------+------------+---------
256+
appuser | total() | EXECUTE | f
257+
reporting | total() | EXECUTE | t
258+
259+
`database_permissions` shows the `CREATE`, `CONNECT` and `TEMPORARY` privileges
260+
on the current database (`CONNECT` and `TEMPORARY` are granted to `PUBLIC` by
261+
default):
262+
263+
SELECT role_name, permission, granted
264+
FROM database_permissions
265+
WHERE role_name IN ('appuser', 'reporting');
266+
267+
role_name | permission | granted
268+
-----------+------------+---------
269+
appuser | CREATE | f
270+
appuser | CONNECT | t
271+
appuser | TEMPORARY | t
272+
reporting | CREATE | f
273+
reporting | CONNECT | t
274+
reporting | TEMPORARY | t
275+
276+
`all_permissions` is the union of all of the above. Filtering on
277+
`granted` gives a concise overview of everything a set of roles can actually do:
278+
279+
SELECT object_type, role_name, object_name, column_name, permission
280+
FROM all_permissions
281+
WHERE granted AND role_name IN ('appuser', 'reporting', 'auditor')
282+
AND coalesce(schema_name, 'appschema') = 'appschema'
283+
ORDER BY object_type, role_name, object_name, column_name, permission;
284+
285+
object_type | role_name | object_name | column_name | permission
286+
-------------+-----------+----------------+-------------+------------
287+
TABLE | appuser | invoice | | SELECT
288+
TABLE | appuser | invoice | | INSERT
289+
TABLE | appuser | invoice | | UPDATE
290+
TABLE | appuser | invoice | | DELETE
291+
TABLE | reporting | invoice | | SELECT
292+
VIEW | reporting | invoice_v | | SELECT
293+
COLUMN | auditor | invoice | customer | SELECT
294+
SEQUENCE | appuser | invoice_id_seq | | USAGE
295+
FUNCTION | reporting | total() | | EXECUTE
296+
SCHEMA | appuser | | | USAGE
297+
SCHEMA | auditor | | | USAGE
298+
SCHEMA | reporting | | | USAGE
299+
DATABASE | appuser | | | CONNECT
300+
DATABASE | appuser | | | TEMPORARY
301+
DATABASE | auditor | | | CONNECT
302+
DATABASE | auditor | | | TEMPORARY
303+
DATABASE | reporting | | | CONNECT
304+
DATABASE | reporting | | | TEMPORARY
305+
115306
### Tables ###
116307

117308
The extension provides a table `permission_target` with which you can describe
@@ -133,6 +324,50 @@ be there but isn't; if `missing` is `FALSE`, the result row is a permission that
133324
is there even though it is not defined in `permission_target` (an extra
134325
permission).
135326

327+
#### Comparing desired and actual state ####
328+
329+
Suppose `appuser` should be able to read and write all tables in `appschema`,
330+
while `reporting` should only be allowed to read them. We record that as the
331+
desired state:
332+
333+
INSERT INTO permission_target (role_name, permissions, object_type, schema_name)
334+
VALUES ('appuser', '{SELECT,INSERT,UPDATE,DELETE}', 'TABLE', 'appschema'),
335+
('reporting', '{SELECT}', 'TABLE', 'appschema');
336+
337+
Over time the actual grants drifted: `appuser` never received `DELETE`, and
338+
`reporting` was accidentally given `INSERT`:
339+
340+
GRANT SELECT, INSERT, UPDATE ON appschema.invoice TO appuser;
341+
GRANT SELECT, INSERT ON appschema.invoice TO reporting;
342+
343+
`permission_diffs()` reports exactly these two deviations:
344+
345+
SELECT * FROM permission_diffs()
346+
WHERE role_name IN ('appuser', 'reporting');
347+
348+
missing | role_name | object_type | schema_name | object_name | column_name | permission
349+
---------+-----------+-------------+-------------+-------------+-------------+------------
350+
t | appuser | TABLE | appschema | invoice | | DELETE
351+
f | reporting | TABLE | appschema | invoice | | INSERT
352+
353+
The `DELETE` privilege is missing for `appuser` (`missing` is `t`), while
354+
`reporting` holds a surplus `INSERT` privilege (`missing` is `f`). Because the
355+
permission views are updatable, both can be corrected without writing `GRANT`
356+
or `REVOKE` by hand:
357+
358+
-- grant the missing privilege
359+
UPDATE table_permissions SET granted = true
360+
WHERE role_name = 'appuser' AND schema_name = 'appschema'
361+
AND object_name = 'invoice' AND permission = 'DELETE';
362+
363+
-- revoke the surplus privilege
364+
UPDATE table_permissions SET granted = false
365+
WHERE role_name = 'reporting' AND schema_name = 'appschema'
366+
AND object_name = 'invoice' AND permission = 'INSERT';
367+
368+
Running `permission_diffs()` again now returns no rows: the actual state
369+
matches the desired state.
370+
136371
Installation
137372
------------
138373

@@ -177,6 +412,44 @@ subdirectory of that directory, e.g.
177412

178413
You still have to run `CREATE EXTENSION` as described above.
179414

415+
Compliance
416+
----------
417+
418+
Virtually every security and data-protection framework requires that access to
419+
data be limited to those who need it (the principle of least privilege), that
420+
this access be reviewed regularly, and that the review be auditable.
421+
`pg_permissions` directly supports those obligations: you declare the intended
422+
permissions in `permission_target`, prove the current state with the
423+
`*_permissions` views, and detect any drift with `permission_diffs()`.
424+
425+
This makes the extension useful when demonstrating database access controls
426+
for, among others:
427+
428+
- **SOX** (Sarbanes-Oxley): IT general controls under section 404 — evidence
429+
that access to financial data is restricted and periodically reviewed.
430+
431+
- **PCI DSS**: requirements 7 and 8, which mandate restricting access to
432+
cardholder data on a business need-to-know basis.
433+
434+
- **GDPR**: Article 32 (security of processing), which expects access to
435+
personal data to be limited to what is necessary.
436+
437+
- **HIPAA**: the Security Rule's access-control standard
438+
(§ 164.312(a)) for electronic protected health information.
439+
440+
- **ISO/IEC 27001 / 27002**: the access-control objectives (Annex A.9 /
441+
control 5.15 and following) covering formal entitlement and periodic review.
442+
443+
- **SOC 2**: the Security and Confidentiality trust-services criteria around
444+
logical access.
445+
446+
- **DORA**: the EU Digital Operational Resilience Act's access-management
447+
requirements for financial entities.
448+
449+
The extension does not enforce these controls on its own; it provides the
450+
reporting and reconciliation you need to define a desired state, prove the
451+
current state, and catch any divergence between audits.
452+
180453
Support
181454
-------
182455

test/Dockerfile

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# Build and regression-test pg_permissions against a single PostgreSQL major
2+
# version. The version is selected with the PG_MAJOR build argument, e.g.
3+
#
4+
# docker build --build-arg PG_MAJOR=17 -f test/Dockerfile -t pg_permissions-test:17 .
5+
#
6+
# Packages are taken from the PostgreSQL APT repository (apt.postgresql.org).
7+
# Versions that have not been released yet (currently 19) are pulled from the
8+
# additional "-pgdg-snapshot" repository.
9+
FROM debian:bookworm-slim
10+
11+
ARG PG_MAJOR=16
12+
ENV PG_MAJOR=${PG_MAJOR}
13+
ENV DEBIAN_FRONTEND=noninteractive
14+
15+
# PostgreSQL APT repository plus the build tool chain. The snapshot repository
16+
# is added unconditionally but only carries packages for not-yet-released major
17+
# versions, so it does no harm for released ones.
18+
RUN set -eux; \
19+
apt-get update; \
20+
apt-get install -y --no-install-recommends \
21+
ca-certificates gnupg lsb-release wget make gcc; \
22+
install -d /usr/share/postgresql-common/pgdg; \
23+
wget -qO /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc \
24+
https://www.postgresql.org/media/keys/ACCC4CF8.asc; \
25+
codename="$(lsb_release -cs)"; \
26+
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' \
27+
"$codename" "$PG_MAJOR" > /etc/apt/sources.list.d/pgdg.list; \
28+
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' \
29+
"$codename" "$PG_MAJOR" > /etc/apt/sources.list.d/pgdg-snapshot.list; \
30+
apt-get update; \
31+
apt-get install -y --no-install-recommends \
32+
"postgresql-${PG_MAJOR}" "postgresql-server-dev-${PG_MAJOR}"; \
33+
rm -rf /var/lib/apt/lists/*
34+
35+
ENV PATH=/usr/lib/postgresql/${PG_MAJOR}/bin:$PATH
36+
37+
WORKDIR /work
38+
COPY . /work
39+
40+
# Install the extension into the server packages (needs root).
41+
RUN make install PG_CONFIG="/usr/lib/postgresql/${PG_MAJOR}/bin/pg_config"
42+
43+
# The regression test must not run as root; pg_virtualenv spins up a throwaway
44+
# cluster owned by this user for the duration of the test.
45+
RUN chown -R postgres:postgres /work
46+
USER postgres
47+
48+
CMD ["test/run-installcheck.sh"]

0 commit comments

Comments
 (0)