├── .editorconfig ├── .github └── workflows │ └── test.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── docbuilder ├── .gitignore ├── README.txt └── Vagrantfile ├── expected └── set_user.out ├── extension └── set_user.sql ├── set_user.control ├── sql └── set_user.sql ├── src ├── compatibility.h ├── set_user.c └── set_user.h ├── test ├── Dockerfile.debian ├── README.md └── test.sh └── updates ├── set_user--1.0--1.1.sql ├── set_user--1.1--1.4.sql ├── set_user--1.4--1.5.sql ├── set_user--1.5--1.6.sql ├── set_user--1.6--2.0.sql ├── set_user--2.0--3.0.sql ├── set_user--3.0--4.0.0.sql ├── set_user--3.0--4.0.0rc1.sql ├── set_user--4.0.0--4.0.1.sql ├── set_user--4.0.0rc1--4.0.0.sql ├── set_user--4.0.0rc1.sql └── set_user--4.0.1--4.1.0.sql /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [Makefile] 12 | indent_style = tab 13 | indent_size = 4 14 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - integration 5 | - '**-ci' 6 | pull_request: 7 | branches: 8 | - master 9 | - integration 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | 15 | strategy: 16 | # Let all the jobs run to completion even if one fails 17 | fail-fast: false 18 | 19 | # Test all supported versions 20 | matrix: 21 | pgver: [13, 14, 15, 16, 17] 22 | 23 | steps: 24 | - name: Checkout Code 25 | uses: actions/checkout@v4 26 | with: 27 | path: set_user 28 | 29 | - name: Build Test Container 30 | run: docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) --build-arg PGVER=${{matrix.pgver}} -f ${GITHUB_WORKSPACE?}/set_user/test/Dockerfile.debian -t set_user-test ${GITHUB_WORKSPACE?}/set_user 31 | 32 | - name: Run Test 33 | run: docker run -v ${GITHUB_WORKSPACE?}/set_user:/set_user set_user-test /set_user/test/test.sh 34 | 35 | - name: Show Any Regression Diffs 36 | if: ${{ failure() }} 37 | run: | 38 | cat ${GITHUB_WORKSPACE?}/set_user/regression.diffs 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Derived objects 2 | set_user.o 3 | set_user.so 4 | set_user.bc 5 | results 6 | 7 | # Generated documentation 8 | *.pdf 9 | 10 | # Generated extension file 11 | extension/set_user--*.sql 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 4.1.0 2 | ===== 3 | 4 | NEW FEATURES 5 | ------------ 6 | - Add PostgreSQL 17 support. 7 | - Remove support for PostgreSQL < 12. 8 | 9 | 4.0.1 10 | ===== 11 | 12 | NEW FEATURES 13 | ------------ 14 | - Reorganized repository structure to allow for easier management of extension files during build process. 15 | - Added NO_PGXS build flag to allow building of extension without PGXS. Restores ability to build on Windows. 16 | - No changes to extension code. 17 | 18 | 2.0.1 19 | ===== 20 | 21 | NEW FEATURES 22 | ------------ 23 | - Deprecated GUCs are removed from `SHOW ALL`. 24 | 25 | BUGFIXES 26 | -------- 27 | - NOTICE fixed to only display on first reference to non-default deprecated variable. 28 | 29 | 2.0.0 30 | ===== 31 | 32 | NEW FEATURES 33 | ------------ 34 | - 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. 35 | - The extension is now non-relocatable and all functions are schema-qualified. 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This code is released under the PostgreSQL license. 2 | 3 | Copyright 2015-2025 Crunchy Data Solutions, Inc. 4 | 5 | Permission to use, copy, modify, and distribute this software and its 6 | documentation for any purpose, without fee, and without a written agreement is 7 | hereby granted, provided that the above copyright notice and this paragraph and 8 | the following two paragraphs appear in all copies. 9 | 10 | IN NO EVENT SHALL CRUNCHY DATA SOLUTIONS, INC. BE LIABLE TO ANY PARTY FOR 11 | DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING LOST 12 | PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF 13 | THE CRUNCHY DATA SOLUTIONS, INC. HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH 14 | DAMAGE. 15 | 16 | THE CRUNCHY DATA SOLUTIONS, INC. SPECIFICALLY DISCLAIMS ANY WARRANTIES, 17 | INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND 18 | FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS 19 | IS" BASIS, AND THE CRUNCHY DATA SOLUTIONS, INC. HAS NO OBLIGATIONS TO PROVIDE 20 | MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | EXTENSION = set_user 2 | EXTVERSION = $(shell grep default_version $(EXTENSION).control | \ 3 | sed -e "s/default_version[[:space:]]*=[[:space:]]*'\([^']*\)'/\1/") 4 | LDFLAGS_SL += $(filter -lm, $(LIBS)) 5 | MODULES = src/set_user 6 | PG_CONFIG = pg_config 7 | PGFILEDESC = "set_user - similar to SET ROLE but with added logging" 8 | REGRESS = set_user 9 | 10 | all: extension/$(EXTENSION)--$(EXTVERSION).sql 11 | 12 | extension/$(EXTENSION)--$(EXTVERSION).sql: extension/set_user.sql 13 | cat $^ > $@ 14 | 15 | DATA = $(wildcard updates/*--*.sql) extension/$(EXTENSION)--$(EXTVERSION).sql 16 | EXTRA_CLEAN = extension/$(EXTENSION)--$(EXTVERSION).sql 17 | 18 | ifdef NO_PGXS 19 | subdir = contrib/set_user 20 | top_builddir = ../.. 21 | include $(top_builddir)/src/Makefile.global 22 | include $(top_srcdir)/contrib/contrib-global.mk 23 | else 24 | PGXS := $(shell $(PG_CONFIG) --pgxs) 25 | include $(PGXS) 26 | endif 27 | 28 | .PHONY: install-headers uninstall-headers 29 | 30 | install: install-headers 31 | 32 | install-headers: 33 | $(MKDIR_P) "$(DESTDIR)$(includedir)" 34 | $(INSTALL_DATA) "src/set_user.h" "$(DESTDIR)$(includedir)" 35 | 36 | uninstall: uninstall-headers 37 | 38 | uninstall-headers: 39 | rm "$(DESTDIR)$(includedir)/set_user.h" 40 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PostgreSQL set_user Extension Module 2 | 3 | ## Syntax 4 | 5 | ``` 6 | set_user(text rolename) returns text 7 | set_user(text rolename, text token) returns text 8 | set_user_u(text rolename) returns text 9 | reset_user() returns text 10 | reset_user(text token) returns text 11 | set_session_auth(text rolename) returns text 12 | ``` 13 | 14 | ## Inputs 15 | 16 | `rolename` is the role to be transitioned to. 17 | `token` if provided during set_user is saved, and then required to be provided 18 | again for reset. 19 | 20 | ## Configuration Options 21 | 22 | * Add `set_user` to `shared_preload_libraries` in postgresql.conf. 23 | 24 | * Optionally, the following custom parameters may be set to control their 25 | respective commands: 26 | * set_user.block_alter_system = off (defaults to "on") 27 | * set_user.block_copy_program = off (defaults to "on") 28 | * set_user.block_log_statement = off (defaults to "on") 29 | * set_user.superuser_allowlist = `''` 30 | * `` can contain any of the following: 31 | * list of user roles (i.e. `, ,...,`) 32 | * Group roles may be indicated by `+` 33 | * The wildcard character `*` 34 | * set_user.nosuperuser_target_allowlist = `''` 35 | * `` can contain any of the following: 36 | * list of user roles (i.e. `, ,...,`) 37 | * Group roles may be indicated by `+` 38 | * The wildcard character `*` 39 | * set_user.exit_on_error = off (defaults to "on") 40 | * To make use of the optional `set_user` and `reset_user` hooks, please refer to 41 | the [hooks](#post-execution-hooks) section. 42 | 43 | ## Description 44 | 45 | This PostgreSQL extension allows switching users and optional privilege 46 | escalation with enhanced logging and control. It provides an additional layer of 47 | logging and control when unprivileged users must escalate themselves to 48 | superuser or object owner roles in order to perform needed maintenance tasks. 49 | Specifically, when an allowed user executes `set_user(text)` or 50 | `set_user_u(text)`, several actions occur: 51 | 52 | * The current effective user becomes `rolename`. 53 | * The role transition is logged, with a specific notation if `rolename` is a 54 | superuser. 55 | * `log_statement` setting is set to "all", meaning every SQL statement executed 56 | while in this state will also get logged. 57 | * If `set_user.block_alter_system` is set to "on", `ALTER SYSTEM` commands will 58 | be blocked. 59 | * If `set_user.block_copy_program` is set to "on", `COPY PROGRAM` commands will 60 | be blocked. 61 | * If `set_user.block_log_statement` is set to "on", `SET log_statement` and 62 | variations will be blocked. 63 | * If `set_user.block_log_statement` is set to "on" and `rolename` is a database 64 | superuser, the current `log_statement` setting is changed to "all", meaning 65 | every SQL statement executed 66 | * If `set_user.superuser_audit_tag` is set, the string value will be appended to 67 | `log_line_prefix` upon superuser escalation. All logs after superuser 68 | escalation will be tagged with the value of `set_user.superuser_audit_tag`. 69 | This value defaults to `'AUDIT'`. 70 | * If `set_user.exit_on_error` is set to "on", the backend process will exit on 71 | ERROR during calls to set_session_auth(). 72 | * [Post-execution hook](#post_set_user_hook) for `set_user` is called if it is 73 | set. 74 | 75 | Only users with `EXECUTE` permission on `set_user_u(text)` may escalate to 76 | superuser. Additionally, all rules in [Superuser 77 | Allowlist](#set_usersuperuser_allowlist-rules-and-logic) 78 | apply to `set_user.superuser_allowlist` and `set_user_u(text)`. 79 | 80 | Postgres roles calling `set_user(text)` can only transition to roles listed or 81 | included in `set_user.nosuperuser_target_allowlist` (defaults to all roles). 82 | Additionally the logic in [Nosuperuser 83 | Allowlist](#set_usernosuperuser_target_allowlist-rules-and-logic) applies to 84 | `current_user` when `set_user()` is invoked. 85 | 86 | Additionally, with `set_user('rolename','token')` the `token` is stored for the 87 | lifetime of the session. 88 | 89 | When finished with required actions as `rolename`, the `reset_user()` function 90 | is executed to restore the original user. At that point, these actions occur: 91 | 92 | * Role transition is logged. 93 | * `log_statement` setting is set to its original value. 94 | * Blocked command behaviors return to normal. 95 | * [Post-execution hook](#post_reset_user_hook) for `reset_user` is called if it 96 | is set. 97 | 98 | If `set_user`, was provided with a `token`, then `reset_user('token')` must be 99 | called instead of `reset_user()`: 100 | 101 | * The provided `token` is compared with the stored token. 102 | * If the tokens do not match, or if a `token` was provided to `set_user` but not 103 | `reset_user`, an ERROR occurs. 104 | 105 | When set_session_auth(text) is called, the effective session and current user is 106 | switched to the rolename supplied, irrevocably. Unlike set_user() or set_user_u(), 107 | it does not affect logging nor allowed statements. If `set_user.exit_on_error` is 108 | "on" (the default), and any error occurs during execution, a FATAL error is thrown 109 | and the backend session exits. 110 | 111 | ### `set_user` Usage 112 | 113 | Typical use of the `set_user` extension is as follows: 114 | 115 | #### `GRANT EXECUTE` to Functions 116 | 117 | In order to make use of the `set_user` functions, some database roles must be 118 | able to execute the functions. Allow these privileges by `GRANT`ing `EXECUTE` on 119 | the appropriate functions to their intended users. 120 | 121 | ```sql 122 | GRANT EXECUTE ON FUNCTION set_user(text) TO dbclient,dbclient2; 123 | GRANT EXECUTE ON FUNCTION set_user(text, text) to dbclient,dbclient2; 124 | GRANT EXECUTE ON FUNCTION set_user_u(text) TO dbadmin; 125 | ``` 126 | 127 | This example assumes that there are three users of `set_user`: 128 | 129 | 1) `dbclient` is an unprivileged user that can run as `dbclient2` through calls 130 | to `set_user`. 131 | 2) `dbclient2` is an unprivileged user that can run as `dbclient` through calls 132 | to `set_user`. 133 | 3) `dbadmin` is the privileged (non-superuser) role, which is able to escalate 134 | privileges to superuser with Enhanced Logging. 135 | 136 | #### Call `set_user` to Transition 137 | 138 | Transitioning to other roles through use of `set_user` provides the ability to 139 | change the session's `current_user`. 140 | 141 | Transitions can be made to unprivileged users through use of `set_user` (with 142 | optional `token`, as described above). 143 | 144 | ```sql 145 | SELECT set_user('dbclient2'); 146 | ``` 147 | 148 | Alternatively, transitions can be made to superusers through use of 149 | `set_user_u`: 150 | 151 | ```sql 152 | SELECT set_user_u('postgres'); 153 | ``` 154 | 155 | **Note:** See rules in [Superuser 156 | Allowlist](#set_usersuperuser_allowlist-rules-and-logic) 157 | for logic around calling `set_user_u(text)`. See [Nosuperuser 158 | Allowlist](#set_usernosuperuser_target_allowlist-rules-and-logic) for reference 159 | logic around calling `set_user(text)`. 160 | 161 | Once one or more unprivileged users are able to run `set_user_u()` in order to 162 | escalate their privileges, the superuser account (typically `postgres`) can be 163 | altered to `NOLOGIN`, preventing any direct database connection by a superuser 164 | which would bypass the enhanced logging. 165 | 166 | Naturally for this to work as expected, the PostgreSQL cluster must be audited 167 | to ensure there are no other PostgreSQL roles existing which are both superuser 168 | and can log in. Additionally there must be no unprivileged PostgreSQL roles 169 | which have been granted access to one of the existing superuser roles. 170 | 171 | #### `set_user.superuser_allowlist` Rules and Logic 172 | 173 | The following rules govern escalation to superuser via the `set_user_u(text)` 174 | function: 175 | 176 | * `current_user` must be `GRANT`ed `EXECUTE ON FUNCTION set_user_u(text)` OR 177 | `current_user` must be the `OWNER` of the `set_user_u(text)` function OR 178 | `current_user` must be a superuser. 179 | * `current_user` must be listed in `set_user.superuser_allowlist` OR 180 | `current_user` must belong to a group that is listed in 181 | `set_user.superuser_allowlist` (e.g. `'+admin'`) 182 | * If `set_user.superuser_allowlist` is the empty set , `''`, superuser 183 | escalation is blocked for all users. 184 | * If `set_user.superuser_allowlist` is the wildcard character, `'*'`, all users 185 | with `EXECUTE` permission on `set_user_u(text)` can escalate to superuser. 186 | * If `set_user.superuser_allowlist` is not specified, the value defaults to the 187 | wildcard character, `'*'`. 188 | 189 | #### `set_user.nosuperuser_target_allowlist` Rules and Logic 190 | 191 | The following rules govern non-superuser role transitions through use of 192 | `set_user(text)` or `set_user(text, text)` function (for simplicity, only 193 | `set_user(text)` is used): 194 | 195 | * `current_user` must be `GRANT`ed `EXECUTE ON FUNCTION set_user(text)` OR 196 | `current_user` must be the `OWNER` of the `set_user(text)` function OR 197 | `current_user` must be a superuser. 198 | * The target rolename must be listed in `set_user.nosuperuser_target_allowlist` 199 | OR the target rolename must belong to a group that is listed in 200 | `set_user.nosuperuser_target_allowlist` (e.g. `'+client'`) 201 | * If `set_user.nosuperuser_target_allowlist` is the empty set , `''`, 202 | `set_user(text)` transitions to non-superusers are blocked for all users. 203 | * If `set_user.nosuperuser_target_allowlist` is the wildcard character, `'*'`, 204 | all users with `EXECUTE` permission on `set_user(text)` can transition to any 205 | other non-superuser role. 206 | * If `set_user.nosuperuser_target_allowlist` is not specified, the value 207 | defaults to the wildcard character, `'*'`. 208 | 209 | #### Perform Actions With Enhanced Logging 210 | 211 | Once a transition has been made, the current session behaves as if it has the 212 | privileges of the new `current_user`. The optional enhanced logging creates an 213 | audit trail upon transition to an alternate role, ensuring that any privilege 214 | escalation/alteration does not go unmonitored. 215 | 216 | This audit trail is tagged with the value of `set_user.superuser_audit_tag`, 217 | such that actions after superuser escalation are easily identifiable. 218 | 219 | #### Reset to Previous User 220 | 221 | ```sql 222 | SELECT reset_user(); 223 | ``` 224 | 225 | If `set_user()` was initially called with a `token`, the same `token` must be 226 | provided in order to reset back to the previous user. 227 | 228 | ```sql 229 | SELECT set_user('dbclient2', 'some_token_string'); 230 | SELECT reset_user('some_token_string'); 231 | ``` 232 | 233 | ### Blocking `ALTER SYSTEM` and `COPY PROGRAM` 234 | 235 | Note that for the blocking of `ALTER SYSTEM` and `COPY PROGRAM` to work 236 | properly, you must include `set_user` in `shared_preload_libraries` in 237 | `postgresql.conf` and restart PostgreSQL. 238 | 239 | 240 | Notes: 241 | 242 | If set_user.block_log_statement is set to "off", the `log_statement` setting is 243 | left unchanged. 244 | 245 | For the blocking of `ALTER SYSTEM` and `COPY PROGRAM` to work properly, you must 246 | include `set_user` in shared_preload_libraries in postgresql.conf and restart 247 | PostgreSQL. 248 | 249 | Neither `set_user(text)` nor `set_user_u(text)` may be executed from 250 | within an explicit transaction block. 251 | 252 | ### `set_session_auth` Usage 253 | 254 | Typical use of the `set_session_auth` function is as follows: 255 | 256 | #### `GRANT EXECUTE` to Functions 257 | 258 | In order to make use of the `set_session_auth` function, some database roles must be 259 | able to execute the function. Allow these privileges by `GRANT`ing `EXECUTE` on 260 | the function to their intended users. 261 | 262 | ```sql 263 | GRANT EXECUTE ON FUNCTION set_session_auth(text) TO dbclient,dbclient2; 264 | ``` 265 | 266 | ## Caveats 267 | 268 | In its current state, this extension cannot prevent `rolename` from performing a 269 | variety of nefarious or otherwise undesireable actions. However, these actions 270 | will be logged providing an audit trail, which could also be used to trigger 271 | alerts. 272 | 273 | This extension supports PostgreSQL versions 12 and higher. Prior versions of 274 | PostgreSQL are supported by prior versions of set_user. 275 | 276 | ## Post-Execution Hooks 277 | 278 | `set_user` exposes two hooks that may be used to control post-execution behavior 279 | for `set_user` and `reset_user`. 280 | 281 | ### Description 282 | 283 | The following hooks are called (if set) directly before returning from 284 | successful calls to `set_user` and `reset_user`. These hooks are meant to give 285 | other extensions awareness of `set_user` actions. This is helpful, for instance, 286 | to keep track of dynamic user switching within a session. 287 | 288 | To avoid order-dependency in `shared_preload_libraries`, these hooks are 289 | registered in the rendezvous hash table of core Postgres. The header defines a 290 | [utility function](set_user.h#L13) for doing all of the necessary setup. 291 | 292 | ###### `post_set_user` hook 293 | 294 | Allows another extension to take action after calls to `set_user`. This hook 295 | takes the username as an argument so that the hook implementation is aware of 296 | the username. 297 | 298 | ###### `post_reset_user` hook 299 | 300 | Allows another extension to take action after calls to `reset_user`. This hook 301 | does not take any arguments, since the resulting username will always be the 302 | `session_user`. 303 | 304 | ### Configuration 305 | 306 | Follow the instructions below to implement `set_user` and `reset_user` 307 | post-execution hooks in another extension: 308 | 309 | * Add '-I$(includedir)' to `CPPFLAGS` of the extension which implements the 310 | post-execution hooks. 311 | * `#include set_user.h` in whichever file implements the hooks. 312 | * Register hook implementations in `rendezvous_variable` hash using the 313 | `register_set_user_hooks` utility function. 314 | 315 | Configuration is described in more detail in the [post-execution 316 | hooks](#install-set_user-post-execution-hooks) subsection of the Install 317 | documentation. 318 | 319 | ### Caveats 320 | 321 | If another extension implements the post-execution hooks, `post_set_user_hook` 322 | and `post_reset_user_hook`, `set_user` must be listed before that extension in 323 | `shared_preload_libraries`. This is due to the way `shared_preload_libraries` 324 | are opened and loaded into memory by Postgres: the hooks need to be loaded into 325 | memory before their implementations can access them. 326 | 327 | ## Installation 328 | 329 | ### Requirements 330 | 331 | * PostgreSQL 13 or higher. 332 | 333 | ### Compile and Install 334 | 335 | Clone PostgreSQL repository: 336 | 337 | ```bash 338 | $> git clone https://github.com/postgres/postgres.git 339 | ``` 340 | 341 | Checkout REL_15_STABLE (for example) branch: 342 | 343 | ```bash 344 | $> git checkout REL_15_STABLE 345 | ``` 346 | 347 | Make PostgreSQL: 348 | 349 | ```bash 350 | $> ./configure 351 | $> make install -s 352 | ``` 353 | 354 | Change to the contrib directory: 355 | 356 | ```bash 357 | $> cd contrib 358 | ``` 359 | 360 | Clone `set_user` extension: 361 | 362 | ```bash 363 | $> git clone https://github.com/pgaudit/set_user 364 | ``` 365 | 366 | Change to `set_user` directory: 367 | 368 | ```bash 369 | $> cd set_user 370 | ``` 371 | 372 | Build `set_user`: 373 | 374 | ```bash 375 | $> make 376 | ``` 377 | 378 | Install `set_user`: 379 | 380 | ```bash 381 | $> make install 382 | ``` 383 | 384 | #### Using PGXS 385 | 386 | If an instance of PostgreSQL is already installed, then PGXS can be utilized to 387 | build and install `set_user`. Ensure that PostgreSQL binaries are available via 388 | the `$PATH` environment variable then use the following commands. 389 | 390 | ```bash 391 | $> make USE_PGXS=1 392 | $> make USE_PGXS=1 install 393 | ``` 394 | 395 | ### Configure 396 | 397 | The following bash commands should configure your system to utilize `set_user`. 398 | Replace all paths as appropriate. It may be prudent to visually inspect the 399 | files afterward to ensure the changes took place. 400 | 401 | ###### Initialize PostgreSQL (if needed): 402 | 403 | ```bash 404 | $> initdb -D /path/to/data/directory 405 | ``` 406 | 407 | ###### Create Target Database (if needed): 408 | 409 | ```bash 410 | $> createdb 411 | ``` 412 | 413 | ###### Install `set_user` functions: 414 | 415 | Edit postgresql.conf and add `set_user` to the `shared_preload_libraries` line, 416 | optionally also changing custom settings as mentioned above. 417 | 418 | First edit postgresql.conf in your favorite editor: 419 | 420 | ``` 421 | $> vi $PGDATA/postgresql.conf 422 | ``` 423 | 424 | Then add these lines to the end of the file: 425 | ``` 426 | # Add set_user to any existing list 427 | shared_preload_libraries = 'set_user' 428 | # The following lines are only required to modify the 429 | # blocking of each respective command if desired 430 | set_user.block_alter_system = off #defaults to "on" 431 | set_user.block_copy_program = off #defaults to "on" 432 | set_user.block_log_statement = off #defaults to "on" 433 | set_user.superuser_allowlist = '' #defaults to '*' 434 | set_user.nosuperuser_target_allowlist = '' #defaults to '*' 435 | ``` 436 | 437 | Finally, restart PostgreSQL (method may vary): 438 | 439 | ``` 440 | $> service postgresql restart 441 | ``` 442 | 443 | Install the extension into your database: 444 | 445 | ```bash 446 | psql 447 | CREATE EXTENSION set_user; 448 | ``` 449 | 450 | ###### Install `set_user` post-execution hooks: 451 | 452 | Ensure that `set_user.h` is copied to `$(includedir)`. 453 | 454 | This can be done automatically upon normal installation: 455 | 456 | ```bash 457 | $> make USE_PGXS=1 install 458 | ``` 459 | 460 | There is also an explicit make target available to copy the header file to the 461 | appropriate directory: 462 | 463 | ```bash 464 | $> make USE_PGXS=1 install-headers 465 | ``` 466 | 467 | Ensure that the implementing extension adds `-I$(includedir)` to `CPPFLAGS` in 468 | its Makefile: 469 | 470 | ``` 471 | # Add -I$(includedir) to CPPFLAGS so the set_user header is included 472 | override CPPFLAGS += -I$(includedir) 473 | ``` 474 | 475 | Ensure that the implementing extension includes the `set_user` header file in 476 | the appropriate C file: 477 | 478 | ```c 479 | /* Include set_user hooks in whichever C file implements the hooks */ 480 | #include "set_user.h" 481 | 482 | ``` 483 | Create your `set_user` hooks and register them in the rendezvous_variable hash: 484 | 485 | ```c 486 | void _PG_Init(void) 487 | { 488 | /* 489 | * Your _PG_Init code here 490 | */ 491 | 492 | register_set_user_hooks(extension_post_set_user, extension_post_reset_user); 493 | 494 | /* 495 | * more _PG_Init code 496 | */ 497 | } 498 | 499 | /* 500 | * extension_post_set_user 501 | * 502 | * Entrypoint of the set_user post-exec hook. 503 | */ 504 | static void 505 | extension_post_set_user(void) 506 | { 507 | /* Some magic */ 508 | } 509 | 510 | /* 511 | * extension_post_reset_user 512 | * 513 | * Entrypoint of the reset_user post-exec hook. 514 | */ 515 | static void 516 | extension_post_reset_user(void) 517 | { 518 | /* Some magic */ 519 | } 520 | 521 | ``` 522 | 523 | ## GUC Parameters 524 | 525 | * Block `ALTER SYSTEM` commands 526 | * `set_user.block_alter_system = on` 527 | * Block `COPY PROGRAM` commands 528 | * `set_user.block_copy_program = on` 529 | * Block `SET log_statement` commands 530 | * `set_user.block_log_statement = on` 531 | * Allow list of roles to escalate to superuser 532 | * `set_user.superuser_allowlist = ',,...,'` 533 | * Allowed list of roles that can be switched to (not used in set_user_u) 534 | * `set_user.nosuperuser_target_allowlist = ',,...,'` 535 | 536 | 537 | ## Examples 538 | 539 | set_user() and related: 540 | ``` 541 | ################################# 542 | # OS command line, terminal 1 543 | ################################# 544 | psql -U postgres 545 | 546 | --------------------------------- 547 | -- psql command line, terminal 1 548 | --------------------------------- 549 | SELECT rolname FROM pg_authid WHERE rolsuper and rolcanlogin; 550 | rolname 551 | ---------- 552 | postgres 553 | (1 row) 554 | 555 | CREATE EXTENSION set_user; 556 | CREATE USER dba_user; 557 | GRANT EXECUTE ON FUNCTION set_user(text) TO dba_user; 558 | GRANT EXECUTE ON FUNCTION set_user_u(text) TO dba_user; 559 | 560 | ################################# 561 | # OS command line, terminal 2 562 | ################################# 563 | psql -U dba_user 564 | 565 | --------------------------------- 566 | -- psql command line, terminal 2 567 | --------------------------------- 568 | SELECT set_user('postgres'); 569 | ERROR: Switching to superuser only allowed for privileged procedure: 570 | 'set_user_u' 571 | SELECT set_user_u('postgres'); 572 | SELECT CURRENT_USER, SESSION_USER; 573 | current_user | session_user 574 | --------------+-------------- 575 | postgres | dba_user 576 | (1 row) 577 | 578 | SELECT reset_user(); 579 | SELECT CURRENT_USER, SESSION_USER; 580 | current_user | session_user 581 | --------------+-------------- 582 | dba_user | dba_user 583 | (1 row) 584 | 585 | \q 586 | 587 | --------------------------------- 588 | -- psql command line, terminal 1 589 | --------------------------------- 590 | ALTER USER postgres NOLOGIN; 591 | -- repeat terminal 2 test with dba_user before exiting 592 | \q 593 | 594 | ################################# 595 | # OS command line, terminal 1 596 | ################################# 597 | tail -n 6 598 | LOG: Role dba_user transitioning to Superuser Role postgres 599 | STATEMENT: SELECT set_user_u('postgres'); 600 | LOG: statement: SELECT CURRENT_USER, SESSION_USER; 601 | LOG: statement: SELECT reset_user(); 602 | LOG: Superuser Role postgres transitioning to Role dba_user 603 | STATEMENT: SELECT reset_user(); 604 | 605 | ################################# 606 | # OS command line, terminal 2 607 | ################################# 608 | psql -U dba_user 609 | 610 | --------------------------------- 611 | -- psql command line, terminal 2 612 | --------------------------------- 613 | -- Verify there are no superusers that can login directly 614 | SELECT rolname FROM pg_authid WHERE rolsuper and rolcanlogin; 615 | rolname 616 | --------- 617 | (0 rows) 618 | 619 | -- Verify there are no unprivileged roles that can login directly 620 | -- that are granted a superuser role even if it is multiple layers 621 | -- removed 622 | DROP VIEW IF EXISTS roletree; 623 | CREATE OR REPLACE VIEW roletree AS 624 | WITH RECURSIVE 625 | roltree AS ( 626 | SELECT u.rolname AS rolname, 627 | u.oid AS roloid, 628 | u.rolcanlogin, 629 | u.rolsuper, 630 | '{}'::name[] AS rolparents, 631 | NULL::oid AS parent_roloid, 632 | NULL::name AS parent_rolname 633 | FROM pg_catalog.pg_authid u 634 | LEFT JOIN pg_catalog.pg_auth_members m on u.oid = m.member 635 | LEFT JOIN pg_catalog.pg_authid g on m.roleid = g.oid 636 | WHERE g.oid IS NULL 637 | UNION ALL 638 | SELECT u.rolname AS rolname, 639 | u.oid AS roloid, 640 | u.rolcanlogin, 641 | u.rolsuper, 642 | t.rolparents || g.rolname AS rolparents, 643 | g.oid AS parent_roloid, 644 | g.rolname AS parent_rolname 645 | FROM pg_catalog.pg_authid u 646 | JOIN pg_catalog.pg_auth_members m on u.oid = m.member 647 | JOIN pg_catalog.pg_authid g on m.roleid = g.oid 648 | JOIN roltree t on t.roloid = g.oid 649 | ) 650 | SELECT 651 | r.rolname, 652 | r.roloid, 653 | r.rolcanlogin, 654 | r.rolsuper, 655 | r.rolparents 656 | FROM roltree r 657 | ORDER BY 1; 658 | 659 | -- For example purposes, given this set of roles 660 | SELECT r.rolname, r.rolsuper, r.rolinherit, 661 | r.rolcreaterole, r.rolcreatedb, r.rolcanlogin, 662 | r.rolconnlimit, r.rolvaliduntil, 663 | ARRAY(SELECT b.rolname 664 | FROM pg_catalog.pg_auth_members m 665 | JOIN pg_catalog.pg_roles b ON (m.roleid = b.oid) 666 | WHERE m.member = r.oid) as memberof 667 | , r.rolreplication 668 | , r.rolbypassrls 669 | FROM pg_catalog.pg_roles r 670 | ORDER BY 1; 671 | List of roles 672 | Role name | Attributes | Member of 673 | -----------+------------------------------------------------------------+------------ 674 | bob | | {} 675 | dba_user | | {su} 676 | joe | | {newbs} 677 | newbs | Cannot login | {} 678 | postgres | Superuser, Create role, Create DB, Replication, Bypass RLS | {} 679 | su | No inheritance, Cannot login | {postgres} 680 | 681 | -- This query shows current status is not acceptable 682 | -- 1) postgres can login directly 683 | -- 2) dba_user can login and is able to escalate without using set_user() 684 | SELECT 685 | ro.rolname, 686 | ro.roloid, 687 | ro.rolcanlogin, 688 | ro.rolsuper, 689 | ro.rolparents 690 | FROM roletree ro 691 | WHERE (ro.rolcanlogin AND ro.rolsuper) 692 | OR 693 | ( 694 | ro.rolcanlogin AND EXISTS 695 | ( 696 | SELECT TRUE FROM roletree ri 697 | WHERE ri.rolname = ANY (ro.rolparents) 698 | AND ri.rolsuper 699 | ) 700 | ); 701 | rolname | roloid | rolcanlogin | rolsuper | rolparents 702 | ----------+--------+-------------+----------+--------------- 703 | dba_user | 16387 | t | f | {postgres,su} 704 | postgres | 10 | t | t | {} 705 | (2 rows) 706 | 707 | -- Fix it 708 | REVOKE postgres FROM su; 709 | ALTER USER postgres NOLOGIN; 710 | 711 | -- Rerun the query - shows current status is acceptable 712 | SELECT 713 | ro.rolname, 714 | ro.roloid, 715 | ro.rolcanlogin, 716 | ro.rolsuper, 717 | ro.rolparents 718 | FROM roletree ro 719 | WHERE (ro.rolcanlogin AND ro.rolsuper) 720 | OR 721 | ( 722 | ro.rolcanlogin AND EXISTS 723 | ( 724 | SELECT TRUE FROM roletree ri 725 | WHERE ri.rolname = ANY (ro.rolparents) 726 | AND ri.rolsuper 727 | ) 728 | ); 729 | rolname | roloid | rolcanlogin | rolsuper | rolparents 730 | ---------+--------+-------------+----------+------------ 731 | (0 rows) 732 | ``` 733 | 734 | set_session_auth(): 735 | ``` 736 | # psql -U postgres test 737 | psql (15.4) 738 | Type "help" for help. 739 | 740 | test=# grant EXECUTE on FUNCTION set_session_auth(text) to dbclient; 741 | \q 742 | 743 | # psql -U dbclient test 744 | psql (15.4) 745 | Type "help" for help. 746 | 747 | test=> select session_user, current_user, user, current_role; 748 | session_user | current_user | user | current_role 749 | --------------+--------------+----------+-------------- 750 | dbclient | dbclient | dbclient | dbclient 751 | (1 row) 752 | 753 | test=> select set_session_auth('jeff'); 754 | set_session_auth 755 | ------------------ 756 | OK 757 | (1 row) 758 | 759 | test=> select session_user, current_user, user, current_role; 760 | session_user | current_user | user | current_role 761 | --------------+--------------+------+-------------- 762 | jeff | jeff | jeff | jeff 763 | (1 row) 764 | 765 | test=> -- the role switch is irrevocable 766 | test=> reset role; 767 | RESET 768 | test=> select session_user, current_user, user, current_role; 769 | session_user | current_user | user | current_role 770 | --------------+--------------+------+-------------- 771 | jeff | jeff | jeff | jeff 772 | (1 row) 773 | 774 | test=> reset session authorization; 775 | RESET 776 | test=> select session_user, current_user, user, current_role; 777 | session_user | current_user | user | current_role 778 | --------------+--------------+------+-------------- 779 | jeff | jeff | jeff | jeff 780 | (1 row) 781 | 782 | test=> set role none; 783 | SET 784 | test=> select session_user, current_user, user, current_role; 785 | session_user | current_user | user | current_role 786 | --------------+--------------+------+-------------- 787 | jeff | jeff | jeff | jeff 788 | (1 row) 789 | ``` 790 | 791 | ## Licensing 792 | 793 | Please see the [LICENSE](./LICENSE) file. 794 | -------------------------------------------------------------------------------- /docbuilder/.gitignore: -------------------------------------------------------------------------------- 1 | .vagrant 2 | -------------------------------------------------------------------------------- /docbuilder/README.txt: -------------------------------------------------------------------------------- 1 | Note: Vagrant Guest Additions is required 2 | You may acquire it on your host system via: "sudo vagrant plugin install vagrant-vbguest" 3 | 4 | To build the docs, do the following: 5 | 1) vagrant up 6 | 2) vagrant destroy 7 | -------------------------------------------------------------------------------- /docbuilder/Vagrantfile: -------------------------------------------------------------------------------- 1 | # Note: Vagrant Guest Additions is required 2 | # You may acquire it on your host system via: "sudo vagrant plugin install vagrant-vbguest" 3 | 4 | Vagrant.configure(2) do |config| 5 | config.vm.box = "centos/7" 6 | 7 | config.vm.provider :virtualbox do |vb| 8 | vb.name = "set-user-centos7-test" 9 | end 10 | 11 | # Provision the VM 12 | config.vm.provision "shell", inline: <<-SHELL 13 | echo "Provisioning..." 14 | 15 | # Setup environment 16 | yum -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm 17 | yum -y install pandoc 18 | yum -y install 'texlive-*' 19 | 20 | # Generate docs 21 | cd /set-user 22 | version=$(grep "default_version" set_user.control | awk '{print $3}' | sed "s/'//g") 23 | pandoc -s README.md -o Set_User-UserGuide-$version.pdf 24 | SHELL 25 | 26 | # Don't share the default vagrant folder 27 | config.vm.synced_folder ".", "/vagrant", disabled: true 28 | 29 | # Mount project path for testing 30 | config.vm.synced_folder "..", "/set-user" 31 | end 32 | -------------------------------------------------------------------------------- /expected/set_user.out: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION set_user; 2 | -- Ensure the library is loaded. 3 | LOAD 'set_user'; 4 | -- Clean up in case a prior regression run failed 5 | -- First suppress NOTICE messages when users/groups don't exist 6 | SET client_min_messages TO 'warning'; 7 | DROP USER IF EXISTS dba, bob, joe, newbs, su; 8 | RESET client_min_messages; 9 | -- Create some users to work with 10 | CREATE USER dba; 11 | CREATE USER bob; 12 | CREATE USER joe; 13 | CREATE ROLE newbs; 14 | CREATE ROLE su NOINHERIT; 15 | -- dba is the role we want to allow to execute set_user() 16 | GRANT EXECUTE ON FUNCTION set_user(text) TO dba; 17 | GRANT EXECUTE ON FUNCTION set_user(text,text) TO dba; 18 | GRANT EXECUTE ON FUNCTION set_user_u(text) TO dba; 19 | GRANT newbs TO bob; 20 | -- joe will be able to escalate without set_user() via su 21 | GRANT su TO joe; 22 | GRANT postgres TO su; 23 | -- test reset_user with no initial set 24 | SELECT reset_user(); 25 | reset_user 26 | ------------ 27 | OK 28 | (1 row) 29 | 30 | -- test set_user 31 | SET SESSION AUTHORIZATION dba; 32 | SELECT SESSION_USER, CURRENT_USER; 33 | session_user | current_user 34 | --------------+-------------- 35 | dba | dba 36 | (1 row) 37 | 38 | SELECT set_user('postgres'); 39 | ERROR: switching to superuser not allowed 40 | HINT: Use 'set_user_u' to escalate. 41 | SELECT SESSION_USER, CURRENT_USER; 42 | session_user | current_user 43 | --------------+-------------- 44 | dba | dba 45 | (1 row) 46 | 47 | -- test set_user_u 48 | SET SESSION AUTHORIZATION dba; 49 | SELECT SESSION_USER, CURRENT_USER; 50 | session_user | current_user 51 | --------------+-------------- 52 | dba | dba 53 | (1 row) 54 | 55 | SELECT set_user_u('postgres'); 56 | set_user_u 57 | ------------ 58 | OK 59 | (1 row) 60 | 61 | SELECT SESSION_USER, CURRENT_USER; 62 | session_user | current_user 63 | --------------+-------------- 64 | dba | postgres 65 | (1 row) 66 | 67 | -- test multiple successive set_user calls 68 | SELECT set_user('joe'); -- fail 69 | ERROR: must reset previous user prior to setting again 70 | -- ALTER SYSTEM should fail 71 | ALTER SYSTEM SET wal_level = minimal; 72 | ERROR: ALTER SYSTEM blocked by set_user config 73 | -- COPY PROGRAM should fail 74 | COPY (select 42) TO PROGRAM 'cat'; 75 | ERROR: COPY PROGRAM blocked by set_user config 76 | -- SET log_statement should fail 77 | SET log_statement = 'none'; 78 | ERROR: "SET log_statement" blocked by set_user config 79 | SET log_statement = DEFAULT; 80 | ERROR: "SET log_statement" blocked by set_user config 81 | RESET log_statement; 82 | ERROR: "SET log_statement" blocked by set_user config 83 | BEGIN; SET LOCAL log_statement = 'none'; ABORT; 84 | ERROR: "SET log_statement" blocked by set_user config 85 | -- set_config() should fail 86 | SELECT set_config('wal_level', 'minimal', false); 87 | ERROR: "pg_catalog.set_config(pg_catalog.text,pg_catalog.text,boolean)" blocked by set_user 88 | HINT: Use "SET" syntax instead. 89 | CREATE OR REPLACE FUNCTION backdoor(text, text, boolean) RETURNS BOOL AS 'set_config_by_name' LANGUAGE INTERNAL; 90 | SELECT backdoor('log_statement', 'none', true); 91 | ERROR: "public.backdoor(pg_catalog.text,pg_catalog.text,boolean)" blocked by set_user 92 | HINT: Use "SET" syntax instead. 93 | UPDATE pg_settings SET setting = 'none' WHERE name = 'log_statement'; 94 | ERROR: "pg_catalog.set_config(pg_catalog.text,pg_catalog.text,boolean)" blocked by set_user 95 | HINT: Use "SET" syntax instead. 96 | -- test reset_user 97 | RESET ROLE; -- should fail 98 | ERROR: "SET/RESET ROLE" blocked by set_user 99 | HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. 100 | RESET SESSION AUTHORIZATION; -- should fail 101 | ERROR: "SET/RESET SESSION AUTHORIZATION" blocked by set_user 102 | HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. 103 | SELECT SESSION_USER, CURRENT_USER; 104 | session_user | current_user 105 | --------------+-------------- 106 | dba | postgres 107 | (1 row) 108 | 109 | SELECT reset_user(); -- succeed 110 | reset_user 111 | ------------ 112 | OK 113 | (1 row) 114 | 115 | -- test set_user and reset_user with token 116 | SELECT SESSION_USER, CURRENT_USER; 117 | session_user | current_user 118 | --------------+-------------- 119 | dba | dba 120 | (1 row) 121 | 122 | SELECT set_user('bob', 'secret'); 123 | set_user 124 | ---------- 125 | OK 126 | (1 row) 127 | 128 | SELECT SESSION_USER, CURRENT_USER; 129 | session_user | current_user 130 | --------------+-------------- 131 | dba | bob 132 | (1 row) 133 | 134 | RESET ROLE; -- should fail 135 | ERROR: "SET/RESET ROLE" blocked by set_user 136 | HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. 137 | RESET SESSION AUTHORIZATION; -- should fail 138 | ERROR: "SET/RESET SESSION AUTHORIZATION" blocked by set_user 139 | HINT: Use "SELECT set_user();" or "SELECT reset_user();" instead. 140 | SELECT SESSION_USER, CURRENT_USER; 141 | session_user | current_user 142 | --------------+-------------- 143 | dba | bob 144 | (1 row) 145 | 146 | SELECT reset_user(); -- should fail 147 | ERROR: reset token required but not provided 148 | SELECT SESSION_USER, CURRENT_USER; 149 | session_user | current_user 150 | --------------+-------------- 151 | dba | bob 152 | (1 row) 153 | 154 | SELECT reset_user('secret'); -- succeed 155 | reset_user 156 | ------------ 157 | OK 158 | (1 row) 159 | 160 | SELECT SESSION_USER, CURRENT_USER; 161 | session_user | current_user 162 | --------------+-------------- 163 | dba | dba 164 | (1 row) 165 | 166 | RESET SESSION AUTHORIZATION; 167 | ALTER SYSTEM SET wal_level = minimal; 168 | COPY (select 42) TO PROGRAM 'cat'; 169 | SET log_statement = DEFAULT; 170 | -- test transaction handling 171 | CREATE FUNCTION bail() RETURNS bool AS $$ 172 | BEGIN 173 | RAISE EXCEPTION 'bailing out !'; 174 | END; 175 | $$ LANGUAGE plpgsql; 176 | SET SESSION AUTHORIZATION dba; 177 | SELECT SESSION_USER, CURRENT_USER; 178 | session_user | current_user 179 | --------------+-------------- 180 | dba | dba 181 | (1 row) 182 | 183 | -- bail during set_user_u 184 | SELECT set_user_u('postgres'), bail(); 185 | ERROR: bailing out ! 186 | CONTEXT: PL/pgSQL function bail() line 3 at RAISE 187 | SELECT SESSION_USER, CURRENT_USER; 188 | session_user | current_user 189 | --------------+-------------- 190 | dba | dba 191 | (1 row) 192 | 193 | SHOW log_statement; 194 | log_statement 195 | --------------- 196 | none 197 | (1 row) 198 | 199 | SHOW log_line_prefix; 200 | log_line_prefix 201 | ----------------- 202 | %m [%p] 203 | (1 row) 204 | 205 | -- bail on reset after successful set_user_u 206 | SELECT set_user_u('postgres'); 207 | set_user_u 208 | ------------ 209 | OK 210 | (1 row) 211 | 212 | SELECT SESSION_USER, CURRENT_USER; 213 | session_user | current_user 214 | --------------+-------------- 215 | dba | postgres 216 | (1 row) 217 | 218 | SHOW log_statement; 219 | log_statement 220 | --------------- 221 | all 222 | (1 row) 223 | 224 | SHOW log_line_prefix; 225 | log_line_prefix 226 | ----------------- 227 | %m [%p] AUDIT: 228 | (1 row) 229 | 230 | SELECT reset_user(), bail(); 231 | ERROR: bailing out ! 232 | CONTEXT: PL/pgSQL function bail() line 3 at RAISE 233 | SELECT SESSION_USER, CURRENT_USER; 234 | session_user | current_user 235 | --------------+-------------- 236 | dba | postgres 237 | (1 row) 238 | 239 | SHOW log_statement; 240 | log_statement 241 | --------------- 242 | all 243 | (1 row) 244 | 245 | SHOW log_line_prefix; 246 | log_line_prefix 247 | ----------------- 248 | %m [%p] AUDIT: 249 | (1 row) 250 | 251 | SELECT reset_user(); 252 | reset_user 253 | ------------ 254 | OK 255 | (1 row) 256 | 257 | -- bail during set_user 258 | SELECT set_user('bob'), bail(); 259 | ERROR: bailing out ! 260 | CONTEXT: PL/pgSQL function bail() line 3 at RAISE 261 | SELECT SESSION_USER, CURRENT_USER; 262 | session_user | current_user 263 | --------------+-------------- 264 | dba | dba 265 | (1 row) 266 | 267 | SHOW log_statement; 268 | log_statement 269 | --------------- 270 | none 271 | (1 row) 272 | 273 | SHOW log_line_prefix; 274 | log_line_prefix 275 | ----------------- 276 | %m [%p] 277 | (1 row) 278 | 279 | -- bail during set_user with token 280 | SELECT set_user('bob', 'secret'), bail(); 281 | ERROR: bailing out ! 282 | CONTEXT: PL/pgSQL function bail() line 3 at RAISE 283 | SELECT SESSION_USER, CURRENT_USER; 284 | session_user | current_user 285 | --------------+-------------- 286 | dba | dba 287 | (1 row) 288 | 289 | SHOW log_statement; 290 | log_statement 291 | --------------- 292 | none 293 | (1 row) 294 | 295 | SHOW log_line_prefix; 296 | log_line_prefix 297 | ----------------- 298 | %m [%p] 299 | (1 row) 300 | 301 | -- bail during reset_user with token 302 | SELECT set_user('bob', 'secret'); 303 | set_user 304 | ---------- 305 | OK 306 | (1 row) 307 | 308 | SELECT SESSION_USER, CURRENT_USER; 309 | session_user | current_user 310 | --------------+-------------- 311 | dba | bob 312 | (1 row) 313 | 314 | SELECT reset_user('secret'), bail(); 315 | ERROR: bailing out ! 316 | CONTEXT: PL/pgSQL function bail() line 3 at RAISE 317 | SELECT SESSION_USER, CURRENT_USER; 318 | session_user | current_user 319 | --------------+-------------- 320 | dba | bob 321 | (1 row) 322 | 323 | SELECT reset_user('secret'); 324 | reset_user 325 | ------------ 326 | OK 327 | (1 row) 328 | 329 | RESET SESSION AUTHORIZATION; 330 | -- this is an example of how we might audit existing roles 331 | SET SESSION AUTHORIZATION dba; 332 | SELECT set_user_u('postgres'); 333 | set_user_u 334 | ------------ 335 | OK 336 | (1 row) 337 | 338 | SELECT rolname FROM pg_authid WHERE rolsuper and rolcanlogin; 339 | rolname 340 | ---------- 341 | postgres 342 | (1 row) 343 | 344 | CREATE OR REPLACE VIEW roletree AS 345 | WITH RECURSIVE 346 | roltree AS ( 347 | SELECT u.rolname AS rolname, 348 | u.oid AS roloid, 349 | u.rolcanlogin, 350 | u.rolsuper, 351 | '{}'::name[] AS rolparents, 352 | NULL::oid AS parent_roloid, 353 | NULL::name AS parent_rolname 354 | FROM pg_catalog.pg_authid u 355 | LEFT JOIN pg_catalog.pg_auth_members m on u.oid = m.member 356 | LEFT JOIN pg_catalog.pg_authid g on m.roleid = g.oid 357 | WHERE g.oid IS NULL 358 | UNION ALL 359 | SELECT u.rolname AS rolname, 360 | u.oid AS roloid, 361 | u.rolcanlogin, 362 | u.rolsuper, 363 | t.rolparents || g.rolname AS rolparents, 364 | g.oid AS parent_roloid, 365 | g.rolname AS parent_rolname 366 | FROM pg_catalog.pg_authid u 367 | JOIN pg_catalog.pg_auth_members m on u.oid = m.member 368 | JOIN pg_catalog.pg_authid g on m.roleid = g.oid 369 | JOIN roltree t on t.roloid = g.oid 370 | ) 371 | SELECT 372 | r.rolname, 373 | r.roloid, 374 | r.rolcanlogin, 375 | r.rolsuper, 376 | r.rolparents 377 | FROM roltree r 378 | ORDER BY 1; 379 | -- this will show unacceptable results 380 | -- since postgres can log in directly and 381 | -- joe can escalate via su to postgres 382 | SELECT 383 | ro.rolname, 384 | ro.rolcanlogin, 385 | ro.rolsuper, 386 | ro.rolparents 387 | FROM roletree ro 388 | WHERE (ro.rolcanlogin AND ro.rolsuper) 389 | OR 390 | ( 391 | ro.rolcanlogin AND EXISTS 392 | ( 393 | SELECT TRUE FROM roletree ri 394 | WHERE ri.rolname = ANY (ro.rolparents) 395 | AND ri.rolsuper 396 | ) 397 | ); 398 | rolname | rolcanlogin | rolsuper | rolparents 399 | ----------+-------------+----------+--------------- 400 | joe | t | f | {postgres,su} 401 | postgres | t | t | {} 402 | (2 rows) 403 | 404 | -- here is how we fix the environment 405 | -- running this in a transaction that will be aborted 406 | -- since we don't really want to make the postgres user 407 | -- nologin during regression testing 408 | BEGIN; 409 | REVOKE postgres FROM su; 410 | ALTER USER postgres NOLOGIN; 411 | -- retest, this time successfully 412 | SELECT 413 | ro.rolname, 414 | ro.rolcanlogin, 415 | ro.rolsuper, 416 | ro.rolparents 417 | FROM roletree ro 418 | WHERE (ro.rolcanlogin AND ro.rolsuper) 419 | OR 420 | ( 421 | ro.rolcanlogin AND EXISTS 422 | ( 423 | SELECT TRUE FROM roletree ri 424 | WHERE ri.rolname = ANY (ro.rolparents) 425 | AND ri.rolsuper 426 | ) 427 | ); 428 | rolname | rolcanlogin | rolsuper | rolparents 429 | ---------+-------------+----------+------------ 430 | (0 rows) 431 | 432 | -- undo those changes 433 | ABORT; 434 | -------------------------------------------------------------------------------- /extension/set_user.sql: -------------------------------------------------------------------------------- 1 | /* set-user.sql */ 2 | 3 | SET LOCAL search_path to @extschema@; 4 | 5 | -- complain if script is sourced in psql, rather than via CREATE EXTENSION 6 | \echo Use "CREATE EXTENSION set_user" to load this file. \quit 7 | 8 | CREATE FUNCTION @extschema@.set_user(text) 9 | RETURNS text 10 | AS 'MODULE_PATHNAME', 'set_user' 11 | LANGUAGE C; 12 | 13 | CREATE FUNCTION @extschema@.set_user(text, text) 14 | RETURNS text 15 | AS 'MODULE_PATHNAME', 'set_user' 16 | LANGUAGE C STRICT; 17 | 18 | REVOKE EXECUTE ON FUNCTION @extschema@.set_user(text) FROM PUBLIC; 19 | REVOKE EXECUTE ON FUNCTION @extschema@.set_user(text, text) FROM PUBLIC; 20 | 21 | CREATE FUNCTION @extschema@.reset_user() 22 | RETURNS text 23 | AS 'MODULE_PATHNAME', 'set_user' 24 | LANGUAGE C; 25 | 26 | CREATE FUNCTION @extschema@.reset_user(text) 27 | RETURNS text 28 | AS 'MODULE_PATHNAME', 'set_user' 29 | LANGUAGE C STRICT; 30 | 31 | GRANT EXECUTE ON FUNCTION @extschema@.reset_user() TO PUBLIC; 32 | GRANT EXECUTE ON FUNCTION @extschema@.reset_user(text) TO PUBLIC; 33 | 34 | /* New functions in 1.1 (now 1.4) begin here */ 35 | 36 | CREATE FUNCTION @extschema@.set_user_u(text) 37 | RETURNS text 38 | AS 'MODULE_PATHNAME', 'set_user' 39 | LANGUAGE C STRICT; 40 | 41 | REVOKE EXECUTE ON FUNCTION @extschema@.set_user_u(text) FROM PUBLIC; 42 | 43 | /* New functions in 3.0 begin here */ 44 | 45 | CREATE FUNCTION @extschema@.set_session_auth(text) 46 | RETURNS text 47 | AS 'MODULE_PATHNAME', 'set_session_auth' 48 | LANGUAGE C STRICT; 49 | REVOKE EXECUTE ON FUNCTION @extschema@.set_session_auth(text) FROM PUBLIC; 50 | -------------------------------------------------------------------------------- /set_user.control: -------------------------------------------------------------------------------- 1 | # set_user extension 2 | comment = 'similar to SET ROLE but with added logging' 3 | default_version = '4.1.0' 4 | module_pathname = '$libdir/set_user' 5 | relocatable = false 6 | -------------------------------------------------------------------------------- /sql/set_user.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION set_user; 2 | 3 | -- Ensure the library is loaded. 4 | LOAD 'set_user'; 5 | 6 | -- Clean up in case a prior regression run failed 7 | -- First suppress NOTICE messages when users/groups don't exist 8 | SET client_min_messages TO 'warning'; 9 | DROP USER IF EXISTS dba, bob, joe, newbs, su; 10 | RESET client_min_messages; 11 | 12 | -- Create some users to work with 13 | CREATE USER dba; 14 | CREATE USER bob; 15 | CREATE USER joe; 16 | CREATE ROLE newbs; 17 | CREATE ROLE su NOINHERIT; 18 | 19 | -- dba is the role we want to allow to execute set_user() 20 | GRANT EXECUTE ON FUNCTION set_user(text) TO dba; 21 | GRANT EXECUTE ON FUNCTION set_user(text,text) TO dba; 22 | GRANT EXECUTE ON FUNCTION set_user_u(text) TO dba; 23 | GRANT newbs TO bob; 24 | -- joe will be able to escalate without set_user() via su 25 | GRANT su TO joe; 26 | GRANT postgres TO su; 27 | 28 | -- test reset_user with no initial set 29 | SELECT reset_user(); 30 | 31 | -- test set_user 32 | SET SESSION AUTHORIZATION dba; 33 | SELECT SESSION_USER, CURRENT_USER; 34 | SELECT set_user('postgres'); 35 | SELECT SESSION_USER, CURRENT_USER; 36 | 37 | -- test set_user_u 38 | SET SESSION AUTHORIZATION dba; 39 | SELECT SESSION_USER, CURRENT_USER; 40 | SELECT set_user_u('postgres'); 41 | SELECT SESSION_USER, CURRENT_USER; 42 | 43 | -- test multiple successive set_user calls 44 | SELECT set_user('joe'); -- fail 45 | 46 | -- ALTER SYSTEM should fail 47 | ALTER SYSTEM SET wal_level = minimal; 48 | 49 | -- COPY PROGRAM should fail 50 | COPY (select 42) TO PROGRAM 'cat'; 51 | 52 | -- SET log_statement should fail 53 | SET log_statement = 'none'; 54 | SET log_statement = DEFAULT; 55 | RESET log_statement; 56 | BEGIN; SET LOCAL log_statement = 'none'; ABORT; 57 | 58 | -- set_config() should fail 59 | SELECT set_config('wal_level', 'minimal', false); 60 | CREATE OR REPLACE FUNCTION backdoor(text, text, boolean) RETURNS BOOL AS 'set_config_by_name' LANGUAGE INTERNAL; 61 | SELECT backdoor('log_statement', 'none', true); 62 | UPDATE pg_settings SET setting = 'none' WHERE name = 'log_statement'; 63 | 64 | -- test reset_user 65 | RESET ROLE; -- should fail 66 | RESET SESSION AUTHORIZATION; -- should fail 67 | SELECT SESSION_USER, CURRENT_USER; 68 | 69 | SELECT reset_user(); -- succeed 70 | 71 | -- test set_user and reset_user with token 72 | SELECT SESSION_USER, CURRENT_USER; 73 | SELECT set_user('bob', 'secret'); 74 | SELECT SESSION_USER, CURRENT_USER; 75 | RESET ROLE; -- should fail 76 | RESET SESSION AUTHORIZATION; -- should fail 77 | SELECT SESSION_USER, CURRENT_USER; 78 | 79 | SELECT reset_user(); -- should fail 80 | SELECT SESSION_USER, CURRENT_USER; 81 | 82 | SELECT reset_user('secret'); -- succeed 83 | SELECT SESSION_USER, CURRENT_USER; 84 | 85 | RESET SESSION AUTHORIZATION; 86 | ALTER SYSTEM SET wal_level = minimal; 87 | COPY (select 42) TO PROGRAM 'cat'; 88 | SET log_statement = DEFAULT; 89 | 90 | -- test transaction handling 91 | CREATE FUNCTION bail() RETURNS bool AS $$ 92 | BEGIN 93 | RAISE EXCEPTION 'bailing out !'; 94 | END; 95 | $$ LANGUAGE plpgsql; 96 | SET SESSION AUTHORIZATION dba; 97 | SELECT SESSION_USER, CURRENT_USER; 98 | 99 | -- bail during set_user_u 100 | SELECT set_user_u('postgres'), bail(); 101 | SELECT SESSION_USER, CURRENT_USER; 102 | SHOW log_statement; 103 | SHOW log_line_prefix; 104 | 105 | -- bail on reset after successful set_user_u 106 | SELECT set_user_u('postgres'); 107 | SELECT SESSION_USER, CURRENT_USER; 108 | SHOW log_statement; 109 | SHOW log_line_prefix; 110 | SELECT reset_user(), bail(); 111 | SELECT SESSION_USER, CURRENT_USER; 112 | SHOW log_statement; 113 | SHOW log_line_prefix; 114 | SELECT reset_user(); 115 | 116 | -- bail during set_user 117 | SELECT set_user('bob'), bail(); 118 | SELECT SESSION_USER, CURRENT_USER; 119 | SHOW log_statement; 120 | SHOW log_line_prefix; 121 | 122 | -- bail during set_user with token 123 | SELECT set_user('bob', 'secret'), bail(); 124 | SELECT SESSION_USER, CURRENT_USER; 125 | SHOW log_statement; 126 | SHOW log_line_prefix; 127 | 128 | -- bail during reset_user with token 129 | SELECT set_user('bob', 'secret'); 130 | SELECT SESSION_USER, CURRENT_USER; 131 | SELECT reset_user('secret'), bail(); 132 | SELECT SESSION_USER, CURRENT_USER; 133 | SELECT reset_user('secret'); 134 | 135 | RESET SESSION AUTHORIZATION; 136 | 137 | -- this is an example of how we might audit existing roles 138 | SET SESSION AUTHORIZATION dba; 139 | SELECT set_user_u('postgres'); 140 | SELECT rolname FROM pg_authid WHERE rolsuper and rolcanlogin; 141 | CREATE OR REPLACE VIEW roletree AS 142 | WITH RECURSIVE 143 | roltree AS ( 144 | SELECT u.rolname AS rolname, 145 | u.oid AS roloid, 146 | u.rolcanlogin, 147 | u.rolsuper, 148 | '{}'::name[] AS rolparents, 149 | NULL::oid AS parent_roloid, 150 | NULL::name AS parent_rolname 151 | FROM pg_catalog.pg_authid u 152 | LEFT JOIN pg_catalog.pg_auth_members m on u.oid = m.member 153 | LEFT JOIN pg_catalog.pg_authid g on m.roleid = g.oid 154 | WHERE g.oid IS NULL 155 | UNION ALL 156 | SELECT u.rolname AS rolname, 157 | u.oid AS roloid, 158 | u.rolcanlogin, 159 | u.rolsuper, 160 | t.rolparents || g.rolname AS rolparents, 161 | g.oid AS parent_roloid, 162 | g.rolname AS parent_rolname 163 | FROM pg_catalog.pg_authid u 164 | JOIN pg_catalog.pg_auth_members m on u.oid = m.member 165 | JOIN pg_catalog.pg_authid g on m.roleid = g.oid 166 | JOIN roltree t on t.roloid = g.oid 167 | ) 168 | SELECT 169 | r.rolname, 170 | r.roloid, 171 | r.rolcanlogin, 172 | r.rolsuper, 173 | r.rolparents 174 | FROM roltree r 175 | ORDER BY 1; 176 | 177 | -- this will show unacceptable results 178 | -- since postgres can log in directly and 179 | -- joe can escalate via su to postgres 180 | SELECT 181 | ro.rolname, 182 | ro.rolcanlogin, 183 | ro.rolsuper, 184 | ro.rolparents 185 | FROM roletree ro 186 | WHERE (ro.rolcanlogin AND ro.rolsuper) 187 | OR 188 | ( 189 | ro.rolcanlogin AND EXISTS 190 | ( 191 | SELECT TRUE FROM roletree ri 192 | WHERE ri.rolname = ANY (ro.rolparents) 193 | AND ri.rolsuper 194 | ) 195 | ); 196 | 197 | -- here is how we fix the environment 198 | -- running this in a transaction that will be aborted 199 | -- since we don't really want to make the postgres user 200 | -- nologin during regression testing 201 | BEGIN; 202 | REVOKE postgres FROM su; 203 | ALTER USER postgres NOLOGIN; 204 | 205 | -- retest, this time successfully 206 | SELECT 207 | ro.rolname, 208 | ro.rolcanlogin, 209 | ro.rolsuper, 210 | ro.rolparents 211 | FROM roletree ro 212 | WHERE (ro.rolcanlogin AND ro.rolsuper) 213 | OR 214 | ( 215 | ro.rolcanlogin AND EXISTS 216 | ( 217 | SELECT TRUE FROM roletree ri 218 | WHERE ri.rolname = ANY (ro.rolparents) 219 | AND ri.rolsuper 220 | ) 221 | ); 222 | 223 | -- undo those changes 224 | ABORT; 225 | -------------------------------------------------------------------------------- /src/compatibility.h: -------------------------------------------------------------------------------- 1 | /* ------------------------------------------------------------------------- 2 | * 3 | * compatibility.h 4 | * 5 | * Definitions for maintaining compatibility across Postgres versions. 6 | * 7 | * Copyright (c) 2010-2022, PostgreSQL Global Development Group 8 | * 9 | * ------------------------------------------------------------------------- 10 | */ 11 | 12 | #ifndef SET_USER_COMPAT_H 13 | #define SET_USER_COMPAT_H 14 | 15 | #ifndef NO_ASSERT_AUTH_UID_ONCE 16 | #define NO_ASSERT_AUTH_UID_ONCE !USE_ASSERT_CHECKING 17 | #endif 18 | 19 | /* 20 | * PostgreSQL version 17+ 21 | * 22 | * - Sets bypass_login_check parameter to false in InitializeSessionUserId funcion 23 | */ 24 | #if PG_VERSION_NUM >= 170000 25 | 26 | #ifndef INITSESSIONUSER 27 | #define INITSESSIONUSER 28 | #define _InitializeSessionUserId(name,ouserid) InitializeSessionUserId(name,ouserid,false) 29 | #endif 30 | 31 | #endif /* 17+ */ 32 | 33 | /* 34 | * PostgreSQL version 14+ 35 | * 36 | * Introduces ReadOnlyTree boolean 37 | */ 38 | #if PG_VERSION_NUM >= 140000 39 | #define _PU_HOOK \ 40 | static void PU_hook(PlannedStmt *pstmt, const char *queryString, bool ReadOnlyTree, \ 41 | ProcessUtilityContext context, ParamListInfo params, \ 42 | QueryEnvironment *queryEnv, \ 43 | DestReceiver *dest, QueryCompletion *qc) 44 | 45 | #define _prev_hook \ 46 | prev_hook(pstmt, queryString, ReadOnlyTree, context, params, queryEnv, dest, qc) 47 | 48 | #define _standard_ProcessUtility \ 49 | standard_ProcessUtility(pstmt, queryString, ReadOnlyTree, context, params, queryEnv, dest, qc) 50 | 51 | #define getObjectIdentity(address) \ 52 | getObjectIdentity(address,false) 53 | 54 | #endif /* 14+ */ 55 | 56 | /* 57 | * PostgreSQL version 13+ 58 | * 59 | * Introduces QueryCompletion struct 60 | */ 61 | #if PG_VERSION_NUM >= 130000 62 | #ifndef _PU_HOOK 63 | #define _PU_HOOK \ 64 | static void PU_hook(PlannedStmt *pstmt, const char *queryString, \ 65 | ProcessUtilityContext context, ParamListInfo params, \ 66 | QueryEnvironment *queryEnv, \ 67 | DestReceiver *dest, QueryCompletion *qc) 68 | 69 | #define _prev_hook \ 70 | prev_hook(pstmt, queryString, context, params, queryEnv, dest, qc) 71 | 72 | #define _standard_ProcessUtility \ 73 | standard_ProcessUtility(pstmt, queryString, context, params, queryEnv, dest, qc) 74 | #endif 75 | 76 | #ifndef INITSESSIONUSER 77 | #define INITSESSIONUSER 78 | #define _InitializeSessionUserId(name,ouserid) InitializeSessionUserId(name,ouserid) 79 | 80 | #endif 81 | 82 | #endif /* 13+ */ 83 | 84 | #if !defined(PG_VERSION_NUM) || PG_VERSION_NUM < 130000 85 | #error "This extension only builds with PostgreSQL 13 or later" 86 | #endif 87 | 88 | /* Use our version-specific static declaration here */ 89 | _PU_HOOK; 90 | 91 | #endif /* SET_USER_COMPAT_H */ 92 | -------------------------------------------------------------------------------- /src/set_user.c: -------------------------------------------------------------------------------- 1 | /* 2 | * set_user.c 3 | * 4 | * Joe Conway 5 | * 6 | * This code is released under the PostgreSQL license. 7 | * 8 | * Copyright 2015-2025 Crunchy Data Solutions, Inc. 9 | * 10 | * Permission to use, copy, modify, and distribute this software and its 11 | * documentation for any purpose, without fee, and without a written 12 | * agreement is hereby granted, provided that the above copyright notice 13 | * and this paragraph and the following two paragraphs appear in all copies. 14 | * 15 | * IN NO EVENT SHALL CRUNCHY DATA SOLUTIONS, INC. BE LIABLE TO ANY PARTY 16 | * FOR DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, 17 | * INCLUDING LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS 18 | * DOCUMENTATION, EVEN IF THE CRUNCHY DATA SOLUTIONS, INC. HAS BEEN ADVISED 19 | * OF THE POSSIBILITY OF SUCH DAMAGE. 20 | * 21 | * THE CRUNCHY DATA SOLUTIONS, INC. SPECIFICALLY DISCLAIMS ANY WARRANTIES, 22 | * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY 23 | * AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS 24 | * ON AN "AS IS" BASIS, AND THE CRUNCHY DATA SOLUTIONS, INC. HAS NO 25 | * OBLIGATIONS TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR 26 | * MODIFICATIONS. 27 | */ 28 | #include "postgres.h" 29 | 30 | #include "pg_config.h" 31 | 32 | #include "access/genam.h" 33 | #include "access/htup_details.h" 34 | #include "access/table.h" 35 | #include "access/xact.h" 36 | #include "catalog/indexing.h" 37 | #include "catalog/objectaccess.h" 38 | #include "catalog/objectaddress.h" 39 | #include "catalog/pg_authid.h" 40 | #include "catalog/pg_proc.h" 41 | #include "miscadmin.h" 42 | #include "parser/parse_func.h" 43 | #include "tcop/utility.h" 44 | #include "utils/acl.h" 45 | #include "utils/builtins.h" 46 | #include "utils/catcache.h" 47 | #include "utils/fmgroids.h" 48 | #include "utils/guc.h" 49 | #include "utils/memutils.h" 50 | #include "utils/snapmgr.h" 51 | #include "utils/syscache.h" 52 | #include "utils/rel.h" 53 | #include "utils/varlena.h" 54 | 55 | #include "set_user.h" 56 | 57 | PG_MODULE_MAGIC; 58 | 59 | #include "compatibility.h" 60 | 61 | #define ALLOWLIST_WILDCARD "*" 62 | #define SUPERUSER_AUDIT_TAG "AUDIT" 63 | 64 | static ProcessUtility_hook_type prev_hook = NULL; 65 | static object_access_hook_type next_object_access_hook; 66 | 67 | /* transaction handler */ 68 | static void set_user_xact_handler (XactEvent event, void *arg); 69 | 70 | /* set_user transaction state */ 71 | typedef struct 72 | { 73 | Oid userid; 74 | bool is_superuser; 75 | char *username; 76 | char *log_statement; 77 | const char *log_prefix; 78 | char *reset_token; 79 | } SetUserXactState; 80 | 81 | static SetUserXactState *curr_state; 82 | static SetUserXactState *pending_state; 83 | static SetUserXactState *prev_state; 84 | static void set_user_free_state(SetUserXactState **state); 85 | 86 | static bool is_reset = false; 87 | 88 | static const char *su = "Superuser "; 89 | static const char *nsu = ""; 90 | 91 | static bool Block_AS = false; 92 | static bool Block_CP = false; 93 | static bool Block_LS = false; 94 | static char *SU_Allowlist = NULL; 95 | static char *NOSU_TargetAllowlist = NULL; 96 | static char *SU_AuditTag = NULL; 97 | static bool exit_on_error = true; 98 | static const char *set_config_proc_name = "set_config_by_name"; 99 | static List *set_config_oid_cache = NIL; 100 | 101 | static void PostSetUserHook(bool is_reset, const char *newuser); 102 | 103 | extern Datum set_user(PG_FUNCTION_ARGS); 104 | void _PG_init(void); 105 | void _PG_fini(void); 106 | 107 | /* used to block set_config() */ 108 | static void set_user_object_access(ObjectAccessType access, Oid classId, Oid objectId, int subId, void *arg); 109 | static void set_user_block_set_config(Oid functionId); 110 | static void set_user_check_proc(HeapTuple procTup, Relation rel); 111 | static void set_user_cache_proc(Oid functionId); 112 | 113 | /* 114 | * check_user_allowlist 115 | * 116 | * Check if user is contained by allowlist 117 | * 118 | */ 119 | static bool 120 | check_user_allowlist(Oid userId, const char *allowlist) 121 | { 122 | char *rawstring = NULL; 123 | List *elemlist; 124 | ListCell *l; 125 | bool result = false; 126 | 127 | if (allowlist == NULL || allowlist[0] == '\0') 128 | return false; 129 | 130 | rawstring = pstrdup(allowlist); 131 | 132 | /* Parse string into list of identifiers */ 133 | if (!SplitIdentifierString(rawstring, ',', &elemlist)) 134 | { 135 | /* syntax error in list */ 136 | ereport(ERROR, 137 | (errcode(ERRCODE_SYNTAX_ERROR), 138 | errmsg("invalid syntax in parameter"))); 139 | } 140 | 141 | /* Allow all users to escalate if allowlist is a solo wildcard character. */ 142 | if (list_length(elemlist) == 1) 143 | { 144 | char *first_elem = NULL; 145 | 146 | first_elem = (char *) linitial(elemlist); 147 | if (pg_strcasecmp(first_elem, ALLOWLIST_WILDCARD) == 0) 148 | return true; 149 | } 150 | 151 | /* 152 | * Check whole allowlist to see if it contains the current username and no 153 | * wildcard character. Throw an error if the allowlist contains both. 154 | */ 155 | foreach(l, elemlist) 156 | { 157 | char *elem = (char *) lfirst(l); 158 | 159 | if (elem[0] == '+') 160 | { 161 | Oid roleId; 162 | roleId = get_role_oid(elem + 1, false); 163 | if (!OidIsValid(roleId)) 164 | result = false; 165 | 166 | /* Check to see if userId is contained by group role in allowlist */ 167 | result = has_privs_of_role(userId, roleId); 168 | } 169 | else 170 | { 171 | if (pg_strcasecmp(elem, GetUserNameFromId(userId, false)) == 0) 172 | result = true; 173 | else if(pg_strcasecmp(elem, ALLOWLIST_WILDCARD) == 0) 174 | /* No explicit usernames intermingled with wildcard. */ 175 | ereport(ERROR, 176 | (errcode(ERRCODE_SYNTAX_ERROR), 177 | errmsg("invalid syntax in parameter"), 178 | errhint("Either remove users from set_user.superuser_allowlist " 179 | "or remove the wildcard character \"%s\". The allowlist " 180 | "cannot contain both.", 181 | ALLOWLIST_WILDCARD))); 182 | } 183 | } 184 | return result; 185 | } 186 | 187 | /* 188 | * Return the oid of the tuple based on the provided catalogID 189 | */ 190 | static Oid 191 | heap_tuple_get_oid(HeapTuple tuple, Oid catalogID) 192 | { 193 | switch (catalogID) 194 | { 195 | case ProcedureRelationId: 196 | return ((Form_pg_proc) GETSTRUCT(tuple))->oid; 197 | break; 198 | 199 | case AuthIdRelationId: 200 | return ((Form_pg_authid) GETSTRUCT(tuple))->oid; 201 | break; 202 | 203 | default: 204 | ereport(ERROR, 205 | (errcode(ERRCODE_SYNTAX_ERROR), 206 | errmsg("set_user: invalid relation ID provided"))); 207 | return 0; 208 | } 209 | } 210 | 211 | /* 212 | * Similar to SET ROLE but with added logging and some additional 213 | * control over allowed actions 214 | * 215 | */ 216 | PG_FUNCTION_INFO_V1(set_user); 217 | Datum 218 | set_user(PG_FUNCTION_ARGS) 219 | { 220 | bool argisnull = PG_ARGISNULL(0); 221 | int nargs = PG_NARGS(); 222 | HeapTuple roleTup; 223 | MemoryContext oldcontext = NULL; 224 | bool is_token = false; 225 | bool is_privileged = false; 226 | 227 | /* 228 | * Disallow `set_user()` inside a transaction block. The 229 | * semantics are too strange, and I cannot think of a 230 | * good use case where it would make sense anyway. 231 | * Perhaps one day we will need to rethink this... 232 | */ 233 | if (IsTransactionBlock()) 234 | { 235 | ereport(ERROR, 236 | (errcode(ERRCODE_FEATURE_NOT_SUPPORTED), 237 | errmsg("set_user: \"set_user()\" not allowed within transaction block"), 238 | errhint("Use \"set_user()\" outside transaction block instead."))); 239 | } 240 | 241 | /* 242 | * set_user(non_null_arg text) 243 | * 244 | * Might be set_user(username) but might also be set_user(reset_token). 245 | * The former case we need to switch user normally, the latter is a 246 | * reset with token provided. We need to determine which one we have. 247 | */ 248 | if (nargs == 1 && !argisnull) 249 | { 250 | Oid funcOid = fcinfo->flinfo->fn_oid; 251 | HeapTuple procTup; 252 | Form_pg_proc procStruct; 253 | char *funcname; 254 | 255 | /* Lookup the pg_proc tuple by Oid */ 256 | procTup = SearchSysCache1(PROCOID, ObjectIdGetDatum(funcOid)); 257 | if (!HeapTupleIsValid(procTup)) 258 | elog(ERROR, "cache lookup failed for function %u", funcOid); 259 | 260 | procStruct = (Form_pg_proc) GETSTRUCT(procTup); 261 | if (!procStruct) 262 | { 263 | ereport(ERROR, 264 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), 265 | errmsg("set_user: function lookup failed for %u", funcOid))); 266 | } 267 | 268 | funcname = pstrdup(NameStr(procStruct->proname)); 269 | ReleaseSysCache(procTup); 270 | 271 | if (strcmp(funcname, "reset_user") == 0) 272 | { 273 | is_reset = true; 274 | is_token = true; 275 | } 276 | 277 | if (strcmp(funcname, "set_user_u") == 0) 278 | is_privileged = true; 279 | } 280 | /* 281 | * set_user() or set_user(NULL) ==> always a reset 282 | */ 283 | else if (nargs == 0 || (nargs == 1 && argisnull)) 284 | is_reset = true; 285 | 286 | /* Switch to a persistent memory context to store state */ 287 | oldcontext = MemoryContextSwitchTo(TopMemoryContext); 288 | 289 | /* Need to pfree in the case of a reset with no initial set */ 290 | pending_state = palloc0(sizeof(SetUserXactState)); 291 | if ((nargs == 1 && !is_reset) || nargs == 2) 292 | { 293 | /* we are setting a new user */ 294 | if (prev_state != NULL && prev_state->userid != InvalidOid) 295 | { 296 | ereport(ERROR, 297 | (errcode(ERRCODE_INTERNAL_ERROR), 298 | errmsg("must reset previous user prior to setting again"))); 299 | } 300 | 301 | pending_state->username = text_to_cstring(PG_GETARG_TEXT_PP(0)); 302 | 303 | /* with 2 args, the caller wants to specify a reset token */ 304 | if (nargs == 2) 305 | { 306 | /* this should never be NULL but just in case */ 307 | if (PG_ARGISNULL(1)) 308 | { 309 | ereport(ERROR, 310 | (errcode(ERRCODE_INVALID_PARAMETER_VALUE), 311 | errmsg("set_user: NULL reset_token not valid"))); 312 | } 313 | 314 | /*capture the reset token */ 315 | pending_state->reset_token = text_to_cstring(PG_GETARG_TEXT_PP(1)); 316 | } 317 | 318 | /* Look up the username */ 319 | roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(pending_state->username)); 320 | if (!HeapTupleIsValid(roleTup)) 321 | elog(ERROR, "role \"%s\" does not exist", pending_state->username); 322 | 323 | pending_state->userid = heap_tuple_get_oid(roleTup, AuthIdRelationId); 324 | pending_state->is_superuser = ((Form_pg_authid) GETSTRUCT(roleTup))->rolsuper; 325 | ReleaseSysCache(roleTup); 326 | 327 | if (pending_state->is_superuser) 328 | { 329 | if (!is_privileged) 330 | /* can only escalate with set_user_u */ 331 | ereport(ERROR, 332 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 333 | errmsg("switching to superuser not allowed"), 334 | errhint("Use \'set_user_u\' to escalate."))); 335 | else if (!check_user_allowlist(GetUserId(), SU_Allowlist)) 336 | /* check superuser allowlist*/ 337 | ereport(ERROR, 338 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 339 | errmsg("switching to superuser not allowed"), 340 | errhint("Add current user to set_user.superuser_allowlist."))); 341 | } 342 | else if(!check_user_allowlist(pending_state->userid, NOSU_TargetAllowlist)) 343 | { 344 | ereport(ERROR, 345 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 346 | errmsg("switching to role is not allowed"), 347 | errhint("Add target role to set_user.nosuperuser_target_allowlist."))); 348 | } 349 | 350 | /* Keep track of current state */ 351 | if (curr_state == NULL) 352 | { 353 | curr_state = palloc0(sizeof(SetUserXactState)); 354 | curr_state->log_statement = pstrdup(GetConfigOption("log_statement", false, false)); 355 | curr_state->log_prefix = pstrdup(GetConfigOption("log_line_prefix", true, false)); 356 | curr_state->reset_token = pending_state->reset_token; 357 | curr_state->userid = GetUserId(); 358 | curr_state->username = GetUserNameFromId(curr_state->userid, false); 359 | curr_state->is_superuser = superuser_arg(curr_state->userid); 360 | } 361 | 362 | if (pending_state->is_superuser && Block_LS) 363 | { 364 | pending_state->log_prefix = NULL; 365 | 366 | /* 367 | * Add a custom AUDIT tag to postgresql.conf setting 368 | * 'log_line_prefix' so log statements are tagged for easy 369 | * filtering. 370 | */ 371 | if (curr_state->log_prefix) 372 | pending_state->log_prefix = psprintf("%s%s: ", curr_state->log_prefix, SU_AuditTag); 373 | else 374 | pending_state->log_prefix = pstrdup(SU_AuditTag); 375 | 376 | /* 377 | * Force logging of everything if block_log_statement is true 378 | * and we are escalating to superuser. If not escalating to superuser the 379 | * caller could always set log_statement to all prior to using set_user, 380 | * and ensure Block_LS is true. 381 | */ 382 | pending_state->log_statement = pstrdup("all"); 383 | } 384 | } 385 | else if (is_reset) 386 | { 387 | /* 388 | * set_user not active. No need to change pending state here. 389 | * The xact handler has no state to process. Just reset the 390 | * `is_reset` flag and return success. 391 | */ 392 | if (prev_state == NULL || prev_state->userid == InvalidOid) 393 | { 394 | is_reset = false; 395 | set_user_free_state(&pending_state); 396 | PG_RETURN_TEXT_P(cstring_to_text("OK")); 397 | } 398 | 399 | /* Enforce token comparison if the reset_token is set */ 400 | if (prev_state->reset_token) 401 | { 402 | if (!is_token) 403 | { 404 | ereport(ERROR, 405 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 406 | errmsg("reset token required but not provided"))); 407 | } 408 | 409 | pending_state->reset_token = text_to_cstring(PG_GETARG_TEXT_PP(0)); 410 | if (strcmp(prev_state->reset_token, pending_state->reset_token) != 0) 411 | { 412 | ereport(ERROR, 413 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 414 | errmsg("incorrect reset token provided"))); 415 | } 416 | } 417 | 418 | /* store old state as pending */ 419 | pending_state->userid = prev_state->userid; 420 | pending_state->username = GetUserNameFromId(prev_state->userid, false); 421 | pending_state->log_statement = prev_state->log_statement; 422 | pending_state->log_prefix = prev_state->log_prefix; 423 | pending_state->is_superuser = superuser_arg(prev_state->userid); 424 | } 425 | else 426 | /* should not happen */ 427 | elog(ERROR, "unexpected argument combination"); 428 | 429 | MemoryContextSwitchTo(oldcontext); 430 | PG_RETURN_TEXT_P(cstring_to_text("OK")); 431 | } 432 | 433 | /* 434 | * set_user_free_state 435 | * 436 | * Convenience function for cleaning up transaction state struct. 437 | */ 438 | static void 439 | set_user_free_state(SetUserXactState **state) 440 | { 441 | if (*state != NULL) 442 | { 443 | (*state)->userid = InvalidOid; 444 | pfree(*state); 445 | *state = NULL; 446 | } 447 | } 448 | 449 | /* 450 | * set_user_xact_handler 451 | * 452 | * Keeps track of variables managed by set_user and ensures proper state during 453 | * transaction ABORT. 454 | */ 455 | static void 456 | set_user_xact_handler (XactEvent event, void *arg) 457 | { 458 | MemoryContext oldcontext = NULL; 459 | 460 | switch (event) 461 | { 462 | case XACT_EVENT_PRE_COMMIT: 463 | if (pending_state == NULL || curr_state == NULL) 464 | return; 465 | 466 | oldcontext = MemoryContextSwitchTo(TopMemoryContext); 467 | elog(LOG, "%sRole %s transitioning to %sRole %s", 468 | curr_state->is_superuser ? su : nsu, 469 | curr_state->username, 470 | pending_state->is_superuser ? su : nsu, 471 | pending_state->username); 472 | 473 | /* Do the actual work */ 474 | SetCurrentRoleId(pending_state->userid, pending_state->is_superuser); 475 | PostSetUserHook(is_reset, pending_state->username); 476 | 477 | /* Update GUCs */ 478 | SetConfigOption("log_statement", pending_state->log_statement, PGC_SUSET, PGC_S_SESSION); 479 | SetConfigOption("log_line_prefix", pending_state->log_prefix, PGC_POSTMASTER, PGC_S_SESSION); 480 | 481 | /* start fresh */ 482 | if (is_reset) 483 | { 484 | set_user_free_state(&pending_state); 485 | set_user_free_state(&curr_state); 486 | set_user_free_state(&prev_state); 487 | 488 | /* always clear is_reset after we've processed it */ 489 | is_reset = false; 490 | } 491 | else 492 | { 493 | prev_state = palloc0(sizeof(SetUserXactState)); 494 | memcpy(prev_state, curr_state, sizeof(SetUserXactState)); 495 | set_user_free_state(&curr_state); 496 | 497 | curr_state = palloc0(sizeof(SetUserXactState)); 498 | memcpy(curr_state, pending_state, sizeof(SetUserXactState)); 499 | set_user_free_state(&pending_state); 500 | } 501 | 502 | MemoryContextSwitchTo(oldcontext); 503 | break; 504 | case XACT_EVENT_ABORT: 505 | set_user_free_state(&pending_state); 506 | is_reset = false; 507 | break; 508 | default: 509 | break; 510 | } 511 | } 512 | 513 | void 514 | _PG_init(void) 515 | { 516 | DefineCustomBoolVariable("set_user.block_alter_system", 517 | "Block ALTER SYSTEM commands", 518 | NULL, &Block_AS, true, PGC_SIGHUP, 519 | 0, NULL, NULL, NULL); 520 | 521 | DefineCustomBoolVariable("set_user.block_copy_program", 522 | "Blocks COPY PROGRAM commands", 523 | NULL, &Block_CP, true, PGC_SIGHUP, 524 | 0, NULL, NULL, NULL); 525 | 526 | DefineCustomBoolVariable("set_user.block_log_statement", 527 | "Blocks \"SET log_statement\" commands", 528 | NULL, &Block_LS, true, PGC_SIGHUP, 529 | 0, NULL, NULL, NULL); 530 | 531 | DefineCustomStringVariable("set_user.nosuperuser_target_allowlist", 532 | "List of roles that can be an argument to set_user", 533 | NULL, &NOSU_TargetAllowlist, ALLOWLIST_WILDCARD, PGC_SIGHUP, 534 | 0, NULL, NULL, NULL); 535 | 536 | DefineCustomStringVariable("set_user.superuser_allowlist", 537 | "Allows a list of users to use set_user_u for superuser escalation", 538 | NULL, &SU_Allowlist, ALLOWLIST_WILDCARD, PGC_SIGHUP, 539 | 0, NULL, NULL, NULL); 540 | 541 | DefineCustomStringVariable("set_user.superuser_audit_tag", 542 | "Set custom tag for superuser audit escalation", 543 | NULL, &SU_AuditTag, SUPERUSER_AUDIT_TAG, PGC_SIGHUP, 544 | 0, NULL, NULL, NULL); 545 | 546 | DefineCustomBoolVariable("set_user.exit_on_error", 547 | "Exit backend process on ERROR during set_session_auth()", 548 | NULL, &exit_on_error, true, PGC_SIGHUP, 549 | 0, NULL, NULL, NULL); 550 | 551 | /* Install hook */ 552 | prev_hook = ProcessUtility_hook; 553 | ProcessUtility_hook = PU_hook; 554 | 555 | /* Object access hook */ 556 | next_object_access_hook = object_access_hook; 557 | object_access_hook = set_user_object_access; 558 | 559 | RegisterXactCallback(set_user_xact_handler, NULL); 560 | } 561 | 562 | void 563 | _PG_fini(void) 564 | { 565 | ProcessUtility_hook = prev_hook; 566 | } 567 | 568 | /* 569 | * _PU_HOOK 570 | * 571 | * Compatibility shim for PU_hook. Handles changing function signature 572 | * between versions of PostgreSQL. 573 | */ 574 | _PU_HOOK 575 | { 576 | /* if set_user has been used to transition, enforce set_user GUCs */ 577 | if (curr_state != NULL && curr_state->userid != InvalidOid) 578 | { 579 | switch (nodeTag((Node *) pstmt->utilityStmt)) 580 | { 581 | case T_AlterSystemStmt: 582 | if (Block_AS) 583 | ereport(ERROR, 584 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 585 | errmsg("ALTER SYSTEM blocked by set_user config"))); 586 | break; 587 | case T_CopyStmt: 588 | if (((CopyStmt *)pstmt->utilityStmt)->is_program && Block_CP) 589 | ereport(ERROR, 590 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 591 | errmsg("COPY PROGRAM blocked by set_user config"))); 592 | break; 593 | case T_VariableSetStmt: 594 | if ((strcmp(((VariableSetStmt *)pstmt->utilityStmt)->name, 595 | "log_statement") == 0) && 596 | Block_LS) 597 | { 598 | ereport(ERROR, 599 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 600 | errmsg("\"SET log_statement\" blocked by set_user config"))); 601 | } 602 | else if ((strcmp(((VariableSetStmt *)pstmt->utilityStmt)->name, 603 | "role") == 0)) 604 | { 605 | ereport(ERROR, 606 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 607 | errmsg("\"SET/RESET ROLE\" blocked by set_user"), 608 | errhint("Use \"SELECT set_user();\" or \"SELECT reset_user();\" instead."))); 609 | } 610 | else if ((strcmp(((VariableSetStmt *)pstmt->utilityStmt)->name, 611 | "session_authorization") == 0)) 612 | { 613 | ereport(ERROR, 614 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 615 | errmsg("\"SET/RESET SESSION AUTHORIZATION\" blocked by set_user"), 616 | errhint("Use \"SELECT set_user();\" or \"SELECT reset_user();\" instead."))); 617 | } 618 | break; 619 | default: 620 | break; 621 | } 622 | } 623 | 624 | /* 625 | * Now pass-off handling either to the previous ProcessUtility hook 626 | * or to the standard ProcessUtility. 627 | * 628 | * These functions are also called by their compatibility variants. 629 | */ 630 | if (prev_hook) 631 | { 632 | _prev_hook; 633 | } 634 | else 635 | { 636 | _standard_ProcessUtility; 637 | } 638 | } 639 | 640 | /* 641 | * PostSetUserHook 642 | * 643 | * Handler for set_user post hooks 644 | */ 645 | void 646 | PostSetUserHook(bool is_reset, const char *username) 647 | { 648 | List **hooks_queue; 649 | ListCell *hooks_entry = NULL; 650 | 651 | hooks_queue = (List **) find_rendezvous_variable(SET_USER_HOOKS_KEY); 652 | foreach (hooks_entry, *hooks_queue) 653 | { 654 | SetUserHooks **post_hooks = (SetUserHooks **) lfirst(hooks_entry); 655 | if (post_hooks) 656 | { 657 | if (!is_reset && (*post_hooks)->post_set_user) 658 | { 659 | (*post_hooks)->post_set_user(username); 660 | } 661 | else if ((*post_hooks)->post_reset_user) 662 | { 663 | (*post_hooks)->post_reset_user(); 664 | } 665 | } 666 | } 667 | } 668 | 669 | /* 670 | * Similar to SET SESSION AUTHORIZATION, except: 671 | * 672 | * 1. does not require superuser (GRANTable) 673 | * 2. does not allow switching to a superuser 674 | * 3. does not allow reset/switching back 675 | * 4. Can be configured to throw FATAL/exit for all ERRORs 676 | */ 677 | PG_FUNCTION_INFO_V1(set_session_auth); 678 | Datum 679 | set_session_auth(PG_FUNCTION_ARGS) 680 | { 681 | bool orig_exit_on_err = ExitOnAnyError; 682 | #if NO_ASSERT_AUTH_UID_ONCE 683 | char *newuser = text_to_cstring(PG_GETARG_TEXT_PP(0)); 684 | HeapTuple roleTup; 685 | bool NewUser_is_superuser = false; 686 | 687 | ExitOnAnyError = exit_on_error; 688 | 689 | /* Look up the username */ 690 | roleTup = SearchSysCache1(AUTHNAME, PointerGetDatum(newuser)); 691 | if (!HeapTupleIsValid(roleTup)) 692 | elog(ERROR, "role \"%s\" does not exist", newuser); 693 | 694 | NewUser_is_superuser = ((Form_pg_authid) GETSTRUCT(roleTup))->rolsuper; 695 | ReleaseSysCache(roleTup); 696 | 697 | /* cannot escalate to superuser */ 698 | if (NewUser_is_superuser) 699 | ereport(ERROR, 700 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 701 | errmsg("switching to superuser not allowed"), 702 | errhint("Use \'set_user_u\' to escalate."))); 703 | 704 | _InitializeSessionUserId(newuser, InvalidOid); 705 | #else 706 | ExitOnAnyError = exit_on_error; 707 | elog(ERROR, "Assert build disables set_session_auth()"); 708 | #endif 709 | 710 | ExitOnAnyError = orig_exit_on_err; 711 | PG_RETURN_TEXT_P(cstring_to_text("OK")); 712 | } 713 | 714 | /* 715 | * set_user_object_access 716 | * 717 | * Add some extra checking of bypass functions using the object access hook. 718 | * 719 | */ 720 | static void 721 | set_user_object_access (ObjectAccessType access, Oid classId, Oid objectId, int subId, void *arg) 722 | { 723 | /* Process the next object_access_hook before continuing */ 724 | if (next_object_access_hook) 725 | { 726 | (*next_object_access_hook)(access, classId, objectId, subId, arg); 727 | } 728 | 729 | /* If set_user has been used to transition, enforce `set_config` block. */ 730 | if (curr_state != NULL && curr_state->userid != InvalidOid) 731 | { 732 | switch (access) 733 | { 734 | case OAT_FUNCTION_EXECUTE: 735 | { 736 | /* Update the `set_config` Oid cache if necessary. */ 737 | set_user_cache_proc(InvalidOid); 738 | 739 | /* Now see if this function is blocked */ 740 | set_user_block_set_config(objectId); 741 | break; 742 | } 743 | case OAT_POST_ALTER: 744 | case OAT_POST_CREATE: 745 | { 746 | if (classId == ProcedureRelationId) 747 | { 748 | set_user_cache_proc(objectId); 749 | } 750 | break; 751 | } 752 | default: 753 | break; 754 | } 755 | } 756 | } 757 | 758 | /* 759 | * set_user_block_set_config 760 | * 761 | * Error out if the provided functionId is in the `set_config_procs` cache. 762 | */ 763 | static void 764 | set_user_block_set_config(Oid functionId) 765 | { 766 | MemoryContext ctx; 767 | 768 | /* This is where we store the set_config Oid cache. */ 769 | ctx = MemoryContextSwitchTo(CacheMemoryContext); 770 | 771 | /* Check the cache for the current function Oid */ 772 | if (list_member_oid(set_config_oid_cache, functionId)) 773 | { 774 | ObjectAddress object; 775 | char *funcname = NULL; 776 | 777 | object.classId = ProcedureRelationId; 778 | object.objectId = functionId; 779 | object.objectSubId = 0; 780 | 781 | funcname = getObjectIdentity(&object); 782 | ereport(ERROR, 783 | (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 784 | errmsg("\"%s\" blocked by set_user", funcname), 785 | errhint("Use \"SET\" syntax instead."))); 786 | } 787 | 788 | MemoryContextSwitchTo(ctx); 789 | } 790 | 791 | /* 792 | * set_user_check_proc 793 | * 794 | * Check the specified HeapTuple to see if its `prosrc` attribute matches 795 | * `set_config_by_name`. Update the cache as appropriate: 796 | * 797 | * 1) Add to the cache if it's not there but `prosrc` matches. 798 | * 799 | * 2) Remove from the cache if it's present and no longer matches. 800 | */ 801 | static void 802 | set_user_check_proc(HeapTuple procTup, Relation rel) 803 | { 804 | MemoryContext ctx; 805 | Datum prosrcdatum; 806 | bool isnull; 807 | Oid procoid; 808 | 809 | /* For function metadata (Oid) */ 810 | procoid = heap_tuple_get_oid(procTup, ProcedureRelationId); 811 | 812 | /* Figure out the underlying function */ 813 | prosrcdatum = heap_getattr(procTup, Anum_pg_proc_prosrc, RelationGetDescr(rel), &isnull); 814 | if (isnull) 815 | { 816 | ereport(ERROR, 817 | (errcode(ERRCODE_INTERNAL_ERROR), 818 | errmsg("set_user: null prosrc for function %u", procoid))); 819 | } 820 | 821 | /* 822 | * The Oid cache is as good as the underlying cache context, so store it 823 | * there. 824 | */ 825 | ctx = MemoryContextSwitchTo(CacheMemoryContext); 826 | 827 | /* Make sure the Oid cache is up-to-date */ 828 | if (strcmp(TextDatumGetCString(prosrcdatum), set_config_proc_name) == 0) 829 | { 830 | set_config_oid_cache = list_append_unique_oid(set_config_oid_cache, procoid); 831 | } 832 | else if (list_member_oid(set_config_oid_cache, procoid)) 833 | { 834 | set_config_oid_cache = list_delete_oid(set_config_oid_cache, procoid); 835 | } 836 | 837 | MemoryContextSwitchTo(ctx); 838 | } 839 | 840 | /* 841 | * set_user_cache_proc 842 | * 843 | * This function has two modes of operation, based on the provided argument: 844 | * 845 | * 1) `functionId` is not set (InvalidOid) - scan all procedures to 846 | * initialize a list of function Oids which call `set_config_by_name()` under the 847 | * hood. 848 | * 849 | * 2) `functionId` is a valid Oid - grab the syscache entry for the provided 850 | * Oid to inspect `prosrc` attribute and determine whether it should be in the 851 | * `set_config_oid_cache` list. 852 | */ 853 | static void 854 | set_user_cache_proc(Oid functionId) 855 | { 856 | HeapTuple procTup; 857 | Relation rel; 858 | SysScanDesc sscan; 859 | /* Defaults for full catalog scan */ 860 | Oid indexId = InvalidOid; 861 | bool indexOk = false; 862 | Snapshot snapshot = NULL; 863 | int nkeys = 0; 864 | ScanKeyData skey; 865 | 866 | /* 867 | * If checking the cache for a specific function Oid, we need to narrow the heap 868 | * scan by setting a scan key and some other data. 869 | */ 870 | if (functionId != InvalidOid) 871 | { 872 | indexId = ProcedureOidIndexId; 873 | indexOk = true; 874 | snapshot = SnapshotSelf; 875 | nkeys = 1; 876 | ScanKeyInit(&skey, Anum_pg_proc_oid, BTEqualStrategyNumber, F_OIDEQ, ObjectIdGetDatum(functionId)); 877 | } 878 | else if (set_config_oid_cache != NIL) 879 | { 880 | /* No need to re-initialize the cache. We've already been here. */ 881 | return; 882 | } 883 | 884 | /* Go ahead and do the work */ 885 | PG_TRY(); 886 | { 887 | rel = table_open(ProcedureRelationId, AccessShareLock); 888 | sscan = systable_beginscan(rel, indexId, indexOk, snapshot, nkeys, &skey); 889 | 890 | /* 891 | * InvalidOid implies complete heap scan to initialize the 892 | * set_config cache. 893 | * 894 | * If we have a scankey, this should only match one item. 895 | */ 896 | while (HeapTupleIsValid(procTup = systable_getnext(sscan))) 897 | { 898 | set_user_check_proc(procTup, rel); 899 | } 900 | } 901 | PG_CATCH(); 902 | { 903 | systable_endscan(sscan); 904 | table_close(rel, NoLock); 905 | 906 | PG_RE_THROW(); 907 | } 908 | PG_END_TRY(); 909 | 910 | systable_endscan(sscan); 911 | table_close(rel, NoLock); 912 | } 913 | -------------------------------------------------------------------------------- /src/set_user.h: -------------------------------------------------------------------------------- 1 | #ifndef SET_USER_H 2 | #define SET_USER_H 3 | 4 | #include "nodes/pg_list.h" 5 | 6 | typedef struct SetUserHooks 7 | { 8 | void (*post_set_user) (const char *username); 9 | void (*post_reset_user) (); 10 | } SetUserHooks; 11 | 12 | #define SET_USER_HOOKS_KEY "SetUserHooks" 13 | 14 | /* 15 | * register_set_user_hooks 16 | * 17 | * Utility function for registering an extension's implementation of the 18 | * set_user hooks. 19 | * 20 | * Takes in two function pointers, which should be defined in the extension. 21 | * Each subsequent call to this function adds a new hook to the queue. 22 | */ 23 | static inline void register_set_user_hooks(void *set_user_hook, void *reset_user_hook) 24 | { 25 | static List **HooksQueue; 26 | static SetUserHooks *next_hook_entry = NULL; 27 | MemoryContext oldcontext; 28 | 29 | oldcontext = MemoryContextSwitchTo(TopMemoryContext); 30 | 31 | /* Grab the SetUserHooks queue from the rendezvous hash */ 32 | HooksQueue = (List **) find_rendezvous_variable(SET_USER_HOOKS_KEY); 33 | 34 | /* Populate a new hooks entry and append it to the queue */ 35 | next_hook_entry = palloc0(sizeof(SetUserHooks)); 36 | next_hook_entry->post_set_user = set_user_hook; 37 | next_hook_entry->post_reset_user = reset_user_hook; 38 | 39 | *HooksQueue = lappend(*HooksQueue, &next_hook_entry); 40 | MemoryContextSwitchTo(oldcontext); 41 | } 42 | 43 | #endif 44 | -------------------------------------------------------------------------------- /test/Dockerfile.debian: -------------------------------------------------------------------------------- 1 | FROM ubuntu:latest 2 | 3 | # Install packages 4 | RUN apt-get update 5 | RUN DEBIAN_FRONTEND=noninteractive apt-get install -y sudo wget gnupg tzdata locales lsb-release apt-utils make gcc libssl-dev \ 6 | libkrb5-dev 7 | 8 | # PostgreSQL version 9 | ARG PGVER=18 10 | 11 | # Remove the default ubuntu user to reduce the chance of a conflict with the host user 12 | RUN userdel ubuntu 13 | 14 | # Create postgres user/group with specific IDs 15 | ARG UID=1000 16 | ARG GID=1000 17 | 18 | RUN groupadd -g $GID -o postgres 19 | RUN useradd -m -u $UID -g $GID -o -s /bin/bash postgres 20 | 21 | # Add PostgreSQL repository 22 | RUN RELEASE_CODENAME=`lsb_release -c | awk '{print $2}'` && \ 23 | echo 'deb http://apt.postgresql.org/pub/repos/apt/ '${RELEASE_CODENAME?}'-pgdg main '${PGVER?} | \ 24 | tee -a /etc/apt/sources.list.d/pgdg.list 25 | RUN APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=1 && \ 26 | wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | sudo apt-key add - 27 | RUN apt-get update 28 | 29 | # Install PostgreSQL 30 | RUN apt-get install -y postgresql-${PGVER?} postgresql-server-dev-${PGVER?} 31 | 32 | # Create PostgreSQL cluster 33 | ENV PGBIN=/usr/lib/postgresql/${PGVER}/bin 34 | ENV PGDATA="/var/lib/postgresql/${PGVER}/test" 35 | ENV PATH="${PATH}:${PGBIN}" 36 | 37 | RUN sudo -u postgres ${PGBIN?}/initdb -A trust -k ${PGDATA?} 38 | RUN echo "shared_preload_libraries = 'set_user'" >> ${PGDATA}/postgresql.conf 39 | 40 | # Configure sudo 41 | RUN echo 'postgres ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers 42 | 43 | USER postgres 44 | -------------------------------------------------------------------------------- /test/README.md: -------------------------------------------------------------------------------- 1 | # Testing 2 | 3 | Testing is performed using a Docker container. First build the container with the desired PostgreSQL version: 4 | ``` 5 | docker build --build-arg UID=$(id -u) --build-arg GID=$(id -g) --build-arg PGVER=17 -f test/Dockerfile.debian -t set_user-test . 6 | ``` 7 | Then run the test: 8 | ``` 9 | docker run --rm -v $(pwd):/set_user set_user-test /set_user/test/test.sh 10 | ``` 11 | -------------------------------------------------------------------------------- /test/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # Clean and build set_user 5 | make -C /set_user clean all USE_PGXS=1 6 | 7 | # Install set_user so postgres will start with shared_preload_libraries set 8 | sudo bash -c "PATH=${PATH?} make -C /set_user install USE_PGXS=1" 9 | 10 | # Start postgres 11 | ${PGBIN}/pg_ctl -w start -D ${PGDATA} 12 | 13 | # Test set_user 14 | make -C /set_user installcheck USE_PGXS=1 15 | -------------------------------------------------------------------------------- /updates/set_user--1.0--1.1.sql: -------------------------------------------------------------------------------- 1 | /* set-user-1.0--1.1.sql */ 2 | 3 | SET LOCAL search_path to @extschema@; 4 | 5 | -- complain if script is sourced in psql, rather than via ALTER EXTENSION 6 | \echo Use "ALTER EXTENSION set_user UPDATE to '1.1'" to load this file. \quit 7 | 8 | CREATE FUNCTION set_user_u(text) 9 | RETURNS text 10 | AS 'MODULE_PATHNAME', 'set_user' 11 | LANGUAGE C; 12 | 13 | REVOKE EXECUTE ON FUNCTION set_user_u(text) FROM PUBLIC; 14 | -------------------------------------------------------------------------------- /updates/set_user--1.1--1.4.sql: -------------------------------------------------------------------------------- 1 | /* set-user-1.1--1.4.sql */ 2 | 3 | -- complain if script is sourced in psql, rather than via ALTER EXTENSION 4 | \echo Use "ALTER EXTENSION set_user UPDATE to '1.4'" to load this file. \quit 5 | 6 | -- just bumping our version to 1.4. no new features here, so nothing to do. 7 | -------------------------------------------------------------------------------- /updates/set_user--1.4--1.5.sql: -------------------------------------------------------------------------------- 1 | /* set-user-1.4--1.5.sql */ 2 | 3 | -- complain if script is sourced in psql, rather than via ALTER EXTENSION 4 | \echo Use "ALTER EXTENSION set_user UPDATE to '1.5'" to load this file. \quit 5 | 6 | -- just bumping our version to 1.5. no new sql function features here, so nothing to do. 7 | -------------------------------------------------------------------------------- /updates/set_user--1.5--1.6.sql: -------------------------------------------------------------------------------- 1 | /* set-user-1.5--1.6.sql */ 2 | 3 | -- complain if script is sourced in psql, rather than via ALTER EXTENSION 4 | \echo Use "ALTER EXTENSION set_user UPDATE to '1.6'" to load this file. \quit 5 | 6 | -- just bumping our version to 1.6. no new sql function features here, so nothing to do. 7 | -------------------------------------------------------------------------------- /updates/set_user--1.6--2.0.sql: -------------------------------------------------------------------------------- 1 | /* set-user-1.6--2.0.sql */ 2 | 3 | -- complain if script is sourced in psql, rather than via ALTER EXTENSION 4 | \echo Use "ALTER EXTENSION set_user UPDATE to '2.0'" to load this file. \quit 5 | 6 | -- just bumping our version to 2.0. no new sql function features here, so nothing to do. 7 | -------------------------------------------------------------------------------- /updates/set_user--2.0--3.0.sql: -------------------------------------------------------------------------------- 1 | /* set-user-2.0--3.0.sql */ 2 | 3 | SET LOCAL search_path to @extschema@; 4 | 5 | -- complain if script is sourced in psql, rather than via ALTER EXTENSION 6 | \echo Use "ALTER EXTENSION set_user UPDATE to '3.0'" to load this file. \quit 7 | 8 | CREATE FUNCTION @extschema@.set_session_auth(text) 9 | RETURNS text 10 | AS 'MODULE_PATHNAME', 'set_session_auth' 11 | LANGUAGE C STRICT; 12 | 13 | REVOKE EXECUTE ON FUNCTION @extschema@.set_session_auth(text) FROM PUBLIC; 14 | -------------------------------------------------------------------------------- /updates/set_user--3.0--4.0.0.sql: -------------------------------------------------------------------------------- 1 | /* set-user-3.0--4.0.0.sql */ 2 | 3 | -- complain if script is sourced in psql, rather than via ALTER EXTENSION 4 | \echo Use "ALTER EXTENSION set_user UPDATE" to load this file. \quit 5 | 6 | -- just bumping our version to 4.0.0. no new SQL features here, so nothing to do. 7 | -------------------------------------------------------------------------------- /updates/set_user--3.0--4.0.0rc1.sql: -------------------------------------------------------------------------------- 1 | /* set-user-3.0--4.0.0rc1.sql */ 2 | 3 | -- complain if script is sourced in psql, rather than via ALTER EXTENSION 4 | \echo Use "ALTER EXTENSION set_user UPDATE" to load this file. \quit 5 | 6 | -- just bumping our version to 4.0.0rc1. no new SQL features here, so nothing to do. 7 | -------------------------------------------------------------------------------- /updates/set_user--4.0.0--4.0.1.sql: -------------------------------------------------------------------------------- 1 | /* set-user--4.0.0--4.0.1.sql */ 2 | 3 | -- complain if script is sourced in psql, rather than via ALTER EXTENSION 4 | \echo Use "ALTER EXTENSION set_user UPDATE" to load this file. \quit 5 | 6 | -- just bumping our version to 4.0.1. no new SQL features here, so nothing to do. 7 | -------------------------------------------------------------------------------- /updates/set_user--4.0.0rc1--4.0.0.sql: -------------------------------------------------------------------------------- 1 | /* set-user--4.0.0rc1--4.0.0.sql */ 2 | 3 | -- complain if script is sourced in psql, rather than via ALTER EXTENSION 4 | \echo Use "ALTER EXTENSION set_user UPDATE" to load this file. \quit 5 | 6 | -- Allow users that may have installed 4.0.0RC1 to upgrade to 4.0.0 stable 7 | -------------------------------------------------------------------------------- /updates/set_user--4.0.0rc1.sql: -------------------------------------------------------------------------------- 1 | /* set-user--4.0.0rc1.sql */ 2 | 3 | SET LOCAL search_path to @extschema@; 4 | 5 | -- complain if script is sourced in psql, rather than via CREATE EXTENSION 6 | \echo Use "CREATE EXTENSION set_user" to load this file. \quit 7 | 8 | CREATE FUNCTION @extschema@.set_user(text) 9 | RETURNS text 10 | AS 'MODULE_PATHNAME', 'set_user' 11 | LANGUAGE C; 12 | 13 | CREATE FUNCTION @extschema@.set_user(text, text) 14 | RETURNS text 15 | AS 'MODULE_PATHNAME', 'set_user' 16 | LANGUAGE C STRICT; 17 | 18 | REVOKE EXECUTE ON FUNCTION @extschema@.set_user(text) FROM PUBLIC; 19 | REVOKE EXECUTE ON FUNCTION @extschema@.set_user(text, text) FROM PUBLIC; 20 | 21 | CREATE FUNCTION @extschema@.reset_user() 22 | RETURNS text 23 | AS 'MODULE_PATHNAME', 'set_user' 24 | LANGUAGE C; 25 | 26 | CREATE FUNCTION @extschema@.reset_user(text) 27 | RETURNS text 28 | AS 'MODULE_PATHNAME', 'set_user' 29 | LANGUAGE C STRICT; 30 | 31 | GRANT EXECUTE ON FUNCTION @extschema@.reset_user() TO PUBLIC; 32 | GRANT EXECUTE ON FUNCTION @extschema@.reset_user(text) TO PUBLIC; 33 | 34 | /* New functions in 1.1 (now 1.4) begin here */ 35 | 36 | CREATE FUNCTION @extschema@.set_user_u(text) 37 | RETURNS text 38 | AS 'MODULE_PATHNAME', 'set_user' 39 | LANGUAGE C STRICT; 40 | 41 | REVOKE EXECUTE ON FUNCTION @extschema@.set_user_u(text) FROM PUBLIC; 42 | 43 | /* No new sql functions for 1.5 */ 44 | /* No new sql functions for 1.6 */ 45 | /* No new sql functions for 2.0 */ 46 | 47 | /* New functions in 3.0 begin here */ 48 | 49 | CREATE FUNCTION @extschema@.set_session_auth(text) 50 | RETURNS text 51 | AS 'MODULE_PATHNAME', 'set_session_auth' 52 | LANGUAGE C STRICT; 53 | REVOKE EXECUTE ON FUNCTION @extschema@.set_session_auth(text) FROM PUBLIC; 54 | 55 | /* No new sql functions for 4.0.0 */ 56 | -------------------------------------------------------------------------------- /updates/set_user--4.0.1--4.1.0.sql: -------------------------------------------------------------------------------- 1 | /* set-user--4.0.1--4.1.0.sql */ 2 | 3 | -- complain if script is sourced in psql, rather than via ALTER EXTENSION 4 | \echo Use "ALTER EXTENSION set_user UPDATE" to load this file. \quit 5 | 6 | -- just bumping our version to 4.1.0. no new SQL features here, so nothing to do. 7 | --------------------------------------------------------------------------------