{"id":1522,"date":"2023-05-31T14:46:54","date_gmt":"2023-05-31T14:46:54","guid":{"rendered":"https:\/\/www.aviator.co\/blog\/?p=1522"},"modified":"2025-09-25T13:50:17","modified_gmt":"2025-09-25T13:50:17","slug":"postgresql-roles-and-privileges-explained","status":"publish","type":"post","link":"https:\/\/www.aviator.co\/blog\/postgresql-roles-and-privileges-explained\/","title":{"rendered":"PostgreSQL roles and privileges explained"},"content":{"rendered":"\n<figure class=\"wp-block-image aligncenter size-large\"><img fetchpriority=\"high\" decoding=\"async\" width=\"1024\" height=\"574\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/postgreSQL-privileges-1024x574.jpg\" alt=\"\" class=\"wp-image-1532\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/postgreSQL-privileges-1024x574.jpg 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/postgreSQL-privileges-300x168.jpg 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/postgreSQL-privileges-768x430.jpg 768w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/postgreSQL-privileges.jpg 1520w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<p>PostgreSQL is one of the most popular <a href=\"https:\/\/www.oracle.com\/database\/what-is-a-relational-database\/\">RDBMS<\/a>. When you use a locally running PostgreSQL, you would just use a single superuser for convenience. When it comes to the production environment, you will want to set up users and permissions properly.<\/p>\n\n\n\n<p>However, while there are many articles on how to insert and query data in PostgreSQL, its access control mechanism is not well explained. This article summarizes how it works as the start guide for PostgreSQL&#8217;s access control.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/docs.aviator.co\/mergequeue\" target=\"_blank\" rel=\" noreferrer noopener\"><img decoding=\"async\" width=\"1024\" height=\"264\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2024\/04\/aviator-mergequeue-blog-documentation-cta-photo-min-1-1024x264.png\" alt=\"MergeQueue CTA\" class=\"wp-image-4921\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2024\/04\/aviator-mergequeue-blog-documentation-cta-photo-min-1-1024x264.png 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2024\/04\/aviator-mergequeue-blog-documentation-cta-photo-min-1-300x77.png 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2024\/04\/aviator-mergequeue-blog-documentation-cta-photo-min-1-768x198.png 768w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2024\/04\/aviator-mergequeue-blog-documentation-cta-photo-min-1-1536x396.png 1536w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2024\/04\/aviator-mergequeue-blog-documentation-cta-photo-min-1.png 1940w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Roles, objects, and privileges<\/h2>\n\n\n\n<p>Like other access control mechanisms, PostgreSQL&#8217;s access control can be explained like &#8220;Role X is allowed to do Y on object Z&#8221;. Here, roles are users and groups, objects are databases, tables, etc., and privileges are actions like <code>SELECT<\/code> or <code>INSERT<\/code>. Conceptually, PostgreSQL&#8217;s ACL entry can be explained as a tuple of <code>(role, object, privilege)<\/code>.<\/p>\n\n\n\n<p>Roles are basically users and groups. It acts like both; you can log in as a role, and a role can belong to another role. Each role has an attribute like <code>LOGIN<\/code> and <code>INHERIT<\/code> that indicate whether you can log in as that role and whether the role inherits privileges from the roles it belongs to. You can add a role to a member of another role by using <code>GRANT ROLE ...<\/code> command.<\/p>\n\n\n\n<p>Objects in PostgreSQL are databases, tables, etc.. There is a tree structure in PostgreSQL objects. A PostgreSQL instance can have multiple databases. A database can have multiple schemas. A schema can have multiple tables.<\/p>\n\n\n\n<p>Privileges are permissions defined over PostgreSQL objects. For example, there is a <code>SELECT<\/code> privilege on tables, which is a permission to run <code>SELECT<\/code> queries on them. Every kind of object has a different set of privileges.<\/p>\n\n\n\n<p>With these elements, you can express access control configuration like &#8220;Role <code>readonly_user<\/code> is allowed to run <code>SELECT<\/code> on <code>accounts<\/code> table&#8221;. You can see the valid combinations of object types and privileges in <a href=\"https:\/\/www.postgresql.org\/docs\/15\/ddl-priv.html\">https:\/\/www.postgresql.org\/docs\/15\/ddl-priv.html<\/a>. You can add or remove the <code>(role, object, privilege)<\/code> tuples with <code>GRANT<\/code> and <code>REVOKE<\/code> commands.<\/p>\n\n\n\n<p>Inheritance happens only between roles, not between objects. Since PostgreSQL objects have a tree structure, you might want to give <code>SELECT<\/code> privilege at the database level, hoping that it gives the <code>SELECT<\/code> privilege to all the tables in the database. PostgreSQL privilege doesn&#8217;t work in such a way.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Object owners<\/h3>\n\n\n\n<p>Each PostgreSQL object has a special role called &#8220;Owner&#8221;. Certain actions like <code>ALTER TABLE<\/code> can be done only by owners, and you cannot <code>GRANT<\/code> such privileges to non-owners.<\/p>\n\n\n\n<p>Sometimes you want to assign more than two owners for an object. Let&#8217;s say, you have two roles, <code>app_user<\/code> and <code>sre_user<\/code>, and you want both users to be able run <code>ALTER TABLE<\/code>, which only the owner can do. Since there can be only one owner per object, you cannot directly make both users to be the owner. At the same time, <code>ALTER TABLE<\/code> is not something you can <code>GRANT<\/code> to roles.<\/p>\n\n\n\n<p>You can use role inheritance to solve this problem. Create <code>table_owner<\/code> role and <code>GRANT table_owner TO app_user, sre_user<\/code>, then transfer the owner role like <code>ALTER TABLE my_table OWNER TO table_owner<\/code>. Now the table owner is <code>table_owner<\/code>, but because <code>app_user<\/code> and <code>sre_user<\/code> are the members of that role, they also have an inherited privilege to run <code>ALTER TABLE<\/code>.<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-large\"><img decoding=\"async\" width=\"1024\" height=\"495\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-10.27.45-AM-1024x495.png\" alt=\"\" class=\"wp-image-1523\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-10.27.45-AM-1024x495.png 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-10.27.45-AM-300x145.png 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-10.27.45-AM-768x372.png 768w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-10.27.45-AM.png 1352w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">There can be only one owner, but other roles can inherit from the owner.<\/figcaption><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Default privileges<\/h3>\n\n\n\n<p>When an object is created, initially only the owner can access that object. For example, if you create a new table, only you can access that table. You will need to grant privileges to other roles separately. This is cumbersome since you need to do this every time you create a new table. PostgreSQL has a feature that allows you to configure the default privileges that are given when an object is newly created.<\/p>\n\n\n\n<p>Let&#8217;s say you want to assign read-only privileges by default to a read-only role for all new tables under a database and schema. In PostgreSQL v14 or later, there is a predefined role <code>pg_read_all_data<\/code> role that allows its members to read all data in all databases, but if you want to restrict it to a certain database, you cannot use this role. We are going to give read-only access to <code>ro_user<\/code> role by using default privileges.<\/p>\n\n\n\n<p>For the existing tables, we can run <code>GRANT SELECT ON ALL TABLES IN SCHEMA public TO ro_user<\/code>. This gives <code>SELECT<\/code> privileges to the existing ones. However, we want to give this privilege to the tables created in the future. To do that, <code>ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO ro_user<\/code>. This changes the default privileges.<\/p>\n\n\n\n<p>Note that these default privileges are applied only when the grantor creates a new table. For example, assume that we have two owner roles <code>table_owner1<\/code> and <code>table_owner2<\/code>. <code>table_owner1<\/code> issues <code>ALTER DEFAULT PRIVILEGES ...<\/code> and the other one doesn&#8217;t have a default privilege. In this case, the issued <code>ALTER DEFAULT PRIVILEGES<\/code> is tied only to <code>table_owner1<\/code> and it&#8217;s applied only when <code>table_owner1<\/code> creates a new table. Even if <code>table_owner2<\/code> creates a new table, it won&#8217;t have default privileges defined by <code>table_owner1<\/code>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Viewing the current ACLs<\/h3>\n\n\n\n<p>With <code>GRANT<\/code> and <code>REVOKE<\/code> commands, you can modify the ACLs explained above, but how can we see the current ACLs? If you use <code>psql<\/code> CLI, there are CLI internal commands that show them:<\/p>\n\n\n\n<figure class=\"wp-block-image aligncenter size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"799\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-10.31.55-AM-1024x799.png\" alt=\"\" class=\"wp-image-1524\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-10.31.55-AM-1024x799.png 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-10.31.55-AM-300x234.png 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-10.31.55-AM-768x600.png 768w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-10.31.55-AM.png 1240w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><figcaption class=\"wp-element-caption\">From https:\/\/www.postgresql.org\/docs\/15\/ddl-priv.html<\/figcaption><\/figure>\n\n\n\n<p>However, you might not have easy access to <code>psql<\/code>; you might have a way to run a read-only query via Redash, Retool, Grafana, etc., but not with <code>psql<\/code>. Even in that case, you can run a <code>SELECT<\/code> query on PostgreSQL internal tables to see the same information as <code>psql<\/code> commands. The <code>psql<\/code> commands also query those PostgreSQL internal tables under the hood, and show them nicely. You can find the <code>psql<\/code>&#8216;s <code>\\dt<\/code> (Show tables) implementation at <a href=\"https:\/\/github.com\/postgres\/postgres\/blob\/f4a9422c0c37ba638adbab853b8badb98a53ce04\/src\/bin\/psql\/describe.c#L3850\">https:\/\/github.com\/postgres\/postgres\/blob\/f4a9422c0c37ba638adbab853b8badb98a53ce04\/src\/bin\/psql\/describe.c#L3850<\/a> and there is a <code>SELECT<\/code> statement there. Here are some examples of privilege queries.<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table><tbody><tr><td>Object type<\/td><td>Query<\/td><\/tr><tr><td>DATABASE<\/td><td>SELECT datname, pg_catalog.pg_get_userbyid(datdba), datacl FROM pg_database;<\/td><\/tr><tr><td>SCHEMA<\/td><td>SELECT nspname, pg_catalog.pg_get_userbyid(nspowner), nspacl FROM pg_namespace;<\/td><\/tr><tr><td>TABLE<\/td><td>SELECT relname, relacl FROM pg_class WHERE relacl IS NOT NULL AND relname NOT LIKE &#8216;pg_%&#8217;;<\/td><\/tr><tr><td>Default Privileges<\/td><td>SELECT pg_catalog.pg_get_userbyid(defaclrole), defaclobjtype, defaultacl FROM pg_default_acl;<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p>Each ACL entry is shown in an abbreviated form, and it looks like <code>arwdDxt<\/code>. Each privilege is shortened to one character. For example <code>r<\/code> in <code>arwdDxt<\/code> is <code>SELECT<\/code>, and <code>w<\/code> is <code>INSERT<\/code>. You can see the mapping in the help document.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"583\" src=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-1.14.49-PM-1024x583.png\" alt=\"\" class=\"wp-image-1540\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-1.14.49-PM-1024x583.png 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-1.14.49-PM-300x171.png 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-1.14.49-PM-768x438.png 768w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-1.14.49-PM-1536x875.png 1536w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/Screenshot-2023-05-31-at-1.14.49-PM.png 1548w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Summary<\/h3>\n\n\n\n<p>PostgreSQL access control mechanism is built based on roles, objects, and privileges. There is a way to automatically set the privileges for the new objects. In order to see the current configuration, you can use <code>psql<\/code> CLI or directly query PostgreSQL internal tables.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><a href=\"https:\/\/www.aviator.co\/\" target=\"_blank\" rel=\"noreferrer noopener\">Aviator<\/a>: Automate your cumbersome merge processes<\/h2>\n\n\n\n<figure class=\"wp-block-image\"><a href=\"https:\/\/www.aviator.co\/\" target=\"_blank\" rel=\"noreferrer noopener\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"727\" src=\"https:\/\/blog.aviator.co\/wp-content\/uploads\/2022\/08\/blog-cta-1024x727.png\" alt=\"\" class=\"wp-image-57\" srcset=\"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/blog-cta-1024x727.png 1024w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/blog-cta-300x213.png 300w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/blog-cta-768x545.png 768w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/blog-cta-1536x1090.png 1536w, https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2022\/08\/blog-cta-2048x1454.png 2048w\" sizes=\"(max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<p>Aviator automates tedious developer workflows by managing git Pull Requests (PRs) and continuous integration test (CI) runs to help your team avoid broken builds, streamline cumbersome merge processes, manage cross-PR dependencies, and handle flaky tests while maintaining their security compliance.<\/p>\n\n\n\n<p>There are 4 key components to Aviator:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>MergeQueue<\/strong>&nbsp;\u2013 an automated queue that manages the merging workflow for your GitHub repository to help protect important branches from broken builds. The Aviator bot uses GitHub Labels to identify Pull Requests (PRs) that are ready to be merged, validates CI checks, processes semantic conflicts, and merges the PRs automatically.<\/li>\n\n\n\n<li><strong>ChangeSets<\/strong>&nbsp;\u2013 workflows to synchronize validating and merging multiple PRs within the same repository or multiple repositories. Useful when your team often sees groups of related PRs that need to be merged together, or otherwise treated as a single broader unit of change.<\/li>\n\n\n\n<li><strong>TestDeck<\/strong>&nbsp;\u2013 a tool to automatically detect, take action on, and process results from flaky tests in your CI infrastructure.<\/li>\n\n\n\n<li><strong>Stacked PRs CLI<\/strong>&nbsp;\u2013 a command line tool that helps developers manage cross-PR dependencies. This tool also automates syncing and merging of stacked PRs. Useful when your team wants to promote a culture of smaller, incremental PRs instead of large changes, or when your workflows involve keeping multiple, dependent PRs in sync.<\/li>\n<\/ol>\n\n\n\n<p><a href=\"https:\/\/www.aviator.co\/\" target=\"_blank\" rel=\"noopener\" title=\"\">Try it for free.<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>This post will explain how the PostgreSQL privilege system works and how to see the current ACLs for various objects.<\/p>\n","protected":false},"author":20,"featured_media":1533,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"inline_featured_image":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[301],"tags":[],"class_list":["post-1522","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tutorials-guides"],"blocksy_meta":{"styles_descriptor":{"styles":{"desktop":"","tablet":"","mobile":""},"google_fonts":[],"version":6}},"acf":[],"aioseo_notices":[],"jetpack_featured_media_url":"https:\/\/www.aviator.co\/blog\/wp-content\/uploads\/2023\/05\/postgreSQL-privileges-1.jpg","post_mailing_queue_ids":[],"_links":{"self":[{"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/posts\/1522","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/users\/20"}],"replies":[{"embeddable":true,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/comments?post=1522"}],"version-history":[{"count":10,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/posts\/1522\/revisions"}],"predecessor-version":[{"id":4961,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/posts\/1522\/revisions\/4961"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/media\/1533"}],"wp:attachment":[{"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/media?parent=1522"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/categories?post=1522"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.aviator.co\/blog\/wp-json\/wp\/v2\/tags?post=1522"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}