{"id":96105,"date":"2023-02-21T21:37:45","date_gmt":"2023-02-21T21:37:45","guid":{"rendered":"https:\/\/www.red-gate.com\/simple-talk\/?p=96105"},"modified":"2026-05-21T15:47:51","modified_gmt":"2026-05-21T15:47:51","slug":"postgresql-basics-object-ownership-and-default-privileges","status":"publish","type":"post","link":"https:\/\/www.red-gate.com\/simple-talk\/featured\/postgresql-basics-object-ownership-and-default-privileges\/","title":{"rendered":"PostgreSQL Object Ownership and Default Privileges: A Practical Guide"},"content":{"rendered":"\n<h2 class=\"wp-block-heading\" id=\"h-executive-summary\">Executive Summary<\/h2>\n\n\n\n<p><strong>When different PostgreSQL users create objects in the same database, ownership and permission problems quickly emerge: one developer creates a table and another gets &#8216;permission denied&#8217; when they try to query it. The root cause is PostgreSQL&#8217;s ownership model &#8211; the role that creates an object is its owner, and object owners control access. The solution involves two concepts: setting object ownership to a shared role (so all team members inherit access), and using ALTER DEFAULT PRIVILEGES to automatically grant permissions on future objects. This article covers both with worked examples for a multi-developer database scenario.<\/strong><\/p>\n\n\n\n<p>In the first security article, <a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/postgresql-basics-roles-and-privileges\/\">PostgreSQL Basics: Roles and Privileges<\/a>, I discussed how roles (users and groups) are created and managed in PostgreSQL Depending on your background with permissions, particularly in other database products, some of those nuances in how permissions work may have been surprising.<\/p>\n\n\n\n<p>Understanding how roles and privileges work in Postgres is key to understanding the next, and often more confusing part of security, <em>object ownership<\/em>. Although there are many privileges that can be assigned in Postgres, object ownership comes with a specific level of privilege that cannot be transferred to other roles. Understanding the implications of this is essential to the management of your database schema, and access to the objects it contains, over time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-who-owns-database-objects\">Who owns database objects?<\/h2>\n\n\n\n<p>In PostgreSQL, the role that creates an object (table, view, function, etc.) becomes the owner. It can be altered after the fact, but initially, the creator is the owner. We can see the owner of objects in the database using <a href=\"https:\/\/www.postgresql.org\/docs\/current\/app-psql.html\">the <code>psql<\/code> interactive terminal<\/a> or querying the <code>pg_catalog<\/code> tables that correspond to the object type.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">SET ROLE user1; --impersonate user1\nCREATE TABLE public.example_tbl (\n  id INT NOT NULL,\n  notes TEXT NULL\n);\nSET ROLE none;<\/pre>\n\n\n\n<p>In psql, use the \u201cdescribe\u201d meta command:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>\\d<\/code><\/pre>\n\n\n\n<p>On a clean database, this will show you (if you have other objects in your database, you may get additional rows of output.):<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"929\" height=\"158\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/02\/a-picture-containing-table-description-automatica.png\" alt=\"A picture containing table\n\nDescription automatically generated\" class=\"wp-image-96106\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>As we can see, the table\u2019s owner is set to <code>user1<\/code> because that role created it.<\/p>\n\n\n\n<p>The <code>\\d<\/code> meta-command in <code>psql <\/code>executes the following query under the covers to show us the list of relations (including the table we created) and who owns each relation.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">SELECT n.nspname as \"Schema\",\n  c.relname as \"Name\",\n  CASE c.relkind \n  \tWHEN 'r' THEN 'table' \n  \tWHEN 'v' THEN 'view' \n  \tWHEN 'm' THEN 'materialized view' \n  \tWHEN 'i' THEN 'index' \n  \tWHEN 'S' THEN 'sequence' \n  \tWHEN 't' THEN 'TOAST table' \n  \tWHEN 'f' THEN 'foreign table' \n  \tWHEN 'p' THEN 'partitioned table' \n  \tWHEN 'I' THEN 'partitioned index' END as \"Type\",\n  pg_catalog.pg_get_userbyid(c.relowner) as \"Owner\"\nFROM pg_catalog.pg_class c\n     LEFT JOIN pg_catalog.pg_namespace n \n         ON n.oid = c.relnamespace\n     LEFT JOIN pg_catalog.pg_am am \n         ON am.oid = c.relam\nWHERE c.relkind IN ('r','p','v','m','S','f','')\n      AND n.nspname &lt;&gt; 'pg_catalog'\n      AND n.nspname !~ '^pg_toast'\n      AND n.nspname &lt;&gt; 'information_schema'\n  AND pg_catalog.pg_table_is_visible(c.oid)\nORDER BY 1,2;<\/pre>\n\n\n\n<p>Tip: As you can see, the catalogs in PostgreSQL contain a lot of useful information. Learning how to query them effectively, however, takes time and experience. This is why the <code>psql<\/code> meta-commands are particularly useful and very popular among PostgreSQL developers. We&#8217;re working on a &#8220;what you need to know about <code>psql<\/code>&#8221; article, but until then, check out <a href=\"https:\/\/psql-tips.org\/\">psql-tips.org<\/a> for some great tips.<\/p>\n\n\n\n<p>I can hear what you\u2019re thinking already.<\/p>\n\n\n\n<p>\u201cYou keep saying that object owners are important, but you haven\u2019t said why that matters! <strong><em>Surely any other role that has appropriate privileges on the same schema can work with that table. Right?<\/em><\/strong>\u201d<\/p>\n\n\n\n<p>It depends.<\/p>\n\n\n\n<p>There are three major points you need to understand about object ownership:<\/p>\n\n\n<div class=\"block-core-list\">\n<ol class=\"wp-block-list\">\n<li>Only a <code>superuser<\/code> or the <code>owner<\/code> of an object (table, function, procedure, sequence, etc.) can <code>ALTER<\/code>\/<code>DROP<\/code> the object.<\/li>\n\n\n\n<li>Only a <code>superuser<\/code> or the <code>owner<\/code> of an object can <code>ALTER<\/code> the ownership of that object.<\/li>\n\n\n\n<li>Only the <code>owner<\/code> of an object can define default privileges for the objects they create.<\/li>\n<\/ol>\n<\/div>\n\n\n<p>Let\u2019s give each of these a look to better describe how ownership and privilege play together, and what you can do to proactively manage this in PostgreSQL.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-setting-up-users-and-groups-for-demonstration\">Setting up users and groups for demonstration<\/h2>\n\n\n\n<p>For the following examples, we will assume that your Postgres instance has the normal superuser principal with the name <code>postgres<\/code>. We\u2019ll then take a cue from the first article to set up two development users and a development group to manage privileges more easily.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">--NOTE: don\u2019t execute test code like this on a cluster that \n--has personal information on it, especially if the cluster \n--can be accessed by the Internet\nCREATE ROLE devgrp WITH NOLOGIN;\nCREATE ROLE dev1 WITH LOGIN PASSWORD 'secretpw' IN ROLE devgrp;\nCREATE ROLE dev2 WITH LOGIN PASSWORD 'secretpw' IN ROLE devgrp;\n-- This will allow our developers to create objects\n-- in the public schema\nGRANT CREATE ON SCHEMA public TO devgrp;\n-- For example purposes only. You should be selective on\n-- privileges based on your needs\nGRANT ALL ON SCHEMA public TO devgrp;\nGRANT ALL ON ALL TABLES IN SCHEMA public TO devgrp;<\/pre>\n\n\n\n<p>Now check with psql that the users are part of the group<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\\du<\/pre>\n\n\n\n<p>This will return:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1432\" height=\"293\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/02\/a-picture-containing-text-description-automatical-1.png\" alt=\"A picture containing text\n\nDescription automatically generated\" class=\"wp-image-96107\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>After running this SQL, the database has two developers that can login to the database, and each is a member of the <code>devgrp<\/code> group role. We\u2019ve granted privileges to the group that allows members to create objects in the public schema and they have all basic DML privileges on all tables.<\/p>\n\n\n\n<p>Now let&#8217;s explore how to overcome a couple of common security issues in PostgreSQL by watching this team of developers begin to implement a new feature.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-problem-1-altering-an-object\">Problem #1: Altering an object<\/h2>\n\n\n\n<p>The first developer is ready to dig into the new project, tracking social media user handles for various networks. To get started, they create a table to store Twitter and Facebook handles.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">-- as the 'postgres' super user we can set the\n-- session to impersonate any role\nSET ROLE dev1;\nCREATE TABLE user_social (\n   user_id INT NOT NULL,\n   twitter_handle TEXT NULL,\n   facebook_handle TEXT NULL );<\/pre>\n\n\n\n<p>In psql, use the &#8220;describe&#8221; meta command:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\\d<\/pre>\n\n\n\n<p>This will return:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1110\" height=\"169\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/02\/a-picture-containing-table-description-automatica-1.png\" alt=\"A picture containing table\n\nDescription automatically generated\" class=\"wp-image-96108\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>As expected, the table was created and is owned by dev1. As the developers get to work on the feature, they quickly realize that a new social network is rising in popularity, and they need to track user handles for it. dev2 offers to add the new column to keep things moving forward.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">SET ROLE dev2;\nALTER TABLE user_social ADD COLUMN mastodon_handle TEXT NULL;<\/pre>\n\n\n\n<p>This will cause the following error to occur:<\/p>\n\n\n\n<p><code>ERROR: must be owner of table user_social<\/code><\/p>\n\n\n\n<p>Hopefully this first, straightforward example helps to clarify why object ownership is so important in your PostgreSQL schema. There is no privilege that can be granted to the second developer which allows them to make modifications to the table. Altering the object is an inherent privilege reserved for the owner (or a superuser).<\/p>\n\n\n\n<p>Most development teams that start using PostgreSQL with roles for each developer hit this problem during migrations and day-to-day development. Because there is no privilege that can be set which allows other roles to alter the object, a different approach needs to be taken.<\/p>\n\n\n\n<p>The most common solution is to set the ownership of all objects to a consistent role and then grant membership in that role to users that need to modify objects. In our example setup a reasonable choice is the <code>devgrp<\/code> role because all developers are members of this role. In a more complex environment and development team structure, you&#8217;ll likely have to create a few more groups to appropriately manage ownership and access. I&#8217;ll provide a starting template of how to manage groups are the end of the article.<\/p>\n\n\n\n<p>To provide an example using our small development team, we can change the owner of this table to a group that all developers are members of, in our case the <code>devgrp<\/code> role. Once the owner is changed, <code>dev2<\/code> should be able to ALTER it because they are members of the group.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">-- as the 'postgres' superuser OR the object owner\nALTER TABLE user_social OWNER TO devgrp;\nSET ROLE dev2;\nALTER TABLE user_social ADD COLUMN mastodon_handle TEXT NULL;\nSELECT * FROM user_social;<\/pre>\n\n\n\n<p>The output of this query is:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1376\" height=\"136\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/02\/word-image-96105-4.png\" alt=\"\" class=\"wp-image-96109\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>Alternatively, you can temporarily set the role of your session to the common owner role <strong>before<\/strong> creating the object (assuming you are a member of that role). Any objects that are created will be owned by the role in force at the time of creation. To demonstrate, I&#8217;ll drop the table and try the same process again, but this time setting the role before creating the table.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">-- as dev1 or 'postgres' superuser\nDROP TABLE user_social;\n-- as dev1 or the 'postgres' superuser we can set the\n-- session to impersonate the devgrp role\nSET ROLE devgrp;\nCREATE TABLE user_social (\n   user_id INT NOT NULL,\n   twitter_handle TEXT NULL,\n   facebook_handle TEXT NULL );<\/pre>\n\n\n\n<p>In psql, use the &#8220;describe&#8221; meta command:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\\d<\/pre>\n\n\n\n<p>Now you will see:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1236\" height=\"209\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/02\/chart-line-chart-description-automatically-gener.png\" alt=\"Chart, line chart\n\nDescription automatically generated\" class=\"wp-image-96110\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>And now, as the second dev user<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">SET ROLE dev2;\nALTER TABLE user_social ADD COLUMN mastodon_handle TEXT NULL;\nSELECT * FROM user_social;\nSET ROLE none; --otherwise, very easy to forget your role context<\/pre>\n\n\n\n<p>This returns:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1376\" height=\"136\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/02\/word-image-96105-6.png\" alt=\"\" class=\"wp-image-96111\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>The key here is that ownership is an essential component in maintaining and altering objects in a PostgreSQL database. Whether you set the roles before creating objects or alter ownership after the fact, only members of the role that own an object can alter the object.<\/p>\n\n\n\n<p>Lesson learned, we can now move on to the second common problem many teams run into when they are working with multiple logins in PostgreSQL &#8211; default object privileges.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-problem-2-default-object-privileges\">Problem #2: Default Object Privileges<\/h2>\n\n\n\n<p>We solved our first problem by setting the table owner to a role that all developers are members of. Essentially, the owner of an object is analogous to a superuser of that object.<\/p>\n\n\n\n<p>But what happens when we add a new role to the database that will be used for reporting or read-only purposes?<\/p>\n\n\n\n<p>The development team has decided that a new role is needed to facilitate reporting functionality for some of the data that will be generated by this new feature.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">-- As superuser or a role that has CREATEROLE attribute\nCREATE ROLE rptusr WITH LOGIN PASSWORD 'secretpw';\n\n-- Set the session to the new role\nSET ROLE rptusr;\n\n-- Count the number of users that have handles for Mastodon\nSELECT count(*) FROM user_social \nWHERE mastodon_handle IS NOT NULL;<\/pre>\n\n\n\n<p>This causes the error:<\/p>\n\n\n\n<p><code>ERROR:  permission denied for table user_social<\/code><\/p>\n\n\n\n<p>This shouldn&#8217;t be much of a surprise given what we&#8217;ve learned so far. The new <code>rptusr<\/code> role was created after the table existed and hasn&#8217;t been granted any privileges to the table. The superuser or owner of an object must specifically grant the necessary privilege.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">-- As a superuser or owner of the required object\nGRANT SELECT ON TABLE user_social TO rptusr;\n\n-- Set the session to rptusr role\nSET ROLE rptusr;\n\n-- Count the number of users that have handles for Mastodon\nSELECT count(*) FROM user_social \nWHERE mastodon_handle IS NOT NULL;\nSET ROLE none;<\/pre>\n\n\n\n<p>This returns:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1044\" height=\"85\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/02\/word-image-96105-7.png\" alt=\"\" class=\"wp-image-96112\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>In the first article in this series, we referred to the process of ensuring users have only the minimum right necessary as the Principle of Least Privilege. Setting permissions, object by object, will quickly become a tiresome task.<\/p>\n\n\n\n<p>Adding a group role doesn&#8217;t help either because the same problem will exist. Privileges are only granted for objects in existence at the time of the <code>GRANT<\/code>. Stated another way, <code>GRANT<\/code> is not a forward-looking action. Instead, we need a way to have PostgreSQL apply privileges every time an object is created.<\/p>\n\n\n\n<p>Enter <a href=\"https:\/\/www.postgresql.org\/docs\/current\/sql-alterdefaultprivileges.html\">default privileges<\/a>.<\/p>\n\n\n\n<p>Each role can create a set of default access privileges that are applied whenever they create an object <em>in a specific database<\/em>. This gives complete control to each role, ensuring that objects are created with the correct privileges each time.<\/p>\n\n\n\n<p>To illustrate, let&#8217;s create new default access privileges <em>before<\/em> creating another new table that the <code>rptuser<\/code> should be able to query.<\/p>\n\n\n\n<p>First, check that there are no default access privileges using psql:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\\ddp<\/pre>\n\n\n\n<p>On my demo server, this shows no default access privileges.<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1077\" height=\"107\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/02\/word-image-96105-8.png\" alt=\"\" class=\"wp-image-96113\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>Next, we will set security context to the group that you want to set default privileges for, which will be applied when they create new objects in certain schemas.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">-- As the role that will create objects, create \n-- default privileges\nSET ROLE devgrp;\nALTER DEFAULT PRIVILEGES IN SCHEMA public\n\tGRANT SELECT ON TABLES TO rptusr;\nSET ROLE none;<\/pre>\n\n\n\n<p>Once again, check to see if the default privilege was created:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\\ddp<\/pre>\n\n\n\n<p>This returns:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1511\" height=\"248\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/02\/diagram-description-automatically-generated-with.png\" alt=\"Diagram\n\nDescription automatically generated with medium confidence\" class=\"wp-image-96114\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>We can see that the default access privilege was created which will grant <code>SELECT<\/code> (read) privilege to the <code>rptusr<\/code> role for any tables that are created in the <code>public<\/code> schema. To verify that it works, we can now create a new table and attempt to select from it as <code>rptusr<\/code> without additional intervention using <code>GRANT<\/code> statements.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">-- As the devgrp role that will own the table\nSET ROLE devgrp;\nCREATE TABLE rpt_log (\nid int NOT NULL,\nrpt_date timestamptz NOT NULL,\nnotes TEXT null\n);\nSET ROLE rptusr;\n-- select from the table to verify that privileges \n-- were applied correctly\nSELECT * FROM rpt_log;<\/pre>\n\n\n\n<p>This returns:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1067\" height=\"73\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/02\/word-image-96105-10.png\" alt=\"\" class=\"wp-image-96115\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>Success! The <code>devgrp<\/code> was able to create the table and the new <code>rptusr<\/code> was able to select from it without error. Moving forward, as long as the <code>devgrp<\/code> is the one to create and own tables (our example object), the <code>rptusr<\/code> will be able to select from them.<\/p>\n\n\n\n<p>Unfortunately, we&#8217;ve only solved our problem for this one &#8220;read-only&#8221; role named rptusr. As soon as another read-only user needs access to database objects, we&#8217;ll have to grant privileges to existing tables and then create another default access privilege for future actions. That&#8217;s not very sustainable and simply highlights what we discussed in the first article.<\/p>\n\n\n\n<p>One common approach to deal with is to create a read-only group role and set default access privileges for it. Then, as new read-only users are created in the database, they can be granted membership into the read-only group role, inheriting the same privileges.<\/p>\n\n\n\n<p>First, check the current default access privileges using <code>psql<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\\ddp<\/pre>\n\n\n\n<p>This returns:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1504\" height=\"201\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/02\/table-description-automatically-generated-with-me-4.png\" alt=\"Table\n\nDescription automatically generated with medium confidence\" class=\"wp-image-96116\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>Now we will undo the current configuration before creating the new one.<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">-- REVOKE the current default access privilege for the single user\nALTER DEFAULT PRIVILEGES IN SCHEMA public\n\tREVOKE SELECT ON TABLES FROM rptusr;<\/pre>\n\n\n\n<p>Check that the access privilege was removed:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\\ddp<\/pre>\n\n\n\n<p>This will return:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"1067\" height=\"112\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/02\/word-image-96105-12.png\" alt=\"\" class=\"wp-image-96117\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>Now create the new group, and security setup:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">-- Create a new read only group role\nCREATE ROLE read_only WITH NOLOGIN;\n\n-- Grant select on all current tables in public schema\n-- Remember: this is just for current tables, not future ones\nGRANT SELECT ON ALL TABLES IN SCHEMA public TO read_only;\n\n-- Grant membership to the read_only role\nGRANT read_only TO rptusr;\n\n-- Now create the same default access privilege for \n-- As the role that will create objects, create \n-- default privileges\nSET ROLE devgrp;\nALTER DEFAULT PRIVILEGES IN SCHEMA public\n\tGRANT SELECT ON TABLES TO read_only;<\/pre>\n\n\n\n<p>Once again, check to see if the default privilege was created:<\/p>\n\n\n\n<pre class=\"wp-block-preformatted\">\\ddp<\/pre>\n\n\n\n<p>This will return the following:<\/p>\n\n\n\n<figure class=\"wp-block-image\"><img loading=\"lazy\" decoding=\"async\" width=\"915\" height=\"181\" src=\"https:\/\/www.red-gate.com\/simple-talk\/wp-content\/uploads\/2023\/02\/diagram-description-automatically-generated-with-1.png\" alt=\"Diagram\n\nDescription automatically generated with low confidence\" class=\"wp-image-96118\"\/><\/figure>\n\n\n\n<p><\/p>\n\n\n\n<p>At this point, any tables that the <code>devgrp<\/code> user creates will have this default access privilege applied and all members of the <code>read_only<\/code> role will be able to select data.<\/p>\n\n\n\n<p>With regards to read-only users, PostgreSQL 14+ does provide new default roles to more easily manage access to table data. There are enough nuances to setting it up correctly in a multi-tenant environment that it warrants a separate article in the near future.<\/p>\n\n\n\n<div id=\"callout-block_1e94cf5bec4bdc7e626246ce3f99bc6a\" class=\"callout alignnone\">\n    <div class=\"child-last:mb-0 child-first:mt-0 bg-gray-50 dark:bg-gray-950 p-4xl my-3xl\">\n\n<p><strong>You may also be interested in:<\/strong><br><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/sql-server\/t-sql-programming-sql-server\/tracking-underlying-object-changes-in-view\/\">Tracking SQL Server view changes and stale views &#8211; a parallel problem across database platforms<\/a><\/p>\n\n<\/div>\n<\/div> \n\n\n<h2 class=\"wp-block-heading\" id=\"h-managing-ownership-and-privileges-at-scale\">Managing ownership and privileges at scale<\/h2>\n\n\n\n<p>Between these first two articles we&#8217;ve covered a lot of ground on PostgreSQL roles and security.<\/p>\n\n\n<div class=\"block-core-list\">\n<ul class=\"wp-block-list\">\n<li>PostgreSQL has roles. Users and groups are synonyms.<\/li>\n\n\n\n<li>By convention, user roles can login while group roles cannot.<\/li>\n\n\n\n<li>Superusers bypass all privilege checks and can access or modify any object and cluster setting.<\/li>\n\n\n\n<li>Access to data and objects must be specifically granted (or revoked) for every role, except the object owner or a superuser.<\/li>\n\n\n\n<li>Roles can be granted the permissions of other roles through inheritance.<\/li>\n\n\n\n<li>All roles have automatic, non-revokable membership in the <code>PUBLIC<\/code> role<\/li>\n\n\n\n<li>Owners of an object (or superusers) are the only role that can <code>ALTER<\/code> or <code>DROP<\/code> the object.<\/li>\n\n\n\n<li>Each role can create default access privileges for any objects they create (like tables) so that other roles have access automatically, rather than applying privileges each time an object is created.<\/li>\n<\/ul>\n<\/div>\n\n\n<p>That&#8217;s a lot of information to think about and manage if PostgreSQL security is new to you, making it challenging to manage PostgreSQL privileges in a larger team. Having a plan or starting template in place for managing PostgreSQL privileges and object ownership as early as possible is a helpful first step. Many times, I&#8217;ve seen teams begin a project without understanding the long-term implications of not having a security plan, often because they didn&#8217;t fully understand the things we&#8217;ve been discussing.<\/p>\n\n\n\n<p>That said, here&#8217;s a sample role and security setup to consider as you get started. Use your knowledge of the things we&#8217;ve discussed so far (and the <a href=\"https:\/\/www.postgresql.org\/docs\/current\/user-manag.html\">official documentation<\/a>), mixed with the requirements of your organization, to modify and craft a scheme that&#8217;s right for you.<\/p>\n\n\n\n<p>In the next article, we&#8217;ll look at how you can put all of this together to create a set of privilege templates each time you create a new database. This will ensure that you have consistent, reproducible access privileges across databases and roles.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\" id=\"h-conclusion\">Conclusion<\/h2>\n\n\n\n<p>PostgreSQL has a security and ownership model that can be confusing to new users. Understanding how roles are created, privileges are assigned, and how to prepare your database for consistent access as time marches on will help you use PostgreSQL effectively and keep your teams working without friction.<\/p>\n\n\n\n<div id=\"callout-block_1e94cf5bec4bdc7e626246ce3f99bc6a\" class=\"callout alignnone\">\n    <div class=\"child-last:mb-0 child-first:mt-0 bg-gray-50 dark:bg-gray-950 p-4xl my-3xl\">\n\n<p><strong>You may also be interested in:<\/strong><br><a href=\"https:\/\/www.red-gate.com\/simple-talk\/databases\/postgresql\/saving-data-historically-with-temporal-tables-part-1-queries\/\">PostgreSQL temporal tables: saving data historically in PostgreSQL<\/a><\/p>\n\n<\/div>\n<\/div> \n\n\n<section id=\"faq\" class=\"faq-block my-5xl\">\n    <h2>FAQs: PostgreSQL Basics: Object Ownership and Default Privileges<\/h2>\n\n                        <h3 class=\"mt-4xl\">1. What does ALTER DEFAULT PRIVILEGES do in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p>ALTER DEFAULT PRIVILEGES configures the permissions that will automatically be granted on objects created in the future by a specified role. Without it, every new table, function, or sequence created by user A is owned by user A &#8211; and other users have no access until explicitly granted. Example: ALTER DEFAULT PRIVILEGES FOR ROLE developer_role IN SCHEMA public GRANT SELECT ON TABLES TO read_group; After this, any table created by developer_role in the public schema will automatically have SELECT granted to read_group &#8211; no per-object GRANT statements needed.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">2. Why does PostgreSQL show &#039;permission denied&#039; even though I can see the table?<\/h3>\n            <div class=\"faq-answer\">\n                <p>PostgreSQL has two levels of permission: schema access (USAGE on the schema) and object access (e.g., SELECT on the table). A user needs both. Common causes: the user has CONNECT to the database and can see the table name in pg_tables but doesn&#8217;t have USAGE on the schema; or the user has schema USAGE but the table wasn&#8217;t created with their access in mind. Fix with: GRANT USAGE ON SCHEMA public TO user; GRANT SELECT ON TABLE tablename TO user; Use ALTER DEFAULT PRIVILEGES so new objects automatically have appropriate access.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">3. How do I change object ownership in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p>Use ALTER TABLE, ALTER FUNCTION, or the appropriate ALTER command: ALTER TABLE tablename OWNER TO new_owner_role; or use REASSIGN OWNED BY old_role TO new_role to transfer all objects owned by one role to another. PostgreSQL also has REASSIGN OWNED which handles all object types at once. To change ownership when creating an object, the creating user must be a member of the target owner role &#8211; you cannot assign an object to a role you&#8217;re not a member of.<\/p>\n            <\/div>\n                    <h3 class=\"mt-4xl\">4. What is the difference between GRANT and ALTER DEFAULT PRIVILEGES in PostgreSQL?<\/h3>\n            <div class=\"faq-answer\">\n                <p>GRANT applies permissions to existing objects &#8211; it takes effect immediately on the named tables, functions, or sequences. ALTER DEFAULT PRIVILEGES applies to objects that will be created in the future by a specified role &#8211; it has no effect on existing objects. You typically need both: GRANT ON ALL TABLES IN SCHEMA public TO role for existing objects, and ALTER DEFAULT PRIVILEGES to cover future objects. Without the default privileges step, teams must run GRANT statements after every DDL change.<\/p>\n            <\/div>\n            <\/section>\n","protected":false},"excerpt":{"rendered":"<p>Solve PostgreSQL permission problems: understand object ownership, the ALTER DEFAULT PRIVILEGES command, and how to configure permissions so multiple developers can work in the same database without constant GRANT statements.&hellip;<\/p>\n","protected":false},"author":341037,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[53,143534],"tags":[158982,159066],"coauthors":[158981],"class_list":["post-96105","post","type-post","status-publish","format-standard","hentry","category-featured","category-postgresql","tag-planetpostgresqlryanbooz","tag-postgresql-101-webinar-sidebar"],"acf":[],"_links":{"self":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/96105","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/users\/341037"}],"replies":[{"embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/comments?post=96105"}],"version-history":[{"count":8,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/96105\/revisions"}],"predecessor-version":[{"id":110851,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/posts\/96105\/revisions\/110851"}],"wp:attachment":[{"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/media?parent=96105"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/categories?post=96105"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/tags?post=96105"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/www.red-gate.com\/simple-talk\/wp-json\/wp\/v2\/coauthors?post=96105"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}