pax_global_header00006660000000000000000000000064152723261120014513gustar00rootroot0000000000000052 comment=45fc481de6874c2afc5199d7efbd2e32c66d702e pgaudit-set_user-45fc481/000077500000000000000000000000001527232611200153375ustar00rootroot00000000000000pgaudit-set_user-45fc481/.editorconfig000066400000000000000000000003001527232611200200050ustar00rootroot00000000000000root = true [*] indent_style = tab indent_size = 4 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [Makefile] indent_style = tab indent_size = 4 pgaudit-set_user-45fc481/.github/000077500000000000000000000000001527232611200166775ustar00rootroot00000000000000pgaudit-set_user-45fc481/.github/workflows/000077500000000000000000000000001527232611200207345ustar00rootroot00000000000000pgaudit-set_user-45fc481/.github/workflows/test.yml000066400000000000000000000017451527232611200224450ustar00rootroot00000000000000on: push: branches: - integration - '**-ci' pull_request: branches: - master - integration jobs: test: runs-on: ubuntu-latest strategy: # Let all the jobs run to completion even if one fails fail-fast: false # Test all supported versions matrix: pgver: [14, 15, 16, 17, 18, 19] steps: - name: Checkout Code uses: actions/checkout@v6 with: path: set_user - name: Build Test Container run: docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) --build-arg PGVER=${{matrix.pgver}} -f ${GITHUB_WORKSPACE?}/set_user/test/Dockerfile.debian -t set_user-test ${GITHUB_WORKSPACE?}/set_user - name: Run Test run: docker run -v ${GITHUB_WORKSPACE?}/set_user:/set_user set_user-test /set_user/test/test.sh - name: Show Any Regression Diffs if: ${{ failure() }} run: | cat ${GITHUB_WORKSPACE?}/set_user/regression.diffs pgaudit-set_user-45fc481/.gitignore000066400000000000000000000002241527232611200173250ustar00rootroot00000000000000# Derived objects set_user.o set_user.so set_user.bc results # Generated documentation *.pdf # Generated extension file extension/set_user--*.sql pgaudit-set_user-45fc481/CHANGELOG.md000066400000000000000000000045061527232611200171550ustar00rootroot000000000000004.3.0 ===== SECURITY FIXES -------------- - Fix a backend crash when `RESET ALL` is run while `set_user()` is active. The crash caused the postmaster to restart all sessions. - Block `DISCARD ALL` while `set_user()` is active. `DISCARD ALL` reverted the session to the original role without the reset token and without logging the role transition. Call `reset_user()` before `DISCARD ALL`. - Block function `SET` clauses for `role` and `session_authorization` (and `log_statement` when `set_user.block_log_statement` is on) while `set_user()` is active. These functions reverted the session to the original role without the reset token and without logging the role transition. These functions can no longer be created, altered or called while `set_user()` is active and calling one from a trigger terminates the session. - Match parameter names case-insensitively when blocking `SET` and `RESET`. Quoted names such as `SET "Role"` bypassed the block. BUGFIXES -------- - Fix allowlist matching so a group entry no longer overrides an earlier match. - Match role names in allowlists case-sensitively. Mixed-case role names must now be quoted, e.g. `'"Alice"'`. - Compare reset tokens in constant time. NEW FEATURES ------------ - Remove support for PostgreSQL 13. - Document the limits of the reset token. 4.2.0 ===== NEW FEATURES ------------ - Add PG_MODULE_MAGIC_EXT() macro for PostgreSQL >= 18. - Remove support for PostgreSQL 12. 4.1.0 ===== NEW FEATURES ------------ - Add PostgreSQL 17 support. - Remove support for PostgreSQL < 12. 4.0.1 ===== NEW FEATURES ------------ - Reorganized repository structure to allow for easier management of extension files during build process. - Added NO_PGXS build flag to allow building of extension without PGXS. Restores ability to build on Windows. - No changes to extension code. 2.0.1 ===== NEW FEATURES ------------ - Deprecated GUCs are removed from `SHOW ALL`. BUGFIXES -------- - NOTICE fixed to only display on first reference to non-default deprecated variable. 2.0.0 ===== NEW FEATURES ------------ - Use of GUCs with `whitelist` have been deprecated in lieu of a more appropriate `allowlist`. The last GUC set by `ALTER SYSTEM` will be used on reload, the first attempt to `SHOW` a deprecated variable will provide a NOTICE. - The extension is now non-relocatable and all functions are schema-qualified. pgaudit-set_user-45fc481/LICENSE000066400000000000000000000020611527232611200163430ustar00rootroot00000000000000This code is released under the PostgreSQL license. Copyright 2015-2025 Crunchy Data Solutions, Inc. 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 CRUNCHY DATA SOLUTIONS, INC. 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 CRUNCHY DATA SOLUTIONS, INC. HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. THE CRUNCHY DATA SOLUTIONS, INC. 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 THE CRUNCHY DATA SOLUTIONS, INC. HAS NO OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. pgaudit-set_user-45fc481/Makefile000066400000000000000000000021711527232611200170000ustar00rootroot00000000000000EXTENSION = set_user EXTVERSION = $(shell grep default_version $(EXTENSION).control | \ sed -e "s/default_version[[:space:]]*=[[:space:]]*'\([^']*\)'/\1/") LDFLAGS_SL += $(filter -lm, $(LIBS)) PG_CPPFLAGS = -DEXTVERSION="\"$(EXTVERSION)\"" MODULES = src/set_user PG_CONFIG = pg_config PGFILEDESC = "set_user - similar to SET ROLE but with added logging" REGRESS = set_user all: extension/$(EXTENSION)--$(EXTVERSION).sql extension/$(EXTENSION)--$(EXTVERSION).sql: extension/set_user.sql cat $^ > $@ DATA = $(wildcard updates/*--*.sql) extension/$(EXTENSION)--$(EXTVERSION).sql EXTRA_CLEAN = extension/$(EXTENSION)--$(EXTVERSION).sql ifdef NO_PGXS subdir = contrib/set_user top_builddir = ../.. include $(top_builddir)/src/Makefile.global include $(top_srcdir)/contrib/contrib-global.mk else PGXS := $(shell $(PG_CONFIG) --pgxs) include $(PGXS) endif .PHONY: install-headers uninstall-headers install: install-headers install-headers: $(MKDIR_P) "$(DESTDIR)$(includedir)" $(INSTALL_DATA) "src/set_user.h" "$(DESTDIR)$(includedir)" uninstall: uninstall-headers uninstall-headers: rm "$(DESTDIR)$(includedir)/set_user.h" pgaudit-set_user-45fc481/README.md000066400000000000000000000623061527232611200166250ustar00rootroot00000000000000# PostgreSQL set_user Extension Module ## Syntax ``` set_user(text rolename) returns text set_user(text rolename, text token) returns text set_user_u(text rolename) returns text reset_user() returns text reset_user(text token) returns text set_session_auth(text rolename) returns text ``` ## Inputs `rolename` is the role to be transitioned to. `token` if provided during set_user is saved, and then required to be provided again for reset. ## Configuration Options * Add `set_user` to `shared_preload_libraries` in postgresql.conf. * Optionally, the following custom parameters may be set to control their respective commands: * set_user.block_alter_system = off (defaults to "on") * set_user.block_copy_program = off (defaults to "on") * set_user.block_log_statement = off (defaults to "on") * set_user.superuser_allowlist = `''` * `` can contain any of the following: * list of user roles (i.e. `, ,...,`) * Group roles may be indicated by `+` * The wildcard character `*` * set_user.nosuperuser_target_allowlist = `''` * `` can contain any of the following: * list of user roles (i.e. `, ,...,`) * Group roles may be indicated by `+` * The wildcard character `*` * set_user.exit_on_error = off (defaults to "on") * To make use of the optional `set_user` and `reset_user` hooks, please refer to the [hooks](#post-execution-hooks) section. ## Description This PostgreSQL extension allows switching users and optional privilege escalation with enhanced logging and control. It provides an additional layer of logging and control when unprivileged users must escalate themselves to superuser or object owner roles in order to perform needed maintenance tasks. Specifically, when an allowed user executes `set_user(text)` or `set_user_u(text)`, several actions occur: * The current effective user becomes `rolename`. * The role transition is logged, with a specific notation if `rolename` is a superuser. * If `set_user.block_alter_system` is set to "on", `ALTER SYSTEM` commands will be blocked. * If `set_user.block_copy_program` is set to "on", `COPY PROGRAM` commands will be blocked. * If `set_user.block_log_statement` is set to "on", `SET log_statement` and variations will be blocked. * If `set_user.block_log_statement` is set to "on" and `rolename` is a database superuser, the current `log_statement` setting is changed to "all", meaning every SQL statement executed * If `set_user.superuser_audit_tag` is set, the string value will be appended to `log_line_prefix` upon superuser escalation. All logs after superuser escalation will be tagged with the value of `set_user.superuser_audit_tag`. This value defaults to `'AUDIT'`. * If `set_user.exit_on_error` is set to "on", the backend process will exit on ERROR during calls to set_session_auth(). * [Post-execution hook](#post_set_user_hook) for `set_user` is called if it is set. Only users with `EXECUTE` permission on `set_user_u(text)` may escalate to superuser. Additionally, all rules in [Superuser Allowlist](#set_usersuperuser_allowlist-rules-and-logic) apply to `set_user.superuser_allowlist` and `set_user_u(text)`. Postgres roles calling `set_user(text)` can only transition to roles listed or included in `set_user.nosuperuser_target_allowlist` (defaults to all roles). Additionally the logic in [Nosuperuser Allowlist](#set_usernosuperuser_target_allowlist-rules-and-logic) applies to `current_user` when `set_user()` is invoked. Additionally, with `set_user('rolename','token')` the `token` is stored for the lifetime of the session. When finished with required actions as `rolename`, the `reset_user()` function is executed to restore the original user. At that point, these actions occur: * Role transition is logged. * `log_statement` setting is set to its original value. * Blocked command behaviors return to normal. * [Post-execution hook](#post_reset_user_hook) for `reset_user` is called if it is set. If `set_user`, was provided with a `token`, then `reset_user('token')` must be called instead of `reset_user()`: * The provided `token` is compared with the stored token. * If the tokens do not match, or if a `token` was provided to `set_user` but not `reset_user`, an ERROR occurs. See [Caveats](#caveats) for the limits of the `token`. When set_session_auth(text) is called, the effective session and current user is switched to the rolename supplied, irrevocably. Unlike set_user() or set_user_u(), it does not affect logging nor allowed statements. If `set_user.exit_on_error` is "on" (the default), and any error occurs during execution, a FATAL error is thrown and the backend session exits. ### `set_user` Usage Typical use of the `set_user` extension is as follows: #### `GRANT EXECUTE` to Functions In order to make use of the `set_user` functions, some database roles must be able to execute the functions. Allow these privileges by `GRANT`ing `EXECUTE` on the appropriate functions to their intended users. ```sql GRANT EXECUTE ON FUNCTION set_user(text) TO dbclient,dbclient2; GRANT EXECUTE ON FUNCTION set_user(text, text) to dbclient,dbclient2; GRANT EXECUTE ON FUNCTION set_user_u(text) TO dbadmin; ``` This example assumes that there are three users of `set_user`: 1) `dbclient` is an unprivileged user that can run as `dbclient2` through calls to `set_user`. 2) `dbclient2` is an unprivileged user that can run as `dbclient` through calls to `set_user`. 3) `dbadmin` is the privileged (non-superuser) role, which is able to escalate privileges to superuser with Enhanced Logging. #### Call `set_user` to Transition Transitioning to other roles through use of `set_user` provides the ability to change the session's `current_user`. Transitions can be made to unprivileged users through use of `set_user` (with optional `token`, as described above). ```sql SELECT set_user('dbclient2'); ``` Alternatively, transitions can be made to superusers through use of `set_user_u`: ```sql SELECT set_user_u('postgres'); ``` **Note:** See rules in [Superuser Allowlist](#set_usersuperuser_allowlist-rules-and-logic) for logic around calling `set_user_u(text)`. See [Nosuperuser Allowlist](#set_usernosuperuser_target_allowlist-rules-and-logic) for reference logic around calling `set_user(text)`. Once one or more unprivileged users are able to run `set_user_u()` in order to escalate their privileges, the superuser account (typically `postgres`) can be altered to `NOLOGIN`, preventing any direct database connection by a superuser which would bypass the enhanced logging. Naturally for this to work as expected, the PostgreSQL cluster must be audited to ensure there are no other PostgreSQL roles existing which are both superuser and can log in. Additionally there must be no unprivileged PostgreSQL roles which have been granted access to one of the existing superuser roles. #### `set_user.superuser_allowlist` Rules and Logic The following rules govern escalation to superuser via the `set_user_u(text)` function: * `current_user` must be `GRANT`ed `EXECUTE ON FUNCTION set_user_u(text)` OR `current_user` must be the `OWNER` of the `set_user_u(text)` function OR `current_user` must be a superuser. * `current_user` must be listed in `set_user.superuser_allowlist` OR `current_user` must belong to a group that is listed in `set_user.superuser_allowlist` (e.g. `'+admin'`) * Role names in `set_user.superuser_allowlist` are case-sensitive. Unquoted names are folded to lower case and quoted names (e.g. `'"Admin"'`) are matched exactly. The quotes of a group entry must enclose the `+` (e.g. `'"+Admin"'`). * If `set_user.superuser_allowlist` is the empty set , `''`, superuser escalation is blocked for all users. * If `set_user.superuser_allowlist` is the wildcard character, `'*'`, all users with `EXECUTE` permission on `set_user_u(text)` can escalate to superuser. * If `set_user.superuser_allowlist` is not specified, the value defaults to the wildcard character, `'*'`. #### `set_user.nosuperuser_target_allowlist` Rules and Logic The following rules govern non-superuser role transitions through use of `set_user(text)` or `set_user(text, text)` function (for simplicity, only `set_user(text)` is used): * `current_user` must be `GRANT`ed `EXECUTE ON FUNCTION set_user(text)` OR `current_user` must be the `OWNER` of the `set_user(text)` function OR `current_user` must be a superuser. * The target rolename must be listed in `set_user.nosuperuser_target_allowlist` OR the target rolename must belong to a group that is listed in `set_user.nosuperuser_target_allowlist` (e.g. `'+client'`) * Role names in `set_user.nosuperuser_target_allowlist` are case-sensitive. Unquoted names are folded to lower case and quoted names (e.g. `'"Client"'`) are matched exactly. The quotes of a group entry must enclose the `+` (e.g. `'"+Client"'`). * If `set_user.nosuperuser_target_allowlist` is the empty set , `''`, `set_user(text)` transitions to non-superusers are blocked for all users. * If `set_user.nosuperuser_target_allowlist` is the wildcard character, `'*'`, all users with `EXECUTE` permission on `set_user(text)` can transition to any other non-superuser role. * If `set_user.nosuperuser_target_allowlist` is not specified, the value defaults to the wildcard character, `'*'`. #### Perform Actions With Enhanced Logging Once a transition has been made, the current session behaves as if it has the privileges of the new `current_user`. The optional enhanced logging creates an audit trail upon transition to an alternate role, ensuring that any privilege escalation/alteration does not go unmonitored. This audit trail is tagged with the value of `set_user.superuser_audit_tag`, such that actions after superuser escalation are easily identifiable. #### Reset to Previous User ```sql SELECT reset_user(); ``` If `set_user()` was initially called with a `token`, the same `token` must be provided in order to reset back to the previous user. ```sql SELECT set_user('dbclient2', 'some_token_string'); SELECT reset_user('some_token_string'); ``` ### Blocking `ALTER SYSTEM` and `COPY PROGRAM` Note that for the blocking of `ALTER SYSTEM` and `COPY PROGRAM` to work properly, you must include `set_user` in `shared_preload_libraries` in `postgresql.conf` and restart PostgreSQL. Notes: If set_user.block_log_statement is set to "off", the `log_statement` setting is left unchanged. For the blocking of `ALTER SYSTEM` and `COPY PROGRAM` to work properly, you must include `set_user` in shared_preload_libraries in postgresql.conf and restart PostgreSQL. Neither `set_user(text)` nor `set_user_u(text)` may be executed from within an explicit transaction block. ### `set_session_auth` Usage Typical use of the `set_session_auth` function is as follows: #### `GRANT EXECUTE` to Functions In order to make use of the `set_session_auth` function, some database roles must be able to execute the function. Allow these privileges by `GRANT`ing `EXECUTE` on the function to their intended users. ```sql GRANT EXECUTE ON FUNCTION set_session_auth(text) TO dbclient,dbclient2; ``` ## Caveats In its current state, this extension cannot prevent `rolename` from performing a variety of nefarious or otherwise undesireable actions. However, these actions will be logged providing an audit trail, which could also be used to trigger alerts. The reset `token` is a best-effort safeguard, not a security boundary. While `set_user()` is active, set_user blocks the statements that it knows can change the current role, such as `SET ROLE`, `SET SESSION AUTHORIZATION`, `DISCARD ALL` and functions with a `SET role` clause. These blocks cover specific statements rather than every possible path. Code running in the session that finds another way to change the role can return to the original role without the `token` and the role transition will not be logged. Do not rely on the `token` to protect the original role from untrusted code. For example, a connection pooler that hands escalated sessions to untrusted clients should connect as a role whose privileges are acceptable for those clients to have. ## Post-Execution Hooks `set_user` exposes two hooks that may be used to control post-execution behavior for `set_user` and `reset_user`. ### Description The following hooks are called (if set) directly before returning from successful calls to `set_user` and `reset_user`. These hooks are meant to give other extensions awareness of `set_user` actions. This is helpful, for instance, to keep track of dynamic user switching within a session. To avoid order-dependency in `shared_preload_libraries`, these hooks are registered in the rendezvous hash table of core Postgres. The header defines a [utility function](set_user.h#L13) for doing all of the necessary setup. ###### `post_set_user` hook Allows another extension to take action after calls to `set_user`. This hook takes the username as an argument so that the hook implementation is aware of the username. ###### `post_reset_user` hook Allows another extension to take action after calls to `reset_user`. This hook does not take any arguments, since the resulting username will always be the `session_user`. ### Configuration Follow the instructions below to implement `set_user` and `reset_user` post-execution hooks in another extension: * Add '-I$(includedir)' to `CPPFLAGS` of the extension which implements the post-execution hooks. * `#include set_user.h` in whichever file implements the hooks. * Register hook implementations in `rendezvous_variable` hash using the `register_set_user_hooks` utility function. Configuration is described in more detail in the [post-execution hooks](#install-set_user-post-execution-hooks) subsection of the Install documentation. ### Caveats If another extension implements the post-execution hooks, `post_set_user_hook` and `post_reset_user_hook`, `set_user` must be listed before that extension in `shared_preload_libraries`. This is due to the way `shared_preload_libraries` are opened and loaded into memory by Postgres: the hooks need to be loaded into memory before their implementations can access them. ## Installation ### Requirements * PostgreSQL 14 or higher. Prior versions of PostgreSQL are supported by prior versions of set_user. ### Compile and Install Clone PostgreSQL repository: ```bash $> git clone https://github.com/postgres/postgres.git ``` Checkout REL_15_STABLE (for example) branch: ```bash $> git checkout REL_15_STABLE ``` Make PostgreSQL: ```bash $> ./configure $> make install -s ``` Change to the contrib directory: ```bash $> cd contrib ``` Clone `set_user` extension: ```bash $> git clone https://github.com/pgaudit/set_user ``` Change to `set_user` directory: ```bash $> cd set_user ``` Build `set_user`: ```bash $> make ``` Install `set_user`: ```bash $> make install ``` #### Using PGXS If an instance of PostgreSQL is already installed, then PGXS can be utilized to build and install `set_user`. Ensure that PostgreSQL binaries are available via the `$PATH` environment variable then use the following commands. ```bash $> make USE_PGXS=1 $> make USE_PGXS=1 install ``` ### Configure The following bash commands should configure your system to utilize `set_user`. Replace all paths as appropriate. It may be prudent to visually inspect the files afterward to ensure the changes took place. ###### Initialize PostgreSQL (if needed): ```bash $> initdb -D /path/to/data/directory ``` ###### Create Target Database (if needed): ```bash $> createdb ``` ###### Install `set_user` functions: Edit postgresql.conf and add `set_user` to the `shared_preload_libraries` line, optionally also changing custom settings as mentioned above. First edit postgresql.conf in your favorite editor: ``` $> vi $PGDATA/postgresql.conf ``` Then add these lines to the end of the file: ``` # Add set_user to any existing list shared_preload_libraries = 'set_user' # The following lines are only required to modify the # blocking of each respective command if desired set_user.block_alter_system = off #defaults to "on" set_user.block_copy_program = off #defaults to "on" set_user.block_log_statement = off #defaults to "on" set_user.superuser_allowlist = '' #defaults to '*' set_user.nosuperuser_target_allowlist = '' #defaults to '*' ``` Finally, restart PostgreSQL (method may vary): ``` $> service postgresql restart ``` Install the extension into your database: ```bash psql CREATE EXTENSION set_user; ``` ###### Install `set_user` post-execution hooks: Ensure that `set_user.h` is copied to `$(includedir)`. This can be done automatically upon normal installation: ```bash $> make USE_PGXS=1 install ``` There is also an explicit make target available to copy the header file to the appropriate directory: ```bash $> make USE_PGXS=1 install-headers ``` Ensure that the implementing extension adds `-I$(includedir)` to `CPPFLAGS` in its Makefile: ``` # Add -I$(includedir) to CPPFLAGS so the set_user header is included override CPPFLAGS += -I$(includedir) ``` Ensure that the implementing extension includes the `set_user` header file in the appropriate C file: ```c /* Include set_user hooks in whichever C file implements the hooks */ #include "set_user.h" ``` Create your `set_user` hooks and register them in the rendezvous_variable hash: ```c void _PG_Init(void) { /* * Your _PG_Init code here */ register_set_user_hooks(extension_post_set_user, extension_post_reset_user); /* * more _PG_Init code */ } /* * extension_post_set_user * * Entrypoint of the set_user post-exec hook. */ static void extension_post_set_user(void) { /* Some magic */ } /* * extension_post_reset_user * * Entrypoint of the reset_user post-exec hook. */ static void extension_post_reset_user(void) { /* Some magic */ } ``` ## GUC Parameters * Block `ALTER SYSTEM` commands * `set_user.block_alter_system = on` * Block `COPY PROGRAM` commands * `set_user.block_copy_program = on` * Block `SET log_statement` commands * `set_user.block_log_statement = on` * Allow list of roles to escalate to superuser * `set_user.superuser_allowlist = ',,...,'` * Allowed list of roles that can be switched to (not used in set_user_u) * `set_user.nosuperuser_target_allowlist = ',,...,'` ## Examples set_user() and related: ``` ################################# # OS command line, terminal 1 ################################# psql -U postgres --------------------------------- -- psql command line, terminal 1 --------------------------------- SELECT rolname FROM pg_authid WHERE rolsuper and rolcanlogin; rolname ---------- postgres (1 row) CREATE EXTENSION set_user; CREATE USER dba_user; GRANT EXECUTE ON FUNCTION set_user(text) TO dba_user; GRANT EXECUTE ON FUNCTION set_user_u(text) TO dba_user; ################################# # OS command line, terminal 2 ################################# psql -U dba_user --------------------------------- -- psql command line, terminal 2 --------------------------------- SELECT set_user('postgres'); ERROR: Switching to superuser only allowed for privileged procedure: 'set_user_u' SELECT set_user_u('postgres'); SELECT CURRENT_USER, SESSION_USER; current_user | session_user --------------+-------------- postgres | dba_user (1 row) SELECT reset_user(); SELECT CURRENT_USER, SESSION_USER; current_user | session_user --------------+-------------- dba_user | dba_user (1 row) \q --------------------------------- -- psql command line, terminal 1 --------------------------------- ALTER USER postgres NOLOGIN; -- repeat terminal 2 test with dba_user before exiting \q ################################# # OS command line, terminal 1 ################################# tail -n 6 LOG: Role dba_user transitioning to Superuser Role postgres STATEMENT: SELECT set_user_u('postgres'); LOG: statement: SELECT CURRENT_USER, SESSION_USER; LOG: statement: SELECT reset_user(); LOG: Superuser Role postgres transitioning to Role dba_user STATEMENT: SELECT reset_user(); ################################# # OS command line, terminal 2 ################################# psql -U dba_user --------------------------------- -- psql command line, terminal 2 --------------------------------- -- Verify there are no superusers that can login directly SELECT rolname FROM pg_authid WHERE rolsuper and rolcanlogin; rolname --------- (0 rows) -- Verify there are no unprivileged roles that can login directly -- that are granted a superuser role even if it is multiple layers -- removed DROP VIEW IF EXISTS roletree; CREATE OR REPLACE VIEW roletree AS WITH RECURSIVE roltree AS ( SELECT u.rolname AS rolname, u.oid AS roloid, u.rolcanlogin, u.rolsuper, '{}'::name[] AS rolparents, NULL::oid AS parent_roloid, NULL::name AS parent_rolname FROM pg_catalog.pg_authid u LEFT JOIN pg_catalog.pg_auth_members m on u.oid = m.member LEFT JOIN pg_catalog.pg_authid g on m.roleid = g.oid WHERE g.oid IS NULL UNION ALL SELECT u.rolname AS rolname, u.oid AS roloid, u.rolcanlogin, u.rolsuper, t.rolparents || g.rolname AS rolparents, g.oid AS parent_roloid, g.rolname AS parent_rolname FROM pg_catalog.pg_authid u JOIN pg_catalog.pg_auth_members m on u.oid = m.member JOIN pg_catalog.pg_authid g on m.roleid = g.oid JOIN roltree t on t.roloid = g.oid ) SELECT r.rolname, r.roloid, r.rolcanlogin, r.rolsuper, r.rolparents FROM roltree r ORDER BY 1; -- For example purposes, given this set of roles SELECT r.rolname, r.rolsuper, r.rolinherit, r.rolcreaterole, r.rolcreatedb, r.rolcanlogin, r.rolconnlimit, r.rolvaliduntil, ARRAY(SELECT b.rolname FROM pg_catalog.pg_auth_members m JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) WHERE m.member = r.oid) as memberof , r.rolreplication , r.rolbypassrls FROM pg_catalog.pg_roles r ORDER BY 1; List of roles Role name | Attributes | Member of -----------+------------------------------------------------------------+------------ bob | | {} dba_user | | {su} joe | | {newbs} newbs | Cannot login | {} postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {} su | No inheritance, Cannot login | {postgres} -- This query shows current status is not acceptable -- 1) postgres can login directly -- 2) dba_user can login and is able to escalate without using set_user() SELECT ro.rolname, ro.roloid, ro.rolcanlogin, ro.rolsuper, ro.rolparents FROM roletree ro WHERE (ro.rolcanlogin AND ro.rolsuper) OR ( ro.rolcanlogin AND EXISTS ( SELECT TRUE FROM roletree ri WHERE ri.rolname = ANY (ro.rolparents) AND ri.rolsuper ) ); rolname | roloid | rolcanlogin | rolsuper | rolparents ----------+--------+-------------+----------+--------------- dba_user | 16387 | t | f | {postgres,su} postgres | 10 | t | t | {} (2 rows) -- Fix it REVOKE postgres FROM su; ALTER USER postgres NOLOGIN; -- Rerun the query - shows current status is acceptable SELECT ro.rolname, ro.roloid, ro.rolcanlogin, ro.rolsuper, ro.rolparents FROM roletree ro WHERE (ro.rolcanlogin AND ro.rolsuper) OR ( ro.rolcanlogin AND EXISTS ( SELECT TRUE FROM roletree ri WHERE ri.rolname = ANY (ro.rolparents) AND ri.rolsuper ) ); rolname | roloid | rolcanlogin | rolsuper | rolparents ---------+--------+-------------+----------+------------ (0 rows) ``` set_session_auth(): ``` # psql -U postgres test psql (15.4) Type "help" for help. test=# grant EXECUTE on FUNCTION set_session_auth(text) to dbclient; \q # psql -U dbclient test psql (15.4) Type "help" for help. test=> select session_user, current_user, user, current_role; session_user | current_user | user | current_role --------------+--------------+----------+-------------- dbclient | dbclient | dbclient | dbclient (1 row) test=> select set_session_auth('jeff'); set_session_auth ------------------ OK (1 row) test=> select session_user, current_user, user, current_role; session_user | current_user | user | current_role --------------+--------------+------+-------------- jeff | jeff | jeff | jeff (1 row) test=> -- the role switch is irrevocable test=> reset role; RESET test=> select session_user, current_user, user, current_role; session_user | current_user | user | current_role --------------+--------------+------+-------------- jeff | jeff | jeff | jeff (1 row) test=> reset session authorization; RESET test=> select session_user, current_user, user, current_role; session_user | current_user | user | current_role --------------+--------------+------+-------------- jeff | jeff | jeff | jeff (1 row) test=> set role none; SET test=> select session_user, current_user, user, current_role; session_user | current_user | user | current_role --------------+--------------+------+-------------- jeff | jeff | jeff | jeff (1 row) ``` ## Licensing Please see the [LICENSE](./LICENSE) file. pgaudit-set_user-45fc481/docbuilder/000077500000000000000000000000001527232611200174535ustar00rootroot00000000000000pgaudit-set_user-45fc481/docbuilder/.gitignore000066400000000000000000000000111527232611200214330ustar00rootroot00000000000000.vagrant pgaudit-set_user-45fc481/docbuilder/README.txt000066400000000000000000000003141527232611200211470ustar00rootroot00000000000000Note: Vagrant Guest Additions is required You may acquire it on your host system via: "sudo vagrant plugin install vagrant-vbguest" To build the docs, do the following: 1) vagrant up 2) vagrant destroy pgaudit-set_user-45fc481/docbuilder/Vagrantfile000066400000000000000000000017341527232611200216450ustar00rootroot00000000000000# Note: Vagrant Guest Additions is required # You may acquire it on your host system via: "sudo vagrant plugin install vagrant-vbguest" Vagrant.configure(2) do |config| config.vm.box = "centos/7" config.vm.provider :virtualbox do |vb| vb.name = "set-user-centos7-test" end # Provision the VM config.vm.provision "shell", inline: <<-SHELL echo "Provisioning..." # Setup environment yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm yum -y install pandoc yum -y install 'texlive-*' # Generate docs cd /set-user version=$(grep "default_version" set_user.control | awk '{print $3}' | sed "s/'//g") pandoc -s README.md -o Set_User-UserGuide-$version.pdf SHELL # Don't share the default vagrant folder config.vm.synced_folder ".", "/vagrant", disabled: true # Mount project path for testing config.vm.synced_folder "..", "/set-user" end pgaudit-set_user-45fc481/expected/000077500000000000000000000000001527232611200171405ustar00rootroot00000000000000pgaudit-set_user-45fc481/expected/set_user.out000066400000000000000000000470141527232611200215300ustar00rootroot00000000000000CREATE EXTENSION set_user; -- Ensure the library is loaded. LOAD 'set_user'; -- Clean up in case a prior regression run failed -- First suppress NOTICE messages when users/groups don't exist SET client_min_messages TO 'warning'; DROP USER IF EXISTS dba, bob, joe, newbs, su, "Joe", "Newbs"; RESET client_min_messages; -- Create some users to work with CREATE USER dba; CREATE USER bob; CREATE USER joe; CREATE ROLE newbs; CREATE ROLE su NOINHERIT; -- dba is the role we want to allow to execute set_user() GRANT EXECUTE ON FUNCTION set_user(text) TO dba; GRANT EXECUTE ON FUNCTION set_user(text,text) TO dba; GRANT EXECUTE ON FUNCTION set_user_u(text) TO dba; GRANT newbs TO bob; -- joe will be able to escalate without set_user() via su GRANT su TO joe; GRANT postgres TO su; -- test reset_user with no initial set SELECT reset_user(); reset_user ------------ OK (1 row) -- test set_user SET SESSION AUTHORIZATION dba; SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | dba (1 row) SELECT set_user('postgres'); ERROR: switching to superuser not allowed HINT: Use 'set_user_u' to escalate. SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | dba (1 row) -- test set_user_u SET SESSION AUTHORIZATION dba; SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | dba (1 row) SELECT set_user_u('postgres'); set_user_u ------------ OK (1 row) SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | postgres (1 row) -- test multiple successive set_user calls SELECT set_user('joe'); -- fail ERROR: must reset previous user prior to setting again -- ALTER SYSTEM should fail ALTER SYSTEM SET wal_level = minimal; ERROR: ALTER SYSTEM blocked by set_user config -- COPY PROGRAM should fail COPY (select 42) TO PROGRAM 'cat'; ERROR: COPY PROGRAM blocked by set_user config -- SET log_statement should fail SET log_statement = 'none'; ERROR: "SET log_statement" blocked by set_user config SET log_statement = DEFAULT; ERROR: "SET log_statement" blocked by set_user config RESET log_statement; ERROR: "SET log_statement" blocked by set_user config BEGIN; SET LOCAL log_statement = 'none'; ABORT; ERROR: "SET log_statement" blocked by set_user config SET "Log_Statement" = 'none'; ERROR: "SET log_statement" blocked by set_user config -- set_config() should fail SELECT set_config('wal_level', 'minimal', false); ERROR: "pg_catalog.set_config(pg_catalog.text,pg_catalog.text,boolean)" blocked by set_user HINT: Use "SET" syntax instead. CREATE OR REPLACE FUNCTION backdoor(text, text, boolean) RETURNS BOOL AS 'set_config_by_name' LANGUAGE INTERNAL; SELECT backdoor('log_statement', 'none', true); ERROR: "public.backdoor(pg_catalog.text,pg_catalog.text,boolean)" blocked by set_user HINT: Use "SET" syntax instead. UPDATE pg_settings SET setting = 'none' WHERE name = 'log_statement'; ERROR: "pg_catalog.set_config(pg_catalog.text,pg_catalog.text,boolean)" blocked by set_user HINT: Use "SET" syntax instead. -- test reset_user RESET ROLE; -- should fail ERROR: "SET/RESET ROLE" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. RESET SESSION AUTHORIZATION; -- should fail ERROR: "SET/RESET SESSION AUTHORIZATION" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. RESET ALL; -- should fail ERROR: "RESET ALL" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | postgres (1 row) SELECT reset_user(); -- succeed reset_user ------------ OK (1 row) -- test set_user and reset_user with token SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | dba (1 row) SELECT set_user('bob', 'secret'); set_user ---------- OK (1 row) SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | bob (1 row) RESET ROLE; -- should fail ERROR: "SET/RESET ROLE" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. RESET SESSION AUTHORIZATION; -- should fail ERROR: "SET/RESET SESSION AUTHORIZATION" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. SET "Role" = 'none'; -- should fail ERROR: "SET/RESET ROLE" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. RESET "ROLE"; -- should fail ERROR: "SET/RESET ROLE" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. SET "Session_Authorization" = 'dba'; -- should fail ERROR: "SET/RESET SESSION AUTHORIZATION" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. DISCARD ALL; -- should fail ERROR: "DISCARD ALL" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. DISCARD PLANS; -- succeed DISCARD SEQUENCES; -- succeed DISCARD TEMP; -- succeed SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | bob (1 row) SELECT reset_user(); -- should fail ERROR: reset token required but not provided SELECT reset_user('secreT'); -- should fail ERROR: incorrect reset token provided SELECT reset_user('secre'); -- should fail ERROR: incorrect reset token provided SELECT reset_user('secrets'); -- should fail ERROR: incorrect reset token provided SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | bob (1 row) SELECT reset_user('secret'); -- succeed reset_user ------------ OK (1 row) SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | dba (1 row) RESET SESSION AUTHORIZATION; ALTER SYSTEM SET wal_level = minimal; COPY (select 42) TO PROGRAM 'cat'; SET log_statement = DEFAULT; -- test transaction handling CREATE FUNCTION bail() RETURNS bool AS $$ BEGIN RAISE EXCEPTION 'bailing out !'; END; $$ LANGUAGE plpgsql; SET SESSION AUTHORIZATION dba; SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | dba (1 row) -- bail during set_user_u SELECT set_user_u('postgres'), bail(); ERROR: bailing out ! CONTEXT: PL/pgSQL function bail() line 3 at RAISE SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | dba (1 row) SHOW log_statement; log_statement --------------- none (1 row) SHOW log_line_prefix; log_line_prefix ----------------- %m [%p] (1 row) -- bail on reset after successful set_user_u SELECT set_user_u('postgres'); set_user_u ------------ OK (1 row) SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | postgres (1 row) SHOW log_statement; log_statement --------------- all (1 row) SHOW log_line_prefix; log_line_prefix ----------------- %m [%p] AUDIT: (1 row) SELECT reset_user(), bail(); ERROR: bailing out ! CONTEXT: PL/pgSQL function bail() line 3 at RAISE SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | postgres (1 row) SHOW log_statement; log_statement --------------- all (1 row) SHOW log_line_prefix; log_line_prefix ----------------- %m [%p] AUDIT: (1 row) SELECT reset_user(); reset_user ------------ OK (1 row) -- bail during set_user SELECT set_user('bob'), bail(); ERROR: bailing out ! CONTEXT: PL/pgSQL function bail() line 3 at RAISE SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | dba (1 row) SHOW log_statement; log_statement --------------- none (1 row) SHOW log_line_prefix; log_line_prefix ----------------- %m [%p] (1 row) -- bail during set_user with token SELECT set_user('bob', 'secret'), bail(); ERROR: bailing out ! CONTEXT: PL/pgSQL function bail() line 3 at RAISE SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | dba (1 row) SHOW log_statement; log_statement --------------- none (1 row) SHOW log_line_prefix; log_line_prefix ----------------- %m [%p] (1 row) -- bail during reset_user with token SELECT set_user('bob', 'secret'); set_user ---------- OK (1 row) SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | bob (1 row) SELECT reset_user('secret'), bail(); ERROR: bailing out ! CONTEXT: PL/pgSQL function bail() line 3 at RAISE SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | bob (1 row) SELECT reset_user('secret'); reset_user ------------ OK (1 row) RESET SESSION AUTHORIZATION; -- this is an example of how we might audit existing roles SET SESSION AUTHORIZATION dba; SELECT set_user_u('postgres'); set_user_u ------------ OK (1 row) SELECT rolname FROM pg_authid WHERE rolsuper and rolcanlogin; rolname ---------- postgres (1 row) CREATE OR REPLACE VIEW roletree AS WITH RECURSIVE roltree AS ( SELECT u.rolname AS rolname, u.oid AS roloid, u.rolcanlogin, u.rolsuper, '{}'::name[] AS rolparents, NULL::oid AS parent_roloid, NULL::name AS parent_rolname FROM pg_catalog.pg_authid u LEFT JOIN pg_catalog.pg_auth_members m on u.oid = m.member LEFT JOIN pg_catalog.pg_authid g on m.roleid = g.oid WHERE g.oid IS NULL UNION ALL SELECT u.rolname AS rolname, u.oid AS roloid, u.rolcanlogin, u.rolsuper, t.rolparents || g.rolname AS rolparents, g.oid AS parent_roloid, g.rolname AS parent_rolname FROM pg_catalog.pg_authid u JOIN pg_catalog.pg_auth_members m on u.oid = m.member JOIN pg_catalog.pg_authid g on m.roleid = g.oid JOIN roltree t on t.roloid = g.oid ) SELECT r.rolname, r.roloid, r.rolcanlogin, r.rolsuper, r.rolparents FROM roltree r ORDER BY 1; -- this will show unacceptable results -- since postgres can log in directly and -- joe can escalate via su to postgres SELECT ro.rolname, ro.rolcanlogin, ro.rolsuper, ro.rolparents FROM roletree ro WHERE (ro.rolcanlogin AND ro.rolsuper) OR ( ro.rolcanlogin AND EXISTS ( SELECT TRUE FROM roletree ri WHERE ri.rolname = ANY (ro.rolparents) AND ri.rolsuper ) ); rolname | rolcanlogin | rolsuper | rolparents ----------+-------------+----------+--------------- joe | t | f | {postgres,su} postgres | t | t | {} (2 rows) -- here is how we fix the environment -- running this in a transaction that will be aborted -- since we don't really want to make the postgres user -- nologin during regression testing BEGIN; REVOKE postgres FROM su; ALTER USER postgres NOLOGIN; -- retest, this time successfully SELECT ro.rolname, ro.rolcanlogin, ro.rolsuper, ro.rolparents FROM roletree ro WHERE (ro.rolcanlogin AND ro.rolsuper) OR ( ro.rolcanlogin AND EXISTS ( SELECT TRUE FROM roletree ri WHERE ri.rolname = ANY (ro.rolparents) AND ri.rolsuper ) ); rolname | rolcanlogin | rolsuper | rolparents ---------+-------------+----------+------------ (0 rows) -- undo those changes ABORT; -- test statements that are only blocked while escalated SELECT reset_user(); reset_user ------------ OK (1 row) RESET SESSION AUTHORIZATION; SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- postgres | postgres (1 row) RESET ALL; -- succeed DISCARD ALL; -- succeed -- test function SET clauses CREATE FUNCTION set_role_none() RETURNS text LANGUAGE sql SET role = 'none' AS $$ SELECT 'ok'::text $$; CREATE FUNCTION set_search_path() RETURNS text LANGUAGE sql SET search_path = 'pg_catalog' AS $$ SELECT 'ok'::text $$; CREATE FUNCTION security_definer() RETURNS text LANGUAGE sql SECURITY DEFINER SET search_path = 'pg_catalog' AS $$ SELECT current_user::text $$; CREATE FUNCTION call_set_role_none() RETURNS text LANGUAGE plpgsql AS $$ BEGIN RETURN set_role_none(); END $$; CREATE TABLE set_clause_call (id int); GRANT INSERT ON set_clause_call TO bob; CREATE FUNCTION trigger_call_set_role_none() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN PERFORM set_role_none(); RETURN NEW; END $$; CREATE TRIGGER set_clause_call_trigger BEFORE INSERT ON set_clause_call FOR EACH ROW EXECUTE FUNCTION trigger_call_set_role_none(); CREATE TABLE set_clause_trigger (id int); GRANT INSERT ON set_clause_trigger TO bob; CREATE FUNCTION trigger_set_role_none() RETURNS trigger LANGUAGE plpgsql SET role = 'none' AS $$ BEGIN RETURN NEW; END $$; CREATE TRIGGER set_clause_trigger_trigger BEFORE INSERT ON set_clause_trigger FOR EACH ROW EXECUTE FUNCTION trigger_set_role_none(); SELECT set_role_none(); -- succeed set_role_none --------------- ok (1 row) SELECT call_set_role_none(); -- succeed call_set_role_none -------------------- ok (1 row) INSERT INTO set_clause_call VALUES (1); -- succeed INSERT INTO set_clause_trigger VALUES (1); -- succeed SET SESSION AUTHORIZATION dba; SELECT set_user('bob', 'secret'); set_user ---------- OK (1 row) CREATE FUNCTION pg_temp.f() RETURNS text LANGUAGE sql SET role = 'none' AS $$ SELECT current_user::text $$; -- should fail ERROR: function SET clause for parameter "role" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. CREATE FUNCTION pg_temp.f() RETURNS text LANGUAGE sql SET "Role" FROM CURRENT AS $$ SELECT current_user::text $$; -- should fail ERROR: function SET clause for parameter "Role" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. CREATE FUNCTION pg_temp.f() RETURNS text LANGUAGE sql AS $$ SELECT current_user::text $$; -- succeed ALTER FUNCTION pg_temp.f() SET session_authorization = 'dba'; -- should fail ERROR: function SET clause for parameter "session_authorization" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. ALTER FUNCTION pg_temp.f() RESET role; -- succeed SELECT set_role_none(); -- should fail ERROR: function "set_role_none" with SET clause for parameter "role" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. SELECT call_set_role_none(); -- should fail, a nested call is checked ERROR: function "set_role_none" with SET clause for parameter "role" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. CONTEXT: PL/pgSQL function call_set_role_none() line 1 at RETURN INSERT INTO set_clause_call VALUES (2); -- should fail, a call from a trigger is checked ERROR: function "set_role_none" with SET clause for parameter "role" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. CONTEXT: SQL statement "SELECT set_role_none()" PL/pgSQL function trigger_call_set_role_none() line 1 at PERFORM SELECT set_search_path(); -- succeed set_search_path ----------------- ok (1 row) SELECT security_definer(); -- succeed, runs as the function owner security_definer ------------------ postgres (1 row) SELECT SESSION_USER, CURRENT_USER; session_user | current_user --------------+-------------- dba | bob (1 row) SELECT reset_user('secret'); -- succeed reset_user ------------ OK (1 row) RESET SESSION AUTHORIZATION; -- A trigger function with a blocked SET clause is caught by the fmgr hook after -- the SET clause has been applied so the session is terminated. Run it in a -- separate session so this one survives. \setenv PGHOST :HOST \setenv PGPORT :PORT \setenv PGUSER :USER \setenv PGDATABASE :DBNAME \! psql -X -q -t -A -c "SET SESSION AUTHORIZATION dba" -c "SELECT set_user('bob', 'secret')" -c "INSERT INTO set_clause_trigger VALUES (2)" OK FATAL: function "trigger_set_role_none" with SET clause for parameter "role" blocked by set_user HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. connection to server was lost SELECT count(*) FROM set_clause_trigger; -- 1, the insert was rolled back count ------- 1 (1 row) DROP TABLE set_clause_call; DROP TABLE set_clause_trigger; DROP FUNCTION pg_temp.f(); DROP FUNCTION trigger_call_set_role_none(); DROP FUNCTION trigger_set_role_none(); DROP FUNCTION call_set_role_none(); DROP FUNCTION set_role_none(); DROP FUNCTION set_search_path(); DROP FUNCTION security_definer(); -- test nosuperuser_target_allowlist \c - ALTER SYSTEM SET set_user.nosuperuser_target_allowlist = 'joe,+newbs'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \c - SHOW set_user.nosuperuser_target_allowlist; set_user.nosuperuser_target_allowlist --------------------------------------- joe,+newbs (1 row) SET SESSION AUTHORIZATION dba; SELECT set_user('joe'); -- succeed, later group entry does not overwrite match set_user ---------- OK (1 row) SELECT reset_user(); reset_user ------------ OK (1 row) SELECT set_user('bob'); -- succeed, bob is a member of newbs set_user ---------- OK (1 row) SELECT reset_user(); reset_user ------------ OK (1 row) SELECT set_user('su'); -- fail, su is not listed ERROR: switching to role is not allowed HINT: Add target role to set_user.nosuperuser_target_allowlist. RESET SESSION AUTHORIZATION; -- test case-sensitive role names in nosuperuser_target_allowlist CREATE USER "Joe"; SET SESSION AUTHORIZATION dba; SELECT set_user('Joe'); -- fail, joe does not match "Joe" ERROR: switching to role is not allowed HINT: Add target role to set_user.nosuperuser_target_allowlist. RESET SESSION AUTHORIZATION; ALTER SYSTEM SET set_user.nosuperuser_target_allowlist = '"Joe"'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \c - SHOW set_user.nosuperuser_target_allowlist; set_user.nosuperuser_target_allowlist --------------------------------------- "Joe" (1 row) SET SESSION AUTHORIZATION dba; SELECT set_user('Joe'); -- succeed set_user ---------- OK (1 row) SELECT reset_user(); reset_user ------------ OK (1 row) SELECT set_user('joe'); -- fail, "Joe" does not match joe ERROR: switching to role is not allowed HINT: Add target role to set_user.nosuperuser_target_allowlist. -- test case-sensitive group names in nosuperuser_target_allowlist \c - CREATE ROLE "Newbs"; GRANT "Newbs" TO joe; ALTER SYSTEM SET set_user.nosuperuser_target_allowlist = '+"Newbs"'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \c - SHOW set_user.nosuperuser_target_allowlist; set_user.nosuperuser_target_allowlist --------------------------------------- +"Newbs" (1 row) SET SESSION AUTHORIZATION dba; SELECT set_user('joe'); -- fail, the quotes must enclose the + ERROR: role ""newbs"" does not exist RESET SESSION AUTHORIZATION; ALTER SYSTEM SET set_user.nosuperuser_target_allowlist = '"+Newbs"'; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \c - SHOW set_user.nosuperuser_target_allowlist; set_user.nosuperuser_target_allowlist --------------------------------------- "+Newbs" (1 row) SET SESSION AUTHORIZATION dba; SELECT set_user('joe'); -- succeed, joe is a member of "Newbs" set_user ---------- OK (1 row) SELECT reset_user(); reset_user ------------ OK (1 row) SELECT set_user('bob'); -- fail, bob is a member of newbs not "Newbs" ERROR: switching to role is not allowed HINT: Add target role to set_user.nosuperuser_target_allowlist. \c - ALTER SYSTEM RESET set_user.nosuperuser_target_allowlist; SELECT pg_reload_conf(); pg_reload_conf ---------------- t (1 row) \c - pgaudit-set_user-45fc481/extension/000077500000000000000000000000001527232611200173535ustar00rootroot00000000000000pgaudit-set_user-45fc481/extension/set_user.sql000066400000000000000000000025241527232611200217300ustar00rootroot00000000000000/* set-user.sql */ SET LOCAL search_path to @extschema@; -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION set_user" to load this file. \quit CREATE FUNCTION @extschema@.set_user(text) RETURNS text AS 'MODULE_PATHNAME', 'set_user' LANGUAGE C; CREATE FUNCTION @extschema@.set_user(text, text) RETURNS text AS 'MODULE_PATHNAME', 'set_user' LANGUAGE C STRICT; REVOKE EXECUTE ON FUNCTION @extschema@.set_user(text) FROM PUBLIC; REVOKE EXECUTE ON FUNCTION @extschema@.set_user(text, text) FROM PUBLIC; CREATE FUNCTION @extschema@.reset_user() RETURNS text AS 'MODULE_PATHNAME', 'set_user' LANGUAGE C; CREATE FUNCTION @extschema@.reset_user(text) RETURNS text AS 'MODULE_PATHNAME', 'set_user' LANGUAGE C STRICT; GRANT EXECUTE ON FUNCTION @extschema@.reset_user() TO PUBLIC; GRANT EXECUTE ON FUNCTION @extschema@.reset_user(text) TO PUBLIC; /* New functions in 1.1 (now 1.4) begin here */ CREATE FUNCTION @extschema@.set_user_u(text) RETURNS text AS 'MODULE_PATHNAME', 'set_user' LANGUAGE C STRICT; REVOKE EXECUTE ON FUNCTION @extschema@.set_user_u(text) FROM PUBLIC; /* New functions in 3.0 begin here */ CREATE FUNCTION @extschema@.set_session_auth(text) RETURNS text AS 'MODULE_PATHNAME', 'set_session_auth' LANGUAGE C STRICT; REVOKE EXECUTE ON FUNCTION @extschema@.set_session_auth(text) FROM PUBLIC; pgaudit-set_user-45fc481/set_user.control000066400000000000000000000002371527232611200205740ustar00rootroot00000000000000# set_user extension comment = 'similar to SET ROLE but with added logging' default_version = '4.3.0' module_pathname = '$libdir/set_user' relocatable = false pgaudit-set_user-45fc481/sql/000077500000000000000000000000001527232611200161365ustar00rootroot00000000000000pgaudit-set_user-45fc481/sql/set_user.sql000066400000000000000000000261421527232611200205150ustar00rootroot00000000000000CREATE EXTENSION set_user; -- Ensure the library is loaded. LOAD 'set_user'; -- Clean up in case a prior regression run failed -- First suppress NOTICE messages when users/groups don't exist SET client_min_messages TO 'warning'; DROP USER IF EXISTS dba, bob, joe, newbs, su, "Joe", "Newbs"; RESET client_min_messages; -- Create some users to work with CREATE USER dba; CREATE USER bob; CREATE USER joe; CREATE ROLE newbs; CREATE ROLE su NOINHERIT; -- dba is the role we want to allow to execute set_user() GRANT EXECUTE ON FUNCTION set_user(text) TO dba; GRANT EXECUTE ON FUNCTION set_user(text,text) TO dba; GRANT EXECUTE ON FUNCTION set_user_u(text) TO dba; GRANT newbs TO bob; -- joe will be able to escalate without set_user() via su GRANT su TO joe; GRANT postgres TO su; -- test reset_user with no initial set SELECT reset_user(); -- test set_user SET SESSION AUTHORIZATION dba; SELECT SESSION_USER, CURRENT_USER; SELECT set_user('postgres'); SELECT SESSION_USER, CURRENT_USER; -- test set_user_u SET SESSION AUTHORIZATION dba; SELECT SESSION_USER, CURRENT_USER; SELECT set_user_u('postgres'); SELECT SESSION_USER, CURRENT_USER; -- test multiple successive set_user calls SELECT set_user('joe'); -- fail -- ALTER SYSTEM should fail ALTER SYSTEM SET wal_level = minimal; -- COPY PROGRAM should fail COPY (select 42) TO PROGRAM 'cat'; -- SET log_statement should fail SET log_statement = 'none'; SET log_statement = DEFAULT; RESET log_statement; BEGIN; SET LOCAL log_statement = 'none'; ABORT; SET "Log_Statement" = 'none'; -- set_config() should fail SELECT set_config('wal_level', 'minimal', false); CREATE OR REPLACE FUNCTION backdoor(text, text, boolean) RETURNS BOOL AS 'set_config_by_name' LANGUAGE INTERNAL; SELECT backdoor('log_statement', 'none', true); UPDATE pg_settings SET setting = 'none' WHERE name = 'log_statement'; -- test reset_user RESET ROLE; -- should fail RESET SESSION AUTHORIZATION; -- should fail RESET ALL; -- should fail SELECT SESSION_USER, CURRENT_USER; SELECT reset_user(); -- succeed -- test set_user and reset_user with token SELECT SESSION_USER, CURRENT_USER; SELECT set_user('bob', 'secret'); SELECT SESSION_USER, CURRENT_USER; RESET ROLE; -- should fail RESET SESSION AUTHORIZATION; -- should fail SET "Role" = 'none'; -- should fail RESET "ROLE"; -- should fail SET "Session_Authorization" = 'dba'; -- should fail DISCARD ALL; -- should fail DISCARD PLANS; -- succeed DISCARD SEQUENCES; -- succeed DISCARD TEMP; -- succeed SELECT SESSION_USER, CURRENT_USER; SELECT reset_user(); -- should fail SELECT reset_user('secreT'); -- should fail SELECT reset_user('secre'); -- should fail SELECT reset_user('secrets'); -- should fail SELECT SESSION_USER, CURRENT_USER; SELECT reset_user('secret'); -- succeed SELECT SESSION_USER, CURRENT_USER; RESET SESSION AUTHORIZATION; ALTER SYSTEM SET wal_level = minimal; COPY (select 42) TO PROGRAM 'cat'; SET log_statement = DEFAULT; -- test transaction handling CREATE FUNCTION bail() RETURNS bool AS $$ BEGIN RAISE EXCEPTION 'bailing out !'; END; $$ LANGUAGE plpgsql; SET SESSION AUTHORIZATION dba; SELECT SESSION_USER, CURRENT_USER; -- bail during set_user_u SELECT set_user_u('postgres'), bail(); SELECT SESSION_USER, CURRENT_USER; SHOW log_statement; SHOW log_line_prefix; -- bail on reset after successful set_user_u SELECT set_user_u('postgres'); SELECT SESSION_USER, CURRENT_USER; SHOW log_statement; SHOW log_line_prefix; SELECT reset_user(), bail(); SELECT SESSION_USER, CURRENT_USER; SHOW log_statement; SHOW log_line_prefix; SELECT reset_user(); -- bail during set_user SELECT set_user('bob'), bail(); SELECT SESSION_USER, CURRENT_USER; SHOW log_statement; SHOW log_line_prefix; -- bail during set_user with token SELECT set_user('bob', 'secret'), bail(); SELECT SESSION_USER, CURRENT_USER; SHOW log_statement; SHOW log_line_prefix; -- bail during reset_user with token SELECT set_user('bob', 'secret'); SELECT SESSION_USER, CURRENT_USER; SELECT reset_user('secret'), bail(); SELECT SESSION_USER, CURRENT_USER; SELECT reset_user('secret'); RESET SESSION AUTHORIZATION; -- this is an example of how we might audit existing roles SET SESSION AUTHORIZATION dba; SELECT set_user_u('postgres'); SELECT rolname FROM pg_authid WHERE rolsuper and rolcanlogin; CREATE OR REPLACE VIEW roletree AS WITH RECURSIVE roltree AS ( SELECT u.rolname AS rolname, u.oid AS roloid, u.rolcanlogin, u.rolsuper, '{}'::name[] AS rolparents, NULL::oid AS parent_roloid, NULL::name AS parent_rolname FROM pg_catalog.pg_authid u LEFT JOIN pg_catalog.pg_auth_members m on u.oid = m.member LEFT JOIN pg_catalog.pg_authid g on m.roleid = g.oid WHERE g.oid IS NULL UNION ALL SELECT u.rolname AS rolname, u.oid AS roloid, u.rolcanlogin, u.rolsuper, t.rolparents || g.rolname AS rolparents, g.oid AS parent_roloid, g.rolname AS parent_rolname FROM pg_catalog.pg_authid u JOIN pg_catalog.pg_auth_members m on u.oid = m.member JOIN pg_catalog.pg_authid g on m.roleid = g.oid JOIN roltree t on t.roloid = g.oid ) SELECT r.rolname, r.roloid, r.rolcanlogin, r.rolsuper, r.rolparents FROM roltree r ORDER BY 1; -- this will show unacceptable results -- since postgres can log in directly and -- joe can escalate via su to postgres SELECT ro.rolname, ro.rolcanlogin, ro.rolsuper, ro.rolparents FROM roletree ro WHERE (ro.rolcanlogin AND ro.rolsuper) OR ( ro.rolcanlogin AND EXISTS ( SELECT TRUE FROM roletree ri WHERE ri.rolname = ANY (ro.rolparents) AND ri.rolsuper ) ); -- here is how we fix the environment -- running this in a transaction that will be aborted -- since we don't really want to make the postgres user -- nologin during regression testing BEGIN; REVOKE postgres FROM su; ALTER USER postgres NOLOGIN; -- retest, this time successfully SELECT ro.rolname, ro.rolcanlogin, ro.rolsuper, ro.rolparents FROM roletree ro WHERE (ro.rolcanlogin AND ro.rolsuper) OR ( ro.rolcanlogin AND EXISTS ( SELECT TRUE FROM roletree ri WHERE ri.rolname = ANY (ro.rolparents) AND ri.rolsuper ) ); -- undo those changes ABORT; -- test statements that are only blocked while escalated SELECT reset_user(); RESET SESSION AUTHORIZATION; SELECT SESSION_USER, CURRENT_USER; RESET ALL; -- succeed DISCARD ALL; -- succeed -- test function SET clauses CREATE FUNCTION set_role_none() RETURNS text LANGUAGE sql SET role = 'none' AS $$ SELECT 'ok'::text $$; CREATE FUNCTION set_search_path() RETURNS text LANGUAGE sql SET search_path = 'pg_catalog' AS $$ SELECT 'ok'::text $$; CREATE FUNCTION security_definer() RETURNS text LANGUAGE sql SECURITY DEFINER SET search_path = 'pg_catalog' AS $$ SELECT current_user::text $$; CREATE FUNCTION call_set_role_none() RETURNS text LANGUAGE plpgsql AS $$ BEGIN RETURN set_role_none(); END $$; CREATE TABLE set_clause_call (id int); GRANT INSERT ON set_clause_call TO bob; CREATE FUNCTION trigger_call_set_role_none() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN PERFORM set_role_none(); RETURN NEW; END $$; CREATE TRIGGER set_clause_call_trigger BEFORE INSERT ON set_clause_call FOR EACH ROW EXECUTE FUNCTION trigger_call_set_role_none(); CREATE TABLE set_clause_trigger (id int); GRANT INSERT ON set_clause_trigger TO bob; CREATE FUNCTION trigger_set_role_none() RETURNS trigger LANGUAGE plpgsql SET role = 'none' AS $$ BEGIN RETURN NEW; END $$; CREATE TRIGGER set_clause_trigger_trigger BEFORE INSERT ON set_clause_trigger FOR EACH ROW EXECUTE FUNCTION trigger_set_role_none(); SELECT set_role_none(); -- succeed SELECT call_set_role_none(); -- succeed INSERT INTO set_clause_call VALUES (1); -- succeed INSERT INTO set_clause_trigger VALUES (1); -- succeed SET SESSION AUTHORIZATION dba; SELECT set_user('bob', 'secret'); CREATE FUNCTION pg_temp.f() RETURNS text LANGUAGE sql SET role = 'none' AS $$ SELECT current_user::text $$; -- should fail CREATE FUNCTION pg_temp.f() RETURNS text LANGUAGE sql SET "Role" FROM CURRENT AS $$ SELECT current_user::text $$; -- should fail CREATE FUNCTION pg_temp.f() RETURNS text LANGUAGE sql AS $$ SELECT current_user::text $$; -- succeed ALTER FUNCTION pg_temp.f() SET session_authorization = 'dba'; -- should fail ALTER FUNCTION pg_temp.f() RESET role; -- succeed SELECT set_role_none(); -- should fail SELECT call_set_role_none(); -- should fail, a nested call is checked INSERT INTO set_clause_call VALUES (2); -- should fail, a call from a trigger is checked SELECT set_search_path(); -- succeed SELECT security_definer(); -- succeed, runs as the function owner SELECT SESSION_USER, CURRENT_USER; SELECT reset_user('secret'); -- succeed RESET SESSION AUTHORIZATION; -- A trigger function with a blocked SET clause is caught by the fmgr hook after -- the SET clause has been applied so the session is terminated. Run it in a -- separate session so this one survives. \setenv PGHOST :HOST \setenv PGPORT :PORT \setenv PGUSER :USER \setenv PGDATABASE :DBNAME \! psql -X -q -t -A -c "SET SESSION AUTHORIZATION dba" -c "SELECT set_user('bob', 'secret')" -c "INSERT INTO set_clause_trigger VALUES (2)" SELECT count(*) FROM set_clause_trigger; -- 1, the insert was rolled back DROP TABLE set_clause_call; DROP TABLE set_clause_trigger; DROP FUNCTION pg_temp.f(); DROP FUNCTION trigger_call_set_role_none(); DROP FUNCTION trigger_set_role_none(); DROP FUNCTION call_set_role_none(); DROP FUNCTION set_role_none(); DROP FUNCTION set_search_path(); DROP FUNCTION security_definer(); -- test nosuperuser_target_allowlist \c - ALTER SYSTEM SET set_user.nosuperuser_target_allowlist = 'joe,+newbs'; SELECT pg_reload_conf(); \c - SHOW set_user.nosuperuser_target_allowlist; SET SESSION AUTHORIZATION dba; SELECT set_user('joe'); -- succeed, later group entry does not overwrite match SELECT reset_user(); SELECT set_user('bob'); -- succeed, bob is a member of newbs SELECT reset_user(); SELECT set_user('su'); -- fail, su is not listed RESET SESSION AUTHORIZATION; -- test case-sensitive role names in nosuperuser_target_allowlist CREATE USER "Joe"; SET SESSION AUTHORIZATION dba; SELECT set_user('Joe'); -- fail, joe does not match "Joe" RESET SESSION AUTHORIZATION; ALTER SYSTEM SET set_user.nosuperuser_target_allowlist = '"Joe"'; SELECT pg_reload_conf(); \c - SHOW set_user.nosuperuser_target_allowlist; SET SESSION AUTHORIZATION dba; SELECT set_user('Joe'); -- succeed SELECT reset_user(); SELECT set_user('joe'); -- fail, "Joe" does not match joe -- test case-sensitive group names in nosuperuser_target_allowlist \c - CREATE ROLE "Newbs"; GRANT "Newbs" TO joe; ALTER SYSTEM SET set_user.nosuperuser_target_allowlist = '+"Newbs"'; SELECT pg_reload_conf(); \c - SHOW set_user.nosuperuser_target_allowlist; SET SESSION AUTHORIZATION dba; SELECT set_user('joe'); -- fail, the quotes must enclose the + RESET SESSION AUTHORIZATION; ALTER SYSTEM SET set_user.nosuperuser_target_allowlist = '"+Newbs"'; SELECT pg_reload_conf(); \c - SHOW set_user.nosuperuser_target_allowlist; SET SESSION AUTHORIZATION dba; SELECT set_user('joe'); -- succeed, joe is a member of "Newbs" SELECT reset_user(); SELECT set_user('bob'); -- fail, bob is a member of newbs not "Newbs" \c - ALTER SYSTEM RESET set_user.nosuperuser_target_allowlist; SELECT pg_reload_conf(); \c - pgaudit-set_user-45fc481/src/000077500000000000000000000000001527232611200161265ustar00rootroot00000000000000pgaudit-set_user-45fc481/src/compatibility.h000066400000000000000000000054061527232611200211550ustar00rootroot00000000000000/* ------------------------------------------------------------------------- * * compatibility.h * * Definitions for maintaining compatibility across Postgres versions. * * Copyright (c) 2010-2022, PostgreSQL Global Development Group * * ------------------------------------------------------------------------- */ #ifndef SET_USER_COMPAT_H #define SET_USER_COMPAT_H #ifndef NO_ASSERT_AUTH_UID_ONCE #define NO_ASSERT_AUTH_UID_ONCE !USE_ASSERT_CHECKING #endif /* * Define PG_MODULE_MAGIC_EXT() as PG_MODULE_MAGIC for versions where the new * macro does not exist */ #if PG_VERSION_NUM < 180000 #define PG_MODULE_MAGIC_EXT(...) PG_MODULE_MAGIC #endif /* PG_VERSION_NUM < 180000 */ /* * Define timingsafe_bcmp() for versions where it does not exist. A distinct * name is used to avoid conflicting with a libc version of the function. * * Copied from PostgreSQL src/port/timingsafe_bcmp.c, which is based on * $OpenBSD: timingsafe_bcmp.c,v 1.3 2015/08/31 02:53:57 guenther Exp $ * * Copyright (c) 2010 Damien Miller. All rights reserved. * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #if PG_VERSION_NUM < 180000 static inline int set_user_timingsafe_bcmp(const void *b1, const void *b2, size_t n) { const unsigned char *p1 = b1; const unsigned char *p2 = b2; int ret = 0; for (; n > 0; n--) ret |= *p1++ ^ *p2++; return (ret != 0); } #define timingsafe_bcmp(b1, b2, n) set_user_timingsafe_bcmp(b1, b2, n) #endif /* PG_VERSION_NUM < 180000 */ /* * PostgreSQL version 17+ * * - Sets bypass_login_check parameter to false in InitializeSessionUserId funcion */ #if PG_VERSION_NUM >= 170000 #ifndef INITSESSIONUSER #define INITSESSIONUSER #define _InitializeSessionUserId(name,ouserid) InitializeSessionUserId(name,ouserid,false) #endif #endif /* 17+ */ /* * Remaining supported versions */ #ifndef INITSESSIONUSER #define INITSESSIONUSER #define _InitializeSessionUserId(name,ouserid) InitializeSessionUserId(name,ouserid) #endif #if !defined(PG_VERSION_NUM) || PG_VERSION_NUM < 140000 #error "This extension only builds with PostgreSQL 14 or later" #endif #endif /* SET_USER_COMPAT_H */ pgaudit-set_user-45fc481/src/set_user.c000066400000000000000000001005051527232611200201240ustar00rootroot00000000000000/* * set_user.c * * Joe Conway * * This code is released under the PostgreSQL license. * * Copyright 2015-2025 Crunchy Data Solutions, Inc. * * 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 CRUNCHY DATA SOLUTIONS, INC. 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 CRUNCHY DATA SOLUTIONS, INC. HAS BEEN ADVISED * OF THE POSSIBILITY OF SUCH DAMAGE. * * THE CRUNCHY DATA SOLUTIONS, INC. 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 THE CRUNCHY DATA SOLUTIONS, INC. HAS NO * OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR * MODIFICATIONS. */ #include "postgres.h" #include "pg_config.h" #include "access/genam.h" #include "access/htup_details.h" #include "access/table.h" #include "access/xact.h" #include "catalog/indexing.h" #include "catalog/objectaccess.h" #include "catalog/objectaddress.h" #include "catalog/pg_authid.h" #include "catalog/pg_proc.h" #include "catalog/pg_type.h" #include "miscadmin.h" #include "parser/parse_func.h" #include "tcop/utility.h" #include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/catcache.h" #include "utils/fmgroids.h" #include "utils/guc.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/snapmgr.h" #include "utils/syscache.h" #include "utils/rel.h" #include "utils/varlena.h" #include "set_user.h" #include "compatibility.h" PG_MODULE_MAGIC_EXT(.name = "set_user", .version = EXTVERSION); #define ALLOWLIST_WILDCARD "*" #define SUPERUSER_AUDIT_TAG "AUDIT" static ProcessUtility_hook_type prev_hook = NULL; static object_access_hook_type next_object_access_hook; static fmgr_hook_type next_fmgr_hook = NULL; /* transaction handler */ static void set_user_xact_handler (XactEvent event, void *arg); /* set_user transaction state */ typedef struct { Oid userid; bool is_superuser; char *username; char *log_statement; const char *log_prefix; char *reset_token; } SetUserXactState; static SetUserXactState *curr_state; static SetUserXactState *pending_state; static SetUserXactState *prev_state; static void set_user_free_state(SetUserXactState **state); static bool is_reset = false; static const char *su = "Superuser "; static const char *nsu = ""; static bool Block_AS = false; static bool Block_CP = false; static bool Block_LS = false; static char *SU_Allowlist = NULL; static char *NOSU_TargetAllowlist = NULL; static char *SU_AuditTag = NULL; static bool exit_on_error = true; static const char *set_config_proc_name = "set_config_by_name"; static List *set_config_oid_cache = NIL; static void PostSetUserHook(bool is_reset, const char *newuser); static void PU_hook(PlannedStmt *pstmt, const char *queryString, bool ReadOnlyTree, ProcessUtilityContext context, ParamListInfo params, QueryEnvironment *queryEnv, DestReceiver *dest, QueryCompletion *qc); extern Datum set_user(PG_FUNCTION_ARGS); void _PG_init(void); void _PG_fini(void); /* used to block set_config() */ static void set_user_object_access(ObjectAccessType access, Oid classId, Oid objectId, int subId, void *arg); static void set_user_block_set_config(Oid functionId); static void set_user_check_proc(HeapTuple procTup, Relation rel); static void set_user_cache_proc(Oid functionId); /* used to block function SET clauses */ static bool set_user_blocked_guc(const char *name); static void set_user_check_set_clauses(List *options); static void set_user_check_proconfig(Oid functionId, int elevel); static void set_user_fmgr_hook(FmgrHookEventType event, FmgrInfo *flinfo, Datum *arg); /* * set_user_token_equal * * Compare reset tokens in constant time so the comparison does not leak the * stored token through timing. The token length is not considered secret. */ static bool set_user_token_equal(const char *token1, const char *token2) { size_t len = strlen(token1); if (strlen(token2) != len) return false; return timingsafe_bcmp(token1, token2, len) == 0; } /* * check_user_allowlist * * Check if user is contained by allowlist * */ static bool check_user_allowlist(Oid userId, const char *allowlist) { char *rawstring = NULL; List *elemlist; ListCell *l; bool result = false; if (allowlist == NULL || allowlist[0] == '\0') return false; rawstring = pstrdup(allowlist); /* Parse string into list of identifiers */ if (!SplitIdentifierString(rawstring, ',', &elemlist)) { /* syntax error in list */ ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("invalid syntax in parameter"))); } /* Allow all users to escalate if allowlist is a solo wildcard character. */ if (list_length(elemlist) == 1) { char *first_elem = NULL; first_elem = (char *) linitial(elemlist); if (pg_strcasecmp(first_elem, ALLOWLIST_WILDCARD) == 0) return true; } /* * Check whole allowlist to see if it contains the current username and no * wildcard character. Throw an error if the allowlist contains both. */ foreach(l, elemlist) { char *elem = (char *) lfirst(l); if (elem[0] == '+') { Oid roleId; roleId = get_role_oid(elem + 1, false); /* * Check to see if userId is contained by group role in allowlist. * Do not overwrite a match from a prior entry. */ if (has_privs_of_role(userId, roleId)) result = true; } else { /* * Role names are case-sensitive so use an exact comparison. * SplitIdentifierString() has already downcased unquoted names. */ if (strcmp(elem, GetUserNameFromId(userId, false)) == 0) result = true; else if(pg_strcasecmp(elem, ALLOWLIST_WILDCARD) == 0) /* No explicit usernames intermingled with wildcard. */ ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("invalid syntax in parameter"), errhint("Either remove users from set_user.superuser_allowlist " "or remove the wildcard character \"%s\". The allowlist " "cannot contain both.", ALLOWLIST_WILDCARD))); } } return result; } /* * Return the oid of the tuple based on the provided catalogID */ static Oid heap_tuple_get_oid(HeapTuple tuple, Oid catalogID) { switch (catalogID) { case ProcedureRelationId: return ((Form_pg_proc) GETSTRUCT(tuple))->oid; break; case AuthIdRelationId: return ((Form_pg_authid) GETSTRUCT(tuple))->oid; break; default: ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("set_user: invalid relation ID provided"))); return 0; } } /* * Similar to SET ROLE but with added logging and some additional * control over allowed actions * */ PG_FUNCTION_INFO_V1(set_user); Datum set_user(PG_FUNCTION_ARGS) { bool argisnull = PG_ARGISNULL(0); int nargs = PG_NARGS(); HeapTuple roleTup; MemoryContext oldcontext = NULL; bool is_token = false; bool is_privileged = false; /* * Disallow `set_user()` inside a transaction block. The * semantics are too strange, and I cannot think of a * good use case where it would make sense anyway. * Perhaps one day we will need to rethink this... */ if (IsTransactionBlock()) { ereport(ERROR, (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), errmsg("set_user: \"set_user()\" not allowed within transaction block"), errhint("Use \"set_user()\" outside transaction block instead."))); } /* * set_user(non_null_arg text) * * Might be set_user(username) but might also be set_user(reset_token). * The former case we need to switch user normally, the latter is a * reset with token provided. We need to determine which one we have. */ if (nargs == 1 && !argisnull) { Oid funcOid = fcinfo->flinfo->fn_oid; HeapTuple procTup; Form_pg_proc procStruct; char *funcname; /* Lookup the pg_proc tuple by Oid */ procTup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcOid)); if (!HeapTupleIsValid(procTup)) elog(ERROR, "cache lookup failed for function %u", funcOid); procStruct = (Form_pg_proc) GETSTRUCT(procTup); if (!procStruct) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("set_user: function lookup failed for %u", funcOid))); } funcname = pstrdup(NameStr(procStruct->proname)); ReleaseSysCache(procTup); if (strcmp(funcname, "reset_user") == 0) { is_reset = true; is_token = true; } if (strcmp(funcname, "set_user_u") == 0) is_privileged = true; } /* * set_user() or set_user(NULL) ==> always a reset */ else if (nargs == 0 || (nargs == 1 && argisnull)) is_reset = true; /* Switch to a persistent memory context to store state */ oldcontext = MemoryContextSwitchTo(TopMemoryContext); /* Need to pfree in the case of a reset with no initial set */ pending_state = palloc0(sizeof(SetUserXactState)); if ((nargs == 1 && !is_reset) || nargs == 2) { /* we are setting a new user */ if (prev_state != NULL && prev_state->userid != InvalidOid) { ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("must reset previous user prior to setting again"))); } pending_state->username = text_to_cstring(PG_GETARG_TEXT_PP(0)); /* with 2 args, the caller wants to specify a reset token */ if (nargs == 2) { /* this should never be NULL but just in case */ if (PG_ARGISNULL(1)) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("set_user: NULL reset_token not valid"))); } /*capture the reset token */ pending_state->reset_token = text_to_cstring(PG_GETARG_TEXT_PP(1)); } /* Look up the username */ roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(pending_state->username)); if (!HeapTupleIsValid(roleTup)) elog(ERROR, "role \"%s\" does not exist", pending_state->username); pending_state->userid = heap_tuple_get_oid(roleTup, AuthIdRelationId); pending_state->is_superuser = ((Form_pg_authid) GETSTRUCT(roleTup))->rolsuper; ReleaseSysCache(roleTup); if (pending_state->is_superuser) { if (!is_privileged) /* can only escalate with set_user_u */ ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("switching to superuser not allowed"), errhint("Use \'set_user_u\' to escalate."))); else if (!check_user_allowlist(GetUserId(), SU_Allowlist)) /* check superuser allowlist*/ ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("switching to superuser not allowed"), errhint("Add current user to set_user.superuser_allowlist."))); } else if(!check_user_allowlist(pending_state->userid, NOSU_TargetAllowlist)) { ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("switching to role is not allowed"), errhint("Add target role to set_user.nosuperuser_target_allowlist."))); } /* Keep track of current state */ if (curr_state == NULL) { curr_state = palloc0(sizeof(SetUserXactState)); curr_state->log_statement = pstrdup(GetConfigOption("log_statement", false, false)); curr_state->log_prefix = pstrdup(GetConfigOption("log_line_prefix", true, false)); curr_state->reset_token = pending_state->reset_token; curr_state->userid = GetUserId(); curr_state->username = GetUserNameFromId(curr_state->userid, false); curr_state->is_superuser = superuser_arg(curr_state->userid); } if (pending_state->is_superuser && Block_LS) { pending_state->log_prefix = NULL; /* * Add a custom AUDIT tag to postgresql.conf setting * 'log_line_prefix' so log statements are tagged for easy * filtering. */ if (curr_state->log_prefix) pending_state->log_prefix = psprintf("%s%s: ", curr_state->log_prefix, SU_AuditTag); else pending_state->log_prefix = pstrdup(SU_AuditTag); /* * Force logging of everything if block_log_statement is true * and we are escalating to superuser. If not escalating to superuser the * caller could always set log_statement to all prior to using set_user, * and ensure Block_LS is true. */ pending_state->log_statement = pstrdup("all"); } } else if (is_reset) { /* * set_user not active. No need to change pending state here. * The xact handler has no state to process. Just reset the * `is_reset` flag and return success. */ if (prev_state == NULL || prev_state->userid == InvalidOid) { is_reset = false; set_user_free_state(&pending_state); PG_RETURN_TEXT_P(cstring_to_text("OK")); } /* Enforce token comparison if the reset_token is set */ if (prev_state->reset_token) { if (!is_token) { ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("reset token required but not provided"))); } pending_state->reset_token = text_to_cstring(PG_GETARG_TEXT_PP(0)); if (!set_user_token_equal(prev_state->reset_token, pending_state->reset_token)) { ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("incorrect reset token provided"))); } } /* store old state as pending */ pending_state->userid = prev_state->userid; pending_state->username = GetUserNameFromId(prev_state->userid, false); pending_state->log_statement = prev_state->log_statement; pending_state->log_prefix = prev_state->log_prefix; pending_state->is_superuser = superuser_arg(prev_state->userid); } else /* should not happen */ elog(ERROR, "unexpected argument combination"); MemoryContextSwitchTo(oldcontext); PG_RETURN_TEXT_P(cstring_to_text("OK")); } /* * set_user_free_state * * Convenience function for cleaning up transaction state struct. */ static void set_user_free_state(SetUserXactState **state) { if (*state != NULL) { (*state)->userid = InvalidOid; pfree(*state); *state = NULL; } } /* * set_user_xact_handler * * Keeps track of variables managed by set_user and ensures proper state during * transaction ABORT. */ static void set_user_xact_handler (XactEvent event, void *arg) { MemoryContext oldcontext = NULL; switch (event) { case XACT_EVENT_PRE_COMMIT: if (pending_state == NULL || curr_state == NULL) return; oldcontext = MemoryContextSwitchTo(TopMemoryContext); elog(LOG, "%sRole %s transitioning to %sRole %s", curr_state->is_superuser ? su : nsu, curr_state->username, pending_state->is_superuser ? su : nsu, pending_state->username); /* Do the actual work */ SetCurrentRoleId(pending_state->userid, pending_state->is_superuser); PostSetUserHook(is_reset, pending_state->username); /* Update GUCs */ SetConfigOption("log_statement", pending_state->log_statement, PGC_SUSET, PGC_S_SESSION); SetConfigOption("log_line_prefix", pending_state->log_prefix, PGC_POSTMASTER, PGC_S_SESSION); /* start fresh */ if (is_reset) { set_user_free_state(&pending_state); set_user_free_state(&curr_state); set_user_free_state(&prev_state); /* always clear is_reset after we've processed it */ is_reset = false; } else { prev_state = palloc0(sizeof(SetUserXactState)); memcpy(prev_state, curr_state, sizeof(SetUserXactState)); set_user_free_state(&curr_state); curr_state = palloc0(sizeof(SetUserXactState)); memcpy(curr_state, pending_state, sizeof(SetUserXactState)); set_user_free_state(&pending_state); } MemoryContextSwitchTo(oldcontext); break; case XACT_EVENT_ABORT: set_user_free_state(&pending_state); is_reset = false; break; default: break; } } void _PG_init(void) { DefineCustomBoolVariable("set_user.block_alter_system", "Block ALTER SYSTEM commands", NULL, &Block_AS, true, PGC_SIGHUP, 0, NULL, NULL, NULL); DefineCustomBoolVariable("set_user.block_copy_program", "Blocks COPY PROGRAM commands", NULL, &Block_CP, true, PGC_SIGHUP, 0, NULL, NULL, NULL); DefineCustomBoolVariable("set_user.block_log_statement", "Blocks \"SET log_statement\" commands", NULL, &Block_LS, true, PGC_SIGHUP, 0, NULL, NULL, NULL); DefineCustomStringVariable("set_user.nosuperuser_target_allowlist", "List of roles that can be an argument to set_user", NULL, &NOSU_TargetAllowlist, ALLOWLIST_WILDCARD, PGC_SIGHUP, 0, NULL, NULL, NULL); DefineCustomStringVariable("set_user.superuser_allowlist", "Allows a list of users to use set_user_u for superuser escalation", NULL, &SU_Allowlist, ALLOWLIST_WILDCARD, PGC_SIGHUP, 0, NULL, NULL, NULL); DefineCustomStringVariable("set_user.superuser_audit_tag", "Set custom tag for superuser audit escalation", NULL, &SU_AuditTag, SUPERUSER_AUDIT_TAG, PGC_SIGHUP, 0, NULL, NULL, NULL); DefineCustomBoolVariable("set_user.exit_on_error", "Exit backend process on ERROR during set_session_auth()", NULL, &exit_on_error, true, PGC_SIGHUP, 0, NULL, NULL, NULL); /* Install hook */ prev_hook = ProcessUtility_hook; ProcessUtility_hook = PU_hook; /* Object access hook */ next_object_access_hook = object_access_hook; object_access_hook = set_user_object_access; /* Function manager hook */ next_fmgr_hook = fmgr_hook; fmgr_hook = set_user_fmgr_hook; RegisterXactCallback(set_user_xact_handler, NULL); } void _PG_fini(void) { ProcessUtility_hook = prev_hook; fmgr_hook = next_fmgr_hook; } /* * PU_hook * * ProcessUtility hook that blocks restricted utility statements while a * set_user() transition is active. */ static void PU_hook(PlannedStmt *pstmt, const char *queryString, bool ReadOnlyTree, ProcessUtilityContext context, ParamListInfo params, QueryEnvironment *queryEnv, DestReceiver *dest, QueryCompletion *qc) { /* if set_user has been used to transition, enforce set_user GUCs */ if (curr_state != NULL && curr_state->userid != InvalidOid) { switch (nodeTag((Node *) pstmt->utilityStmt)) { case T_AlterSystemStmt: if (Block_AS) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("ALTER SYSTEM blocked by set_user config"))); break; case T_CopyStmt: if (((CopyStmt *)pstmt->utilityStmt)->is_program && Block_CP) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("COPY PROGRAM blocked by set_user config"))); break; case T_VariableSetStmt: { VariableSetStmt *vstmt = (VariableSetStmt *) pstmt->utilityStmt; /* * RESET ALL would reset log_statement so block it. RESET ALL * has a NULL name so check the kind before comparing the name. */ if (vstmt->kind == VAR_RESET_ALL) { ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("\"RESET ALL\" blocked by set_user"), errhint("Use \"SELECT set_user();\" or \"SELECT reset_user();\" instead."))); } /* * No other kind should have a NULL name. Error rather than * allow a statement that cannot be checked. */ if (vstmt->name == NULL) elog(ERROR, "unexpected NULL name in VariableSetStmt"); /* * GUC names are case-insensitive: SET "Role" = 'none' * changes the role just like SET ROLE NONE does. */ if ((pg_strcasecmp(vstmt->name, "log_statement") == 0) && Block_LS) { ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("\"SET log_statement\" blocked by set_user config"))); } else if ((pg_strcasecmp(vstmt->name, "role") == 0)) { ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("\"SET/RESET ROLE\" blocked by set_user"), errhint("Use \"SELECT set_user();\" or \"SELECT reset_user();\" instead."))); } else if ((pg_strcasecmp(vstmt->name, "session_authorization") == 0)) { ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("\"SET/RESET SESSION AUTHORIZATION\" blocked by set_user"), errhint("Use \"SELECT set_user();\" or \"SELECT reset_user();\" instead."))); } break; } case T_CreateFunctionStmt: /* * The SET clauses of a function are applied while its body * is validated, and again whenever it is called, without * passing through this hook. See set_user_check_proconfig(). */ set_user_check_set_clauses(((CreateFunctionStmt *) pstmt->utilityStmt)->options); break; case T_AlterFunctionStmt: set_user_check_set_clauses(((AlterFunctionStmt *) pstmt->utilityStmt)->actions); break; case T_DiscardStmt: /* * DISCARD ALL resets session_authorization, which also resets * the role. The session would revert to the original role * without checking the reset token or logging the transition * so block it. Other DISCARD variants do not change the role. */ if (((DiscardStmt *) pstmt->utilityStmt)->target == DISCARD_ALL) { ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("\"DISCARD ALL\" blocked by set_user"), errhint("Use \"SELECT set_user();\" or \"SELECT reset_user();\" instead."))); } break; default: break; } } /* * Now pass-off handling either to the previous ProcessUtility hook * or to the standard ProcessUtility. */ if (prev_hook) { prev_hook(pstmt, queryString, ReadOnlyTree, context, params, queryEnv, dest, qc); } else { standard_ProcessUtility(pstmt, queryString, ReadOnlyTree, context, params, queryEnv, dest, qc); } } /* * PostSetUserHook * * Handler for set_user post hooks */ void PostSetUserHook(bool is_reset, const char *username) { List **hooks_queue; ListCell *hooks_entry = NULL; hooks_queue = (List **) find_rendezvous_variable(SET_USER_HOOKS_KEY); foreach (hooks_entry, *hooks_queue) { SetUserHooks **post_hooks = (SetUserHooks **) lfirst(hooks_entry); if (post_hooks) { if (!is_reset && (*post_hooks)->post_set_user) { (*post_hooks)->post_set_user(username); } else if ((*post_hooks)->post_reset_user) { (*post_hooks)->post_reset_user(); } } } } /* * Similar to SET SESSION AUTHORIZATION, except: * * 1. does not require superuser (GRANTable) * 2. does not allow switching to a superuser * 3. does not allow reset/switching back * 4. Can be configured to throw FATAL/exit for all ERRORs */ PG_FUNCTION_INFO_V1(set_session_auth); Datum set_session_auth(PG_FUNCTION_ARGS) { bool orig_exit_on_err = ExitOnAnyError; #if NO_ASSERT_AUTH_UID_ONCE char *newuser = text_to_cstring(PG_GETARG_TEXT_PP(0)); HeapTuple roleTup; bool NewUser_is_superuser = false; ExitOnAnyError = exit_on_error; /* Look up the username */ roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(newuser)); if (!HeapTupleIsValid(roleTup)) elog(ERROR, "role \"%s\" does not exist", newuser); NewUser_is_superuser = ((Form_pg_authid) GETSTRUCT(roleTup))->rolsuper; ReleaseSysCache(roleTup); /* cannot escalate to superuser */ if (NewUser_is_superuser) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("switching to superuser not allowed"), errhint("Use \'set_user_u\' to escalate."))); _InitializeSessionUserId(newuser, InvalidOid); #else ExitOnAnyError = exit_on_error; elog(ERROR, "Assert build disables set_session_auth()"); #endif ExitOnAnyError = orig_exit_on_err; PG_RETURN_TEXT_P(cstring_to_text("OK")); } /* * set_user_object_access * * Add some extra checking of bypass functions using the object access hook. * */ static void set_user_object_access (ObjectAccessType access, Oid classId, Oid objectId, int subId, void *arg) { /* Process the next object_access_hook before continuing */ if (next_object_access_hook) { (*next_object_access_hook)(access, classId, objectId, subId, arg); } /* If set_user has been used to transition, enforce `set_config` block. */ if (curr_state != NULL && curr_state->userid != InvalidOid) { switch (access) { case OAT_FUNCTION_EXECUTE: { /* Update the `set_config` Oid cache if necessary. */ set_user_cache_proc(InvalidOid); /* Now see if this function is blocked */ set_user_block_set_config(objectId); /* Block functions that SET role or session_authorization */ set_user_check_proconfig(objectId, ERROR); break; } case OAT_POST_ALTER: case OAT_POST_CREATE: { if (classId == ProcedureRelationId) { set_user_cache_proc(objectId); } break; } default: break; } } } /* * set_user_block_set_config * * Error out if the provided functionId is in the `set_config_procs` cache. */ static void set_user_block_set_config(Oid functionId) { MemoryContext ctx; /* This is where we store the set_config Oid cache. */ ctx = MemoryContextSwitchTo(CacheMemoryContext); /* Check the cache for the current function Oid */ if (list_member_oid(set_config_oid_cache, functionId)) { ObjectAddress object; char *funcname = NULL; object.classId = ProcedureRelationId; object.objectId = functionId; object.objectSubId = 0; funcname = getObjectIdentity(&object, false); ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("\"%s\" blocked by set_user", funcname), errhint("Use \"SET\" syntax instead."))); } MemoryContextSwitchTo(ctx); } /* * set_user_check_proc * * Check the specified HeapTuple to see if its `prosrc` attribute matches * `set_config_by_name`. Update the cache as appropriate: * * 1) Add to the cache if it's not there but `prosrc` matches. * * 2) Remove from the cache if it's present and no longer matches. */ static void set_user_check_proc(HeapTuple procTup, Relation rel) { MemoryContext ctx; Datum prosrcdatum; bool isnull; Oid procoid; /* For function metadata (Oid) */ procoid = heap_tuple_get_oid(procTup, ProcedureRelationId); /* Figure out the underlying function */ prosrcdatum = heap_getattr(procTup, Anum_pg_proc_prosrc, RelationGetDescr(rel), &isnull); if (isnull) { ereport(ERROR, (errcode(ERRCODE_INTERNAL_ERROR), errmsg("set_user: null prosrc for function %u", procoid))); } /* * The Oid cache is as good as the underlying cache context, so store it * there. */ ctx = MemoryContextSwitchTo(CacheMemoryContext); /* Make sure the Oid cache is up-to-date */ if (strcmp(TextDatumGetCString(prosrcdatum), set_config_proc_name) == 0) { set_config_oid_cache = list_append_unique_oid(set_config_oid_cache, procoid); } else if (list_member_oid(set_config_oid_cache, procoid)) { set_config_oid_cache = list_delete_oid(set_config_oid_cache, procoid); } MemoryContextSwitchTo(ctx); } /* * set_user_cache_proc * * This function has two modes of operation, based on the provided argument: * * 1) `functionId` is not set (InvalidOid) - scan all procedures to * initialize a list of function Oids which call `set_config_by_name()` under the * hood. * * 2) `functionId` is a valid Oid - grab the syscache entry for the provided * Oid to inspect `prosrc` attribute and determine whether it should be in the * `set_config_oid_cache` list. */ static void set_user_cache_proc(Oid functionId) { HeapTuple procTup; Relation rel; SysScanDesc sscan; /* Defaults for full catalog scan */ Oid indexId = InvalidOid; bool indexOk = false; Snapshot snapshot = NULL; int nkeys = 0; ScanKeyData skey; /* * If checking the cache for a specific function Oid, we need to narrow the heap * scan by setting a scan key and some other data. */ if (functionId != InvalidOid) { indexId = ProcedureOidIndexId; indexOk = true; snapshot = SnapshotSelf; nkeys = 1; ScanKeyInit(&skey, Anum_pg_proc_oid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(functionId)); } else if (set_config_oid_cache != NIL) { /* No need to re-initialize the cache. We've already been here. */ return; } /* Go ahead and do the work */ PG_TRY(); { rel = table_open(ProcedureRelationId, AccessShareLock); sscan = systable_beginscan(rel, indexId, indexOk, snapshot, nkeys, &skey); /* * InvalidOid implies complete heap scan to initialize the * set_config cache. * * If we have a scankey, this should only match one item. */ while (HeapTupleIsValid(procTup = systable_getnext(sscan))) { set_user_check_proc(procTup, rel); } } PG_CATCH(); { systable_endscan(sscan); table_close(rel, NoLock); PG_RE_THROW(); } PG_END_TRY(); systable_endscan(sscan); table_close(rel, NoLock); } /* * set_user_blocked_guc * * Return true if the named parameter must not be changed by a function SET * clause while set_user is active. These are the same parameters the * ProcessUtility hook blocks for plain SET/RESET. GUC names are * case-insensitive, so match them the way guc.c does. */ static bool set_user_blocked_guc(const char *name) { if (name == NULL) return false; if (pg_strcasecmp(name, "role") == 0 || pg_strcasecmp(name, "session_authorization") == 0) return true; if (Block_LS && pg_strcasecmp(name, "log_statement") == 0) return true; return false; } /* * set_user_check_set_clauses * * Error out if the options of a CREATE/ALTER FUNCTION or PROCEDURE statement * contain a SET clause for a blocked parameter. CREATE FUNCTION applies the * SET clauses while running the language validator, so the function does not * even have to be called to change the role. */ static void set_user_check_set_clauses(List *options) { ListCell *lc; foreach(lc, options) { DefElem *defel = (DefElem *) lfirst(lc); VariableSetStmt *sstmt; if (strcmp(defel->defname, "set") != 0) continue; sstmt = (VariableSetStmt *) defel->arg; /* Removing a SET clause is harmless */ if (sstmt->kind == VAR_RESET || sstmt->kind == VAR_RESET_ALL) continue; if (set_user_blocked_guc(sstmt->name)) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("function SET clause for parameter \"%s\" blocked by set_user", sstmt->name), errhint("Use \"SELECT set_user();\" or \"SELECT reset_user();\" instead."))); } } /* * set_user_check_proconfig * * Report at elevel if the given function has a SET clause for a blocked * parameter. * * Function SET clauses are applied by fmgr_security_definer() and never pass * through the ProcessUtility hook. SET role = 'none' or SET * session_authorization = '' would make the function run as * the original role without the reset token and without an audit log entry. * Worse, since set_user switches roles with SetCurrentRoleId() rather than * through the "role" GUC, restoring the GUC when the function exits (or when * the transaction aborts) leaves the session as the original role for good. */ static void set_user_check_proconfig(Oid functionId, int elevel) { HeapTuple procTup; Datum proconfig; bool isnull; char *blocked = NULL; procTup = SearchSysCache1(PROCOID, ObjectIdGetDatum(functionId)); if (!HeapTupleIsValid(procTup)) return; proconfig = SysCacheGetAttr(PROCOID, procTup, Anum_pg_proc_proconfig, &isnull); if (!isnull) { Datum *elems; int nelems; int i; /* text has typlen -1, typbyval false and typalign 'i' */ deconstruct_array(DatumGetArrayTypeP(proconfig), TEXTOID, -1, false, TYPALIGN_INT, &elems, NULL, &nelems); for (i = 0; i < nelems && blocked == NULL; i++) { char *name; char *value; ParseLongOption(TextDatumGetCString(elems[i]), &name, &value); if (set_user_blocked_guc(name)) blocked = name; } } ReleaseSysCache(procTup); if (blocked != NULL) ereport(elevel, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("function \"%s\" with SET clause for parameter \"%s\" blocked by set_user", get_func_name(functionId), blocked), errhint("Use \"SELECT set_user();\" or \"SELECT reset_user();\" instead."))); } /* * set_user_fmgr_hook * * Backstop for set_user_check_proconfig() on call paths that do not invoke * the object access hook first, such as trigger functions. * * By the time FHET_START is called the function's SET clauses have already * been applied, so the role has already changed. Raising ERROR here is not * enough: the abort restores the "role" GUC, which puts the session back to * the original role, the same as a normal exit from the function. Terminate * the session instead. */ static void set_user_fmgr_hook(FmgrHookEventType event, FmgrInfo *flinfo, Datum *arg) { if (event == FHET_START && curr_state != NULL && curr_state->userid != InvalidOid) set_user_check_proconfig(flinfo->fn_oid, FATAL); if (next_fmgr_hook) (*next_fmgr_hook) (event, flinfo, arg); } pgaudit-set_user-45fc481/src/set_user.h000066400000000000000000000022761527232611200201370ustar00rootroot00000000000000#ifndef SET_USER_H #define SET_USER_H #include "nodes/pg_list.h" typedef struct SetUserHooks { void (*post_set_user) (const char *username); void (*post_reset_user) (); } SetUserHooks; #define SET_USER_HOOKS_KEY "SetUserHooks" /* * register_set_user_hooks * * Utility function for registering an extension's implementation of the * set_user hooks. * * Takes in two function pointers, which should be defined in the extension. * Each subsequent call to this function adds a new hook to the queue. */ static inline void register_set_user_hooks(void *set_user_hook, void *reset_user_hook) { static List **HooksQueue; static SetUserHooks *next_hook_entry = NULL; MemoryContext oldcontext; oldcontext = MemoryContextSwitchTo(TopMemoryContext); /* Grab the SetUserHooks queue from the rendezvous hash */ HooksQueue = (List **) find_rendezvous_variable(SET_USER_HOOKS_KEY); /* Populate a new hooks entry and append it to the queue */ next_hook_entry = palloc0(sizeof(SetUserHooks)); next_hook_entry->post_set_user = set_user_hook; next_hook_entry->post_reset_user = reset_user_hook; *HooksQueue = lappend(*HooksQueue, &next_hook_entry); MemoryContextSwitchTo(oldcontext); } #endif pgaudit-set_user-45fc481/test/000077500000000000000000000000001527232611200163165ustar00rootroot00000000000000pgaudit-set_user-45fc481/test/Dockerfile.debian000066400000000000000000000021761527232611200215370ustar00rootroot00000000000000FROM ubuntu:latest # Install packages RUN apt-get update RUN DEBIAN_FRONTEND=noninteractive apt-get install -y sudo wget gnupg tzdata locales lsb-release apt-utils make gcc libssl-dev \ libkrb5-dev # PostgreSQL version ARG PGVER=18 # Remove the default ubuntu user to reduce the chance of a conflict with the host user RUN userdel ubuntu # Create postgres user/group with specific IDs ARG UID=1000 ARG GID=1000 RUN groupadd -g $GID -o postgres RUN useradd -m -u $UID -g $GID -o -s /bin/bash postgres # Add PostgreSQL repository RUN apt-get install -y --no-install-recommends postgresql-common RUN /usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y -c 19 # Install PostgreSQL RUN apt-get install -y postgresql-${PGVER?} postgresql-server-dev-${PGVER?} # Create PostgreSQL cluster ENV PGBIN=/usr/lib/postgresql/${PGVER}/bin ENV PGDATA="/var/lib/postgresql/${PGVER}/test" ENV PATH="${PATH}:${PGBIN}" RUN sudo -u postgres ${PGBIN?}/initdb -A trust -k ${PGDATA?} RUN echo "shared_preload_libraries = 'set_user'" >> ${PGDATA}/postgresql.conf # Configure sudo RUN echo 'postgres ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers USER postgres pgaudit-set_user-45fc481/test/README.md000066400000000000000000000005461527232611200176020ustar00rootroot00000000000000# Testing Testing is performed using a Docker container. First build the container with the desired PostgreSQL version: ``` docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) --build-arg PGVER=18 -f test/Dockerfile.debian -t set_user-test . ``` Then run the test: ``` docker run --rm -v $(pwd):/set_user set_user-test /set_user/test/test.sh ``` pgaudit-set_user-45fc481/test/test.sh000077500000000000000000000005301527232611200176320ustar00rootroot00000000000000#!/bin/bash set -e # Clean and build set_user make -C /set_user clean all USE_PGXS=1 # Install set_user so postgres will start with shared_preload_libraries set sudo bash -c "PATH=${PATH?} make -C /set_user install USE_PGXS=1" # Start postgres ${PGBIN}/pg_ctl -w start -D ${PGDATA} # Test set_user make -C /set_user installcheck USE_PGXS=1 pgaudit-set_user-45fc481/updates/000077500000000000000000000000001527232611200170045ustar00rootroot00000000000000pgaudit-set_user-45fc481/updates/set_user--1.0--1.1.sql000066400000000000000000000005621527232611200223040ustar00rootroot00000000000000/* set-user-1.0--1.1.sql */ SET LOCAL search_path to @extschema@; -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION set_user UPDATE to '1.1'" to load this file. \quit CREATE FUNCTION set_user_u(text) RETURNS text AS 'MODULE_PATHNAME', 'set_user' LANGUAGE C; REVOKE EXECUTE ON FUNCTION set_user_u(text) FROM PUBLIC; pgaudit-set_user-45fc481/updates/set_user--1.1--1.4.sql000066400000000000000000000004021527232611200223010ustar00rootroot00000000000000/* set-user-1.1--1.4.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION set_user UPDATE to '1.4'" to load this file. \quit -- just bumping our version to 1.4. no new features here, so nothing to do. pgaudit-set_user-45fc481/updates/set_user--1.4--1.5.sql000066400000000000000000000004171527232611200223130ustar00rootroot00000000000000/* set-user-1.4--1.5.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION set_user UPDATE to '1.5'" to load this file. \quit -- just bumping our version to 1.5. no new sql function features here, so nothing to do. pgaudit-set_user-45fc481/updates/set_user--1.5--1.6.sql000066400000000000000000000004171527232611200223150ustar00rootroot00000000000000/* set-user-1.5--1.6.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION set_user UPDATE to '1.6'" to load this file. \quit -- just bumping our version to 1.6. no new sql function features here, so nothing to do. pgaudit-set_user-45fc481/updates/set_user--1.6--2.0.sql000066400000000000000000000004171527232611200223110ustar00rootroot00000000000000/* set-user-1.6--2.0.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION set_user UPDATE to '2.0'" to load this file. \quit -- just bumping our version to 2.0. no new sql function features here, so nothing to do. pgaudit-set_user-45fc481/updates/set_user--2.0--3.0.sql000066400000000000000000000006451527232611200223100ustar00rootroot00000000000000/* set-user-2.0--3.0.sql */ SET LOCAL search_path to @extschema@; -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION set_user UPDATE to '3.0'" to load this file. \quit CREATE FUNCTION @extschema@.set_session_auth(text) RETURNS text AS 'MODULE_PATHNAME', 'set_session_auth' LANGUAGE C STRICT; REVOKE EXECUTE ON FUNCTION @extschema@.set_session_auth(text) FROM PUBLIC; pgaudit-set_user-45fc481/updates/set_user--3.0--4.0.0.sql000066400000000000000000000004011527232611200224360ustar00rootroot00000000000000/* set-user-3.0--4.0.0.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION set_user UPDATE" to load this file. \quit -- just bumping our version to 4.0.0. no new SQL features here, so nothing to do. pgaudit-set_user-45fc481/updates/set_user--3.0--4.0.0rc1.sql000066400000000000000000000004071527232611200230520ustar00rootroot00000000000000/* set-user-3.0--4.0.0rc1.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION set_user UPDATE" to load this file. \quit -- just bumping our version to 4.0.0rc1. no new SQL features here, so nothing to do. pgaudit-set_user-45fc481/updates/set_user--4.0.0--4.0.1.sql000066400000000000000000000004041527232611200226010ustar00rootroot00000000000000/* set-user--4.0.0--4.0.1.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION set_user UPDATE" to load this file. \quit -- just bumping our version to 4.0.1. no new SQL features here, so nothing to do. pgaudit-set_user-45fc481/updates/set_user--4.0.0rc1--4.0.0.sql000066400000000000000000000004001527232611200232020ustar00rootroot00000000000000/* set-user--4.0.0rc1--4.0.0.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION set_user UPDATE" to load this file. \quit -- Allow users that may have installed 4.0.0RC1 to upgrade to 4.0.0 stable pgaudit-set_user-45fc481/updates/set_user--4.0.0rc1.sql000066400000000000000000000027561527232611200225100ustar00rootroot00000000000000/* set-user--4.0.0rc1.sql */ SET LOCAL search_path to @extschema@; -- complain if script is sourced in psql, rather than via CREATE EXTENSION \echo Use "CREATE EXTENSION set_user" to load this file. \quit CREATE FUNCTION @extschema@.set_user(text) RETURNS text AS 'MODULE_PATHNAME', 'set_user' LANGUAGE C; CREATE FUNCTION @extschema@.set_user(text, text) RETURNS text AS 'MODULE_PATHNAME', 'set_user' LANGUAGE C STRICT; REVOKE EXECUTE ON FUNCTION @extschema@.set_user(text) FROM PUBLIC; REVOKE EXECUTE ON FUNCTION @extschema@.set_user(text, text) FROM PUBLIC; CREATE FUNCTION @extschema@.reset_user() RETURNS text AS 'MODULE_PATHNAME', 'set_user' LANGUAGE C; CREATE FUNCTION @extschema@.reset_user(text) RETURNS text AS 'MODULE_PATHNAME', 'set_user' LANGUAGE C STRICT; GRANT EXECUTE ON FUNCTION @extschema@.reset_user() TO PUBLIC; GRANT EXECUTE ON FUNCTION @extschema@.reset_user(text) TO PUBLIC; /* New functions in 1.1 (now 1.4) begin here */ CREATE FUNCTION @extschema@.set_user_u(text) RETURNS text AS 'MODULE_PATHNAME', 'set_user' LANGUAGE C STRICT; REVOKE EXECUTE ON FUNCTION @extschema@.set_user_u(text) FROM PUBLIC; /* No new sql functions for 1.5 */ /* No new sql functions for 1.6 */ /* No new sql functions for 2.0 */ /* New functions in 3.0 begin here */ CREATE FUNCTION @extschema@.set_session_auth(text) RETURNS text AS 'MODULE_PATHNAME', 'set_session_auth' LANGUAGE C STRICT; REVOKE EXECUTE ON FUNCTION @extschema@.set_session_auth(text) FROM PUBLIC; /* No new sql functions for 4.0.0 */ pgaudit-set_user-45fc481/updates/set_user--4.0.1--4.1.0.sql000066400000000000000000000004041527232611200226020ustar00rootroot00000000000000/* set-user--4.0.1--4.1.0.sql */ -- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION set_user UPDATE" to load this file. \quit -- just bumping our version to 4.1.0. no new SQL features here, so nothing to do. pgaudit-set_user-45fc481/updates/set_user--4.1.0--4.2.0.sql000066400000000000000000000002171527232611200226050ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION set_user UPDATE" to load this file. \quit pgaudit-set_user-45fc481/updates/set_user--4.2.0--4.3.0.sql000066400000000000000000000002171527232611200226070ustar00rootroot00000000000000-- complain if script is sourced in psql, rather than via ALTER EXTENSION \echo Use "ALTER EXTENSION set_user UPDATE" to load this file. \quit