pax_global_header00006660000000000000000000000064146565432760014534gustar00rootroot0000000000000052 comment=1dc57a8b6ad0adc01e2c9f21d468f2c140056261 set_user-REL4_1_0/000077500000000000000000000000001465654327600140745ustar00rootroot00000000000000set_user-REL4_1_0/.github/000077500000000000000000000000001465654327600154345ustar00rootroot00000000000000set_user-REL4_1_0/.github/docker/000077500000000000000000000000001465654327600167035ustar00rootroot00000000000000set_user-REL4_1_0/.github/docker/Dockerfile000066400000000000000000000005301465654327600206730ustar00rootroot00000000000000# set_user Docker image # This image is used for testing the set_user build process ARG PGVER FROM postgres:${PGVER} ARG PGVER ARG DEVPKG ENV DEBIAN_FRONTEND=noninteractive COPY . /src/set_user WORKDIR /src/set_user RUN apt-get update && \ apt-get -y upgrade && \ apt-get -y install postgresql-server-dev-${DEVPKG} make gcc RUN make install set_user-REL4_1_0/.github/resources/000077500000000000000000000000001465654327600174465ustar00rootroot00000000000000set_user-REL4_1_0/.github/resources/scripts/000077500000000000000000000000001465654327600211355ustar00rootroot00000000000000set_user-REL4_1_0/.github/resources/scripts/healthcheck.sh000066400000000000000000000026401465654327600237360ustar00rootroot00000000000000#!/usr/bin/env bash # Modified from https://github.com/jordyv/wait-for-healthy-container container_name=$1 shift timeout=$1 default_timeout=120 if [ -z ${timeout} ]; then timeout=${default_timeout} fi RETURN_HEALTHY=0 RETURN_STARTING=1 RETURN_UNHEALTHY=2 RETURN_UNKNOWN=3 RETURN_ERROR=99 function usage() { echo " Usage: healthcheck.sh [timeout] " return } function get_health_state { state=$(docker inspect -f '{{ .State.Health.Status }}' ${container_name}) return_code=$? if [ ! ${return_code} -eq 0 ]; then exit ${RETURN_ERROR} fi if [[ "${state}" == "healthy" ]]; then return ${RETURN_HEALTHY} elif [[ "${state}" == "unhealthy" ]]; then return ${RETURN_UNHEALTHY} elif [[ "${state}" == "starting" ]]; then return ${RETURN_STARTING} else return ${RETURN_UNKNOWN} fi } function wait_for() { echo "Wait for container '$container_name' to be healthy for max $timeout seconds..." for i in `seq ${timeout}`; do get_health_state state=$? if [ ${state} -eq 0 ]; then echo "Container is healthy after ${i} seconds." exit 0 fi sleep 1 done echo "Timeout exceeded. Health status returned: $(docker inspect -f '{{ .State.Health.Status }}' ${container_name})" exit 1 } if [ -z ${container_name} ]; then usage exit 1 else wait_for fiset_user-REL4_1_0/.github/resources/set_user.yml000066400000000000000000000005001465654327600220150ustar00rootroot00000000000000# Custom service that contains the postgres with the installed set_user extension version: '3' services: set_user: container_name: set_user image: set_user:latest environment: POSTGRES_HOST_AUTH_METHOD: "trust" healthcheck: test: ["CMD", "pg_isready"] interval: 10s timeout: 5s set_user-REL4_1_0/.github/workflows/000077500000000000000000000000001465654327600174715ustar00rootroot00000000000000set_user-REL4_1_0/.github/workflows/regression-tests.yml000066400000000000000000000034761465654327600235460ustar00rootroot00000000000000# CI for set_user Pull Requests and pushes to the cicd branch. # Runs regression tests against all supported versions of postgres. on: push: branches: - integration - '**-ci' pull_request: branches: - master - integration jobs: regression-tests: runs-on: ubuntu-latest env: DOCKER_DIR: ${{ github.workspace }}/.github/docker RESOURCE_DIR: ${{ github.workspace }}/.github/resources SCRIPT_DIR: ${{ github.workspace }}/.github/resources/scripts DEVPKG: ${{ matrix.devpkg }} strategy: fail-fast: false matrix: pgver: [12, 13, 14, 15, 16, 17beta2] steps: - name: Checkout set_user repo uses: actions/checkout@v4 - name: Set DEVPKG to pgver if unset if: ${{ env.DEVPKG == '' }} run: | # Cut off label and leave only major version number (17beta2 -> 17) DEVPKG=$(echo ${{ matrix.pgver }} | sed 's/^\([0-9]\{2\}\).*/\1/') echo "DEVPKG=$DEVPKG" >> $GITHUB_ENV; - name: Build set_user run: | sudo apt-get install -y docker-compose docker build -t set_user:latest \ --build-arg PGVER=${{ matrix.pgver }} \ --build-arg DEVPKG=${{ env.DEVPKG }} \ -f ${{ env.DOCKER_DIR }}/Dockerfile . - name: Run PG set_user run: | docker-compose -f ${{ env.RESOURCE_DIR }}/set_user.yml up -d /bin/bash ${{ env.SCRIPT_DIR }}/healthcheck.sh set_user 60 - name: Run tests run: | docker exec set_user make -C /src/set_user USE_PGXS=1 REGRESS_OPTS='--user=postgres' installcheck - name: Show any regression diffs if: ${{ failure() }} run: | docker cp set_user:/src/set_user/regression.diffs ./regression.diffs cat ./regression.diffs set_user-REL4_1_0/.gitignore000066400000000000000000000002101465654327600160550ustar00rootroot00000000000000# Derived objects set_user.o set_user.so results # Generated documentation *.pdf # Generated extension file extension/set_user--*.sql set_user-REL4_1_0/CHANGELOG.md000066400000000000000000000016451465654327600157130ustar00rootroot000000000000004.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. set_user-REL4_1_0/LICENSE000066400000000000000000000020611465654327600151000ustar00rootroot00000000000000This code is released under the PostgreSQL license. Copyright 2015-2024 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. set_user-REL4_1_0/Makefile000066400000000000000000000021121465654327600155300ustar00rootroot00000000000000EXTENSION = set_user EXTVERSION = $(shell grep default_version $(EXTENSION).control | \ sed -e "s/default_version[[:space:]]*=[[:space:]]*'\([^']*\)'/\1/") LDFLAGS_SL += $(filter -lm, $(LIBS)) 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" set_user-REL4_1_0/README.md000066400000000000000000000601501465654327600153550ustar00rootroot00000000000000# 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. * `log_statement` setting is set to "all", meaning every SQL statement executed while in this state will also get logged. * 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. 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'`) * 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'`) * 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. This extension supports PostgreSQL versions 12 and higher. Prior versions of PostgreSQL are supported by prior versions of set_user. ## 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 9.4 or higher. ### 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. set_user-REL4_1_0/docbuilder/000077500000000000000000000000001465654327600162105ustar00rootroot00000000000000set_user-REL4_1_0/docbuilder/.gitignore000066400000000000000000000000111465654327600201700ustar00rootroot00000000000000.vagrant set_user-REL4_1_0/docbuilder/README.txt000066400000000000000000000003141465654327600177040ustar00rootroot00000000000000Note: 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 set_user-REL4_1_0/docbuilder/Vagrantfile000066400000000000000000000017341465654327600204020ustar00rootroot00000000000000# 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 set_user-REL4_1_0/expected/000077500000000000000000000000001465654327600156755ustar00rootroot00000000000000set_user-REL4_1_0/expected/set_user.out000066400000000000000000000241551465654327600202660ustar00rootroot00000000000000CREATE 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; 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_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. 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. 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 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; set_user-REL4_1_0/expected/set_user_1.out000066400000000000000000000241051465654327600205010ustar00rootroot00000000000000CREATE 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; 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_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. 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. 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 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 ----------------- (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 ----------------- 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 ----------------- 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 ----------------- (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 ----------------- (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; set_user-REL4_1_0/expected/set_user_2.out000066400000000000000000000235061465654327600205060ustar00rootroot00000000000000CREATE 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; 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_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. 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. 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 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 ! 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 ----------------- (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 ----------------- AUDIT: (1 row) SELECT reset_user(), bail(); ERROR: bailing out ! 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 ----------------- AUDIT: (1 row) SELECT reset_user(); reset_user ------------ OK (1 row) -- bail during set_user SELECT set_user('bob'), bail(); ERROR: bailing out ! 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 ----------------- (1 row) -- bail during set_user with token SELECT set_user('bob', 'secret'), bail(); ERROR: bailing out ! 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 ----------------- (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 ! 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; set_user-REL4_1_0/extension/000077500000000000000000000000001465654327600161105ustar00rootroot00000000000000set_user-REL4_1_0/extension/set_user.sql000066400000000000000000000027531465654327600204710ustar00rootroot00000000000000/* set-user--4.0.0.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 */ set_user-REL4_1_0/set_user.control000066400000000000000000000002371465654327600173310ustar00rootroot00000000000000# set_user extension comment = 'similar to SET ROLE but with added logging' default_version = '4.1.0' module_pathname = '$libdir/set_user' relocatable = false set_user-REL4_1_0/sql/000077500000000000000000000000001465654327600146735ustar00rootroot00000000000000set_user-REL4_1_0/sql/set_user.sql000066400000000000000000000132301465654327600172440ustar00rootroot00000000000000CREATE 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; 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_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 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 SELECT SESSION_USER, CURRENT_USER; SELECT reset_user(); -- 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; set_user-REL4_1_0/src/000077500000000000000000000000001465654327600146635ustar00rootroot00000000000000set_user-REL4_1_0/src/compatibility.h000066400000000000000000000112421465654327600177050ustar00rootroot00000000000000/* ------------------------------------------------------------------------- * * 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 /* * 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+ */ /* * PostgreSQL version 14+ * * Introduces ReadOnlyTree boolean */ #if PG_VERSION_NUM >= 140000 #define _PU_HOOK \ static void PU_hook(PlannedStmt *pstmt, const char *queryString, bool ReadOnlyTree, \ ProcessUtilityContext context, ParamListInfo params, \ QueryEnvironment *queryEnv, \ DestReceiver *dest, QueryCompletion *qc) #define _prev_hook \ prev_hook(pstmt, queryString, ReadOnlyTree, context, params, queryEnv, dest, qc) #define _standard_ProcessUtility \ standard_ProcessUtility(pstmt, queryString, ReadOnlyTree, context, params, queryEnv, dest, qc) #define getObjectIdentity(address) \ getObjectIdentity(address,false) #endif /* 14+ */ /* * PostgreSQL version 13+ * * Introduces QueryCompletion struct */ #if PG_VERSION_NUM >= 130000 #ifndef _PU_HOOK #define _PU_HOOK \ static void PU_hook(PlannedStmt *pstmt, const char *queryString, \ ProcessUtilityContext context, ParamListInfo params, \ QueryEnvironment *queryEnv, \ DestReceiver *dest, QueryCompletion *qc) #define _prev_hook \ prev_hook(pstmt, queryString, context, params, queryEnv, dest, qc) #define _standard_ProcessUtility \ standard_ProcessUtility(pstmt, queryString, context, params, queryEnv, dest, qc) #endif #define TABLEOPEN #endif /* 13+ */ /* * PostgreSQL version 12+ * * - Removes OID column */ #if PG_VERSION_NUM >= 120000 #ifndef _PU_HOOK #define _PU_HOOK \ static void PU_hook(PlannedStmt *pstmt, const char *queryString, \ ProcessUtilityContext context, ParamListInfo params, \ QueryEnvironment *queryEnv, \ DestReceiver *dest, char *completionTag) #define _prev_hook \ prev_hook(pstmt, queryString, context, params, queryEnv, dest, completionTag) #define _standard_ProcessUtility \ standard_ProcessUtility(pstmt, queryString, context, params, queryEnv, dest, completionTag) #endif #include "utils/varlena.h" #define parsetree ((Node *) pstmt->utilityStmt) #define HEAP_TUPLE_GET_OID /* * _heap_tuple_get_oid * * Return the oid of the tuple based on the provided catalogID. */ static inline 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; } } #include "access/table.h" #define OBJECTADDRESS /* * _scan_key_init * * Initialize entry based on the catalogID provided. */ static inline void _scan_key_init(ScanKey entry, Oid catalogID, StrategyNumber strategy, RegProcedure procedure, Datum argument) { switch (catalogID) { case ProcedureRelationId: ScanKeyInit(entry, Anum_pg_proc_oid, strategy, procedure, argument); break; default: ereport(ERROR, (errcode(ERRCODE_SYNTAX_ERROR), errmsg("set_user: invalid relation ID provided"))); } } // Introduces two-argument GetUserNameFromId #define GETUSERNAMEFROMID(ouserid) GetUserNameFromId(ouserid, false) #ifndef INITSESSIONUSER #define INITSESSIONUSER #define _InitializeSessionUserId(name,ouserid) InitializeSessionUserId(name,ouserid) #endif #endif /* 12+ */ #if !defined(PG_VERSION_NUM) || PG_VERSION_NUM < 120000 #error "This extension only builds with PostgreSQL 12 or later" #endif /* Use our version-specific static declaration here */ _PU_HOOK; #endif /* SET_USER_COMPAT_H */ set_user-REL4_1_0/src/set_user.c000066400000000000000000000605151465654327600166670ustar00rootroot00000000000000/* * set_user.c * * Joe Conway * * This code is released under the PostgreSQL license. * * Copyright 2015-2022 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/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 "miscadmin.h" #include "parser/parse_func.h" #include "tcop/utility.h" #include "utils/acl.h" #include "utils/builtins.h" #include "utils/catcache.h" #include "utils/fmgroids.h" #include "utils/guc.h" #include "utils/memutils.h" #include "utils/snapmgr.h" #include "utils/syscache.h" #include "utils/rel.h" #include "set_user.h" PG_MODULE_MAGIC; #include "compatibility.h" #define ALLOWLIST_WILDCARD "*" #define SUPERUSER_AUDIT_TAG "AUDIT" static ProcessUtility_hook_type prev_hook = NULL; static object_access_hook_type next_object_access_hook; /* 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); 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); /* * 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); if (!OidIsValid(roleId)) result = false; /* Check to see if userId is contained by group role in allowlist */ result = has_privs_of_role(userId, roleId); } else { if (pg_strcasecmp(elem, GETUSERNAMEFROMID(userId)) == 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; } /* * 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))); } else if (!NameStr(procStruct->proname)) { ereport(ERROR, (errcode(ERRCODE_INVALID_PARAMETER_VALUE), errmsg("set_user: NULL name for function %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); 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 (strcmp(prev_state->reset_token, pending_state->reset_token) != 0) { 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); 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; RegisterXactCallback(set_user_xact_handler, NULL); } void _PG_fini(void) { ProcessUtility_hook = prev_hook; } /* * _PU_HOOK * * Compatibility shim for PU_hook. Handles changing function signature * between versions of PostgreSQL. */ _PU_HOOK { /* if set_user has been used to transition, enforce set_user GUCs */ if (curr_state != NULL && curr_state->userid != InvalidOid) { switch (nodeTag(parsetree)) { 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 *) parsetree)->is_program && Block_CP) ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("COPY PROGRAM blocked by set_user config"))); break; case T_VariableSetStmt: if ((strcmp(((VariableSetStmt *) parsetree)->name, "log_statement") == 0) && Block_LS) { ereport(ERROR, (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), errmsg("\"SET log_statement\" blocked by set_user config"))); } else if ((strcmp(((VariableSetStmt *) parsetree)->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 ((strcmp(((VariableSetStmt *) parsetree)->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; default: break; } } /* * Now pass-off handling either to the previous ProcessUtility hook * or to the standard ProcessUtility. * * These functions are also called by their compatibility variants. */ if (prev_hook) { _prev_hook; } else { _standard_ProcessUtility; } } /* * 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); 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); 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; _scan_key_init(&skey, ProcedureRelationId, 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-REL4_1_0/src/set_user.h000066400000000000000000000022761465654327600166740ustar00rootroot00000000000000#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 set_user-REL4_1_0/updates/000077500000000000000000000000001465654327600155415ustar00rootroot00000000000000set_user-REL4_1_0/updates/set_user--1.0--1.1.sql000066400000000000000000000005621465654327600210410ustar00rootroot00000000000000/* 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; set_user-REL4_1_0/updates/set_user--1.1--1.4.sql000066400000000000000000000004021465654327600210360ustar00rootroot00000000000000/* 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. set_user-REL4_1_0/updates/set_user--1.4--1.5.sql000066400000000000000000000004171465654327600210500ustar00rootroot00000000000000/* 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. set_user-REL4_1_0/updates/set_user--1.5--1.6.sql000066400000000000000000000004171465654327600210520ustar00rootroot00000000000000/* 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. set_user-REL4_1_0/updates/set_user--1.6--2.0.sql000066400000000000000000000004171465654327600210460ustar00rootroot00000000000000/* 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. set_user-REL4_1_0/updates/set_user--2.0--3.0.sql000066400000000000000000000006451465654327600210450ustar00rootroot00000000000000/* 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; set_user-REL4_1_0/updates/set_user--3.0--4.0.0.sql000066400000000000000000000004011465654327600211730ustar00rootroot00000000000000/* 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. set_user-REL4_1_0/updates/set_user--3.0--4.0.0rc1.sql000066400000000000000000000004071465654327600216070ustar00rootroot00000000000000/* 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. set_user-REL4_1_0/updates/set_user--4.0.0--4.0.1.sql000066400000000000000000000004041465654327600213360ustar00rootroot00000000000000/* 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. set_user-REL4_1_0/updates/set_user--4.0.0rc1--4.0.0.sql000066400000000000000000000004001465654327600217370ustar00rootroot00000000000000/* 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 set_user-REL4_1_0/updates/set_user--4.0.0rc1.sql000066400000000000000000000027561465654327600212450ustar00rootroot00000000000000/* 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 */ set_user-REL4_1_0/updates/set_user--4.0.1--4.1.0.sql000066400000000000000000000004041465654327600213370ustar00rootroot00000000000000/* 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.