https://supabase.io/blog/2021/07/01/roles-postgres-hooks Open main menu Logo Product Developers BetaPricing Start your projectSign in View more posts 2021-07-02*24 minute read Protecting reserved roles with PostgreSQL Hooks [steve-chav] Steve ChavezEngineering [cover-post] postgres Table of contents * Reserved Roles * Extending PostgreSQL with Hooks * Setting up the SupaUtils extension * Initializing the extension * Running the SupaUtils hook * Looking for the reserved role * Testing the extension * Wrapping up PostgreSQL manages permissions through roles. To create these roles, a database user needs the CREATEROLE privilege, which not only allows role creation but also modification of any role (except superusers). At Supabase, we use dedicated roles for each of our customers' backend services. For instance, our Storage API uses the supabase_storage_admin role for connecting to the database. Giving the CREATEROLE privilege to our customers would allow them to drop this role and take their own Storage API down. And yet, we want to give our customers the ability to manage roles the same as they do it in on-premises databases, with the usual CREATE ROLE, ALTER ROLE, and DROP ROLE statements. So, how do we grant them the CREATEROLE privilege and, at the same time, protect our own roles? In this blog post, we explain how we managed to do this by using PostgreSQL Hooks in our SupaUtils extension. Reserved Roles PostgreSQL has a list of predefined roles -- all prefixed with "pg_" -- that cannot be dropped or altered. Attempting to do so will throw an error mentioning that the role is "reserved". 1alter role pg_monitor createdb; 2ERROR: role name "pg_monitor" is reserved 3DETAIL: Cannot alter reserved roles. 4 Copy This mechanism is an internal implementation detail. Unfortunately Postgres doesn't allow us to define our own reserved roles. RDS reserved roles Amazon RDS has a similar defense mechanism, all of its predefined roles -- prefixed with "rds" -- cannot be modified. 1alter role rdsadmin rename to another; 2ERROR: The "rdsadmin" role cannot be renamed. 3DETAIL: The "rdsadmin" role cannot be renamed because either the source 4 or the target name refer to an Amazon RDS reserved role name. 5LOCATION: handle_rename, rdsutils.c:1534 6 Copy Again, the error mentions that the role is "reserved". Also note the rdsutils.c mention. That's not a stock Postgres source file. This means that the logic comes from an RDS extension. We can confirm this is the case by showing the preloaded libraries. 1show shared_preload_libraries; 2 3 shared_preload_libraries 4----------------------------- 5 rdsutils,pg_stat_statements 6 Copy rdsutils can be seen there. Naturally this lead us into thinking we can achieve the same logic with an extension of our own, and thus the SupaUtils idea was born. Extending PostgreSQL with Hooks PostgreSQL hooks allow us to extend internal functionality. Hooks can modify behavior at different places, including when running SQL statements. For example, if we wanted to enforce our own password restrictions whenever a user changes passwords, we could use the check_password_hook to verify the password. We would write out our own Custom Logic, and raise an error if the password fails the password requirements. This is an images of the lifecycle of a Postgres Hook. For SupaUtils, we're particularly interested in the ProcessUtility_hook, which allows us to hook into utility statements: every statement except select, insert, delete or update. They include alter role and drop role, which are the statements we want to hook on. Hooks are global function pointers To use hooks, we can override functions pointers that are global. On the Postgres codebase, the ProcessUtility_hook is basically used^1 like this: 1// src/backend/tcop/utility.c 2 3// ProcessUtility_hook is NULL by default 4ProcessUtility_hook_type ProcessUtility_hook = NULL; 5 6// This function is used for processing all the utility statements 7void 8ProcessUtility(PARAMS_OMITTED_FOR_BREVITY) 9{ 10// call the ProcessUtility_hook if it's not NULL 11if (ProcessUtility_hook) 12 (*ProcessUtility_hook)(PARAMS_OMITTED_FOR_BREVITY); 13// otherwise call the standard function used to process utility statements 14else 15 standard_ProcessUtility(PARAMS_OMITTED_FOR_BREVITY); 16} 17 Copy As you can see, ProcessUtility_hook is NULL by default, so our extension should set it for the hook to run. Also, the standard_ProcessUtility function is the one that actually does the job of creating or modifying the roles (among other things) so our hook should also call it. Loading and running the hook Each extension set in shared_preload_libraries will get its _PG_init function called. This function will allow us to set our hook onto ProcessUtility_hook. Since hooks are global function pointers, it might be the case that another extension modifies the hook pointer (on its own _PG_init) before us and sets its own hook. So we need to ensure we also run this previously-set hook, before or after our own hook runs. It's typically^2 done like this: 1// variable to store the previous hook 2static ProcessUtility_hook_type prev_hook = NULL; 3 4// initialize our extension 5void 6_PG_init(void) 7{ 8 // ProcessUtility_hook has the global function pointer. 9 // Store its value in case another extension already set its own hook. 10 prev_hook = ProcessUtility_hook; 11 // Now override the ProcessUtility_hook with our hook 12 ProcessUtility_hook = our_hook; 13} 14 15static void 16our_hook(PARAMS_OMITTED_FOR_BREVITY) 17{ 18 // our hook logic goes here 19 20 // If there was a previous hook, run it after our hook 21 if (prev_hook) 22 prev_hook(PARAMS_OMITTED_FOR_BREVITY); 23 // If there's no previous hook, call the standard function 24 else 25 standard_ProcessUtility(PARAMS_OMITTED_FOR_BREVITY); 26} 27 Copy Setting up the SupaUtils extension We can use the concepts above to build our extension. First we'll need a Makefile in order to compile the extension code and include it into our PostgreSQL installation. 1# Makefile 2 3# Our shared library 4MODULE_big = supautils 5 6# Our object files to build for the library 7OBJS = src/supautils.o 8 9# Tell pg_config to pass us the PostgreSQL extensions makefile(PGXS) 10# and include it into our own Makefile through the standard "include" directive. 11PG_CONFIG = pg_config 12PGXS := $(shell $(PG_CONFIG) --pgxs) 13include $(PGXS) 14 Copy For the source file, we'll start with variable definitions and functions declarations. 1// src/supautils.c 2 3// include common declarations 4#include "postgres.h" 5 6// required macro for extension libraries to work 7PG_MODULE_MAGIC; 8 9// variable for the previous hook 10static ProcessUtility_hook_type prev_hook = NULL; 11 12// variable for our reserved roles configuration parameter 13static char *reserved_roles = NULL; 14 15// function declaration for extension initialization 16void _PG_init(void); 17 18// function declaration for our hook 19static void supautils_hook( 20 PlannedStmt *pstmt, 21 const char *queryString, 22 ProcessUtilityContext context, 23 ParamListInfo params, 24 QueryEnvironment *queryEnv, 25 DestReceiver *dest, 26 QueryCompletion *completionTag 27); 28 29// function declaration for our pure function that will return a reserved role 30static char* look_for_reserved_role(Node *utility_stmt, List *reserved_role_list); 31 Copy Up next we'll define each one of these function declarations. Initializing the extension Let's now _PG_init our extension. Besides setting the hook here, we want to define our reserved roles as a configuration parameter, that way they can be modified by editing the postgresql.conf file. For this, we can use the DefineCustomStringVariable function, which inserts the parameter into Postgres "Grand Unified Configuration" (GUC) system. 1void 2_PG_init(void) 3{ 4 // Store the previous hook 5 prev_hook = ProcessUtility_hook; 6 // Set our hook 7 ProcessUtility_hook = supautils_hook; 8 9 // Define our "supautils.reserved_roles" parameter 10 // some arguments are unused so they are left as NULL 11 DefineCustomStringVariable("supautils.reserved_roles", 12 "Comma-separated list of roles that cannot be altered or dropped", 13 NULL, 14 // It will be assigned to the reserved_roles variable 15 &reserved_roles, 16 NULL, 17 // We should be able to reload this parameter without restarting the server, 18 // e.g. with "select pg_reload_conf()". 19 PGC_SIGHUP, 20 0, 21 NULL, NULL, NULL); 22} 23 Copy Running the SupaUtils hook Now that our hook is set, we'll define what it will do. As specified in the ProcessUtility_hook_type, the hook's first parameter is a PlannedStmt, this represents the planned statement -- the output from the Postgres planner. This is a step before the statement is executed. We'll look for the presence of a reserved role in the planned statement. If there's one present, we'll report an error and abort the statement execution step. 1static void 2supautils_hook( 3 // The planned statement 4 PlannedStmt *pstmt, 5 // These parameters are here for completion, we'll not use any of them 6 const char *queryString, 7 ProcessUtilityContext context, 8 ParamListInfo params, 9 QueryEnvironment *queryEnv, 10 DestReceiver *dest, 11 QueryCompletion *completionTag 12) 13{ 14 // Get the utility statement from the planned statement 15 Node *utility_stmt = pstmt->utilityStmt; 16 17 // Only do the logic if supautils.reserved_roles is not NULL 18 if(reserved_roles){ 19 // The found reserved role, assume none was found by default 20 char *reserved_role = NULL; 21 // Temp var for storing the list of reserved roles 22 List *reserved_role_list; 23 24 // split the comma-separated string into a List by using a 25 // helper function from varlena.h 26 if (!SplitIdentifierString(pstrdup(reserved_roles), ',', &reserved_role_list)) 27 // abort and report an error if the splitting fails 28 ereport(ERROR, 29 (errcode(ERRCODE_INVALID_PARAMETER_VALUE), 30 errmsg("parameter \"%s\" must be a comma-separated list of " 31 "identifiers", reserved_roles))); 32 33 // look for the reserved role in an internal function 34 reserved_role = look_for_reserved_role(utility_stmt, reserved_role_list); 35 36 // we're done with the list so free it from memory 37 list_free(reserved_role_list); 38 39 // abort and report an error if a reserved role was found 40 if(reserved_role) 41 ereport(ERROR, 42 (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE), 43 errmsg("\"%s\" is a reserved role, it cannot be modified", reserved_role))); 44 } 45 46 // Run the previous hook if defined or call the standard function 47 if (prev_hook) 48 prev_hook(pstmt, queryString, 49 context, params, queryEnv, 50 dest, completionTag); 51 else 52 standard_ProcessUtility(pstmt, queryString, 53 context, params, queryEnv, 54 dest, completionTag); 55} 56 57 Copy Looking for the reserved role Lastly, we'll define how we look for the reserved role. At this stage, we already have the utility statement and the reserved role list. All that's left to do is to define if the utility statement is an ALTER ROLE or DROP ROLE statement, and whether if the role it affects is a reserved one. 1static char* 2look_for_reserved_role(Node *utility_stmt, List *reserved_role_list) 3{ 4 // Check the utility statement type 5 switch (utility_stmt->type) 6 { 7 // Matches statements like: 8 // ALTER ROLE role NOLOGIN 9 case T_AlterRoleStmt: 10 { 11 // cast the utility statement to an alter role statement 12 AlterRoleStmt *stmt = (AlterRoleStmt *) utility_stmt; 13 14 //RoleSpec has the role name plus some attributes 15 RoleSpec *role = stmt->role; 16 17 // postgres defines its own list utilities in pg_list.h 18 // ListCell is an element of the list that we'll use for iteration 19 ListCell *role_cell; 20 21 // pg_list.h includes the foreach macro for iterating over lists 22 foreach(role_cell, reserved_role_list) 23 { 24 // get the list element 25 char *reserved_role = (char *) lfirst(role_cell); 26 27 // compare the statement role with the reserved role 28 // get_rolespec_name will get the RoleSpec role name, 29 // even in cases where the role is the special case of 30 // "current_user" or "session_user" 31 if (strcmp(get_rolespec_name(role), reserved_role) == 0) 32 return reserved_role; 33 } 34 35 break; 36 } 37 38 // Matches statements like: 39 // DROP ROLE role 40 case T_DropRoleStmt: 41 { 42 // cast the utility statement to a drop role statement 43 DropRoleStmt *stmt = (DropRoleStmt *) utility_stmt; 44 ListCell *item; 45 46 // the logic is the same as before, iterate over the reserved role list 47 // and find a match 48 foreach(item, stmt->roles) 49 { 50 RoleSpec *role = lfirst(item); 51 ListCell *role_cell; 52 53 foreach(role_cell, reserved_role_list) 54 { 55 char *reserved_role = (char *) lfirst(role_cell); 56 57 if (strcmp(get_rolespec_name(role), reserved_role) == 0) 58 return reserved_role; 59 } 60 } 61 62 break; 63 } 64 65 default: 66 break; 67 } 68 // Didn't find any reserved role on the statement, so return NULL 69 return NULL; 70} 71 Copy Testing the extension Now that the code is finished, we can test the extension. Since we already have a Makefile, the extension can be installed by doing make && make install. Then, in postgresql.conf: 1# set the extension as preloaded, this will require a restart 2shared_preload_libraries="supautils" 3 4# the reserved roles 5supautils.reserved_roles="supabase_storage_admin, supabase_auth_admin" 6 Copy We'll now try to alter or drop the reserved roles: 1alter role supabase_storage_admin nologin password 'fake'; 2ERROR: "supabase_storage_admin" is a reserved role, it cannot be modified 3 4drop role supabase_auth_admin; 5ERROR: "supabase_auth_admin" is a reserved role, it cannot be modified 6 7-- Success!! 8 Copy Wrapping up As you can see, PostgreSQL Hooks allow us to intercept SQL statements. There are many types of hooks, you can see unofficial documentation for these at AmatanHead/psql-hooks. The full SupaUtils code is in our GitHub repository. By the way, if you like working on PostgreSQL tooling and extensions: we are hiring PostgreSQL experts! --------------------------------------------------------------------- 1. You can see the ProcessUtility_hook declaration here and its usage here.- 2. This is also done on pg_stat_statements and sepgsql.- postgres Table of contents * Reserved Roles * Extending PostgreSQL with Hooks * Setting up the SupaUtils extension * Initializing the extension * Running the SupaUtils hook * Looking for the reserved role * Testing the extension * Wrapping up Related articles Protecting reserved roles with PostgreSQL Hooks Developers stay up to date with intheloop.dev Using Supabase in Replit Postgres as a CRON Server Cracking PostgreSQL Interview Questions View all posts Next post Supabase Beta June 2021 June 2, 2021 release-notes Build in a weekend, scale to millions Start your project Footer Supabase TwitterGitHub Product * Database * Authentication * Storage * FunctionsComing soon * Pricing Resources * Support * Case Studies * System Status * Terms of service Developers * Documentation * API Reference * Guides Company * Blog * Open Source * Humans.txt * Lawyers.txt * Security.txt (c) Supabase Inc Toggle Themes