Postgres permission to select from information_schema tables - postgresql

I'm trying to lock down the user permissions used by an application to connect to its Postgres database. The idea is that the application just needs to access data but not create or drop tables. I created a role called readwrite and assigned the role to the user. I configured the role like this:
CREATE ROLE readwrite;
GRANT CONNECT ON DATABASE corre TO readwrite;
GRANT USAGE, CREATE ON SCHEMA public TO readwrite;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO readwrite;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO readwrite;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA public TO readwrite;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT USAGE ON SEQUENCES TO readwrite;
I’ve discovered that the role breaks a specific select that's done in a trigger function. The select is:
SELECT c.column_name::text
FROM information_schema.table_constraints tc
JOIN information_schema.constraint_column_usage AS ccu USING (constraint_schema, constraint_name)
JOIN information_schema.columns AS c ON c.table_schema = tc.constraint_schema
AND tc.table_name = c.table_name AND ccu.column_name = c.column_name
WHERE constraint_type = 'PRIMARY KEY' and tc.table_name = TG_TABLE_NAME;
This is to find out the name of the PK column of the table. The select works fine for the user postgres because it’s an admin. It returns a single row (I don't have any composite PKs). If I run the select as a user with the readwrite role, it runs but returns no rows.
I think I need to grant the role some additional permission for the select to work but I have no idea which one.
Any ideas how I can get this to work as intended?
UPDATE: I originally noticed the issue on Postgres 10.6 but I've also confirmed the same behavior on 11.5
UPDATE 2: Breaking down the select above, the role can't see any rows in information_schema.constraint_column_usage. It also misses a handful of rows in the other two tables (compared to selecting as the admin user postgres) but they don't seem relevant. I tried granting REFERENCES permission but that didn't make any difference:
GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON ALL TABLES IN SCHEMA public TO readwrite;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES ON TABLES TO readwrite;

Just a side note about reworking default privileges. I might be wrong (someone please correct me), but I remember getting erratic results if I didn't REVOKE defaults before resetting them. GRANTS can be applied in so many places, and they interact in some (at least to me) confusing ways. So, I strip everything down to the metal, and then build it up again. It's been months since I looked at this, but I ended up having to write a script to build up all of the GRANTS statements, here's a sample:
------------------------------------------------------------------------------
-- REVOKE ALL on each schema.
------------------------------------------------------------------------------
REVOKE ALL PRIVILEGES ON SCHEMA api FROM PUBLIC; -- -- Clear out the magic PUBLIC pseudo-user.
REVOKE ALL PRIVILEGES ON SCHEMA api FROM group_admins;
REVOKE ALL PRIVILEGES ON SCHEMA api FROM group_api_users;
REVOKE ALL PRIVILEGES ON SCHEMA api FROM group_developers;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM PUBLIC;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM group_admins;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM group_api_users;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM group_developers;
------------------------------------------------------------------------------
-- GRANT USAGE on each schema and CREATE selectively.
-- Note: The api group only gets access to the api schema.
------------------------------------------------------------------------------
GRANT USAGE, CREATE ON SCHEMA api TO group_admins;
GRANT USAGE ON SCHEMA api TO group_api_users;
GRANT USAGE, CREATE ON SCHEMA api TO group_developers;
------------------------------------------------------------------------------
-- REGRANT tables/views.
------------------------------------------------------------------------------
-- REVOKE ALL on tables/views.
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM PUBLIC;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM group_admins;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM group_api_users;
REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA api FROM group_developers;
-- GRANT rights that can be applied to all tables/views in a schema.
GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES, TRIGGER, TRUNCATE ON ALL TABLES IN SCHEMA api TO group_admins;
GRANT SELECT ON ALL TABLES IN SCHEMA api TO group_api_users;
GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES, TRIGGER ON ALL TABLES IN SCHEMA api TO group_developers;
-- GRANT full CRUD rights selectively by table.
-- Note: group_admins and group_developers are granted full CRUD rights on all tables above.
-- Snip
------------------------------------------------------------------------------
-- REGRANT DEFAULT privileges
------------------------------------------------------------------------------
-- Clear any existing table defaults from each schema.
ALTER DEFAULT PRIVILEGES IN SCHEMA api REVOKE ALL PRIVILEGES ON TABLES FROM PUBLIC;
ALTER DEFAULT PRIVILEGES IN SCHEMA api REVOKE ALL PRIVILEGES ON TABLES FROM group_admins;
ALTER DEFAULT PRIVILEGES IN SCHEMA api REVOKE ALL PRIVILEGES ON TABLES FROM group_api_users;
ALTER DEFAULT PRIVILEGES IN SCHEMA api REVOKE ALL PRIVILEGES ON TABLES FROM group_developers;
-- ALTER DEFAULT PRIVILEGES that can be applied to all tables/views in a schema
ALTER DEFAULT PRIVILEGES IN SCHEMA api GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES, TRIGGER, TRUNCATE ON TABLES TO group_admins;
ALTER DEFAULT PRIVILEGES IN SCHEMA api GRANT SELECT ON TABLES TO group_api_users;
ALTER DEFAULT PRIVILEGES IN SCHEMA api GRANT SELECT, INSERT, UPDATE, DELETE, REFERENCES, TRIGGER ON TABLES TO group_developers;
After that, I tend to double-check table and view rights. Here's a function I adapted from code I found to summarize table grants:
CREATE OR REPLACE FUNCTION data.show_table_rights(t_name text)
RETURNS TABLE("Table_Name" name, "User_Name" name, "SELECT" text, "INSERT" text, "UPDATE" text, "DELETE" text, "TRUNCATE" text, "REFERENCES" text, "TRIGGER" text)
LANGUAGE sql
STABLE
AS $function$
SELECT
t.tablename,
u.usename,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'select') = TRUE then 'X' ELSE ' ' END AS select,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'insert')= TRUE then 'X' ELSE ' ' END AS insert,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'update') = TRUE then 'X' ELSE ' ' END AS update,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'delete') = TRUE then 'X' ELSE ' ' END AS delete,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'truncate') = TRUE then 'X' ELSE ' ' END AS truncate,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'references') = TRUE then 'X' ELSE ' ' END AS references,
CASE WHEN has_table_privilege(u.usename, concat(schemaname, '.', t.tablename), 'trigger') = TRUE then 'X' ELSE ' ' END AS trigger
FROM pg_tables t,
pg_user u
WHERE t.tablename = t_name
ORDER BY u.usename;
$function$
I don't love that function...but I don't hate it enough that I ever get around to rewriting it. (The bits I hate are my fault, not whoever I adapted it from.) If I were to rewrite it, I'd get rid of the uppercase column titles and make the input a regclass. Live and learn. Anyway, to call it:
select * from show_table_rights('item');
That spits out a cross-tab with rolls down the left and rights as columns. I've got one for views too. The difference there is that you're joining against and use pg_views instead of pg_tables. I see that I've got versions for schema and database rights, but rarely ever use those.

Another side note since GRANTs and DEFAULT grants are coming up. I got rid of the public schema pretty early in the piece. I found the default behaviors there...confusing. Plus, thinking about a space where multiple users can share in that way feels really easy to screw up. Later, a privilege escalation CVE that hinged on PUBLIC popped up...so I was glad to have gotten rid of the public schema.
I love Postgres, but don't really grok the permissions system fully. Grants can be set at the database, schema, table, and more...and then on the user (roll) who may inherit from other rolls. With the object hierarchy, the grants are cumulatively restrictive. So, you can be granted SELECT on a table, but that's meaningless unless you have USAGE on the database and access to the schema. On the other hand, inherited roll privileges are additive. So, rights are a restrictive funnel on the database-schema-table side, and an expansive (more permissive) system on the rolls side. If I've got that right, it's a fundamentally confusing design. Hence my code to spit out ~1,000 GRANTs to rebuild everything. And I've got rolls for users and groups (rolls with no log on) and try to put all of the rights into the groups.
Chances are, I'm missing something obvious. My setup has the distinct smell of something where you keep pouring code on top until it stops moving because you don't understand the system well enough to do the simple thing. Granted ;-) I'll circle back to it eventually. Postgres is huge, I'm constantly studying as tasks come to hand, but there are only so many days in the week.
The best piece I remember running into on GRANTs in Postgres is this one:
https://illuminatedcomputing.com/posts/2017/03/postgres-permissions/

Too long an answer for comments...
I've never used row- or column-level security features.There are some incredibly knowledgeable people here who seem to monitor the questions 24/7. I'd be curious if anyone else could comment.
I did look into row-level security for a multi-tenant setup. I remember coming to the conclusion that it is complicated. I figured that I'd use views as they're simple to review, edit, and understand. And you can find them in any SQL database. The row level security features I found...more complicated to understand, more hidden under the hood, and a bit Postgres-specific. With that said it's a super cool idea. As I understand it, you're bolting a policy (filter) onto a base table, and then that rule flows through to any view, etc. automatically. You can't subvert it if you try:
Here are some articles I found useful when looking into this:
https://www.2ndquadrant.com/en/blog/application-users-vs-row-level-security/
https://www.citusdata.com/blog/2018/04/04/raw-sql-access-with-row-level-security/
https://medium.com/#cazzer/practical-application-of-row-level-security-b33be18fd198
https://info.crunchydata.com/blog/a-postgresql-row-level-security-primer-creating-large-policies
Here's a simple policy to limit users to viewing scans by their department:
create policy filter_scan
on data.scan
using (department_id = user_get_department_id(current_user));
The content of the policy above is the USING clause. It' a WHERE clause, so far as I can see. You can also add an optional CHECK clause on some operations.
Nearly every example I found assumes you're filtering by current_user() and that there is a column to match. The RLS system is based on roles, so this makes sense. And while it makes sense to use the role name, it's not realistic to assume you'll have such a matching column in every table. Or, for that matter, that such a column would even make sense. Examples commonly use chat systems, etc. where the current user is a care part of the data. In my organization's case, that rarely makes sense. (We're tracking physical objects, users are just slow, error-prone peripherals.) Hence the fake stored function call to user_get_department_id. The idea here is that you've got some utility tables to map roles to specific IDs or other attributes for specific tables. Then a function such as user_get_department_id or perhaps user_get_visible_department_ids queries the utility table for the user's allowed ID or IDs and returns them as a list. Slick!
But, like I said, I only ever tested this out on scratch tables and threw it all away. Views seem good enough for the small number of tables, etc. we're dealing with. For folks with multi-tenant setups with 10,000 clients, life would be different. (The Citus folks suggest splitting the tables physically into different databases, as they would.)

Related

Create readonly user on all 1000 postgres databases and schema's

Using Vault I'm trying to create an on-demand temporary read-only users in a Postgres 11.8 instance.
I will have:
1000+ databases (one per customer aka tenant)
Each database has a public and reporting schema.
So I'm trying to find a way to grant this read-only user access to every database, and on all tables in both schemas.
While I came up with the following snippet:
-- Create a new user
CREATE ROLE "my-readonly-user" WITH LOGIN PASSWORD 'test123';
-- Grant access to the two schema's we have
GRANT USAGE ON SCHEMA public TO "my-readonly-user";
GRANT USAGE ON SCHEMA reporting TO "my-readonly-user";
-- Grant access to all tables in our two schema's
GRANT SELECT ON ALL TABLES IN SCHEMA public TO "my-readonly-user";
GRANT SELECT ON ALL TABLES IN SCHEMA reporting TO "my-readonly-user";
-- Grant access to sequences
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO "my-readonly-user";
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA reporting TO "my-readonly-user";
-- Grant access to future tables
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO "my-readonly-user";
ALTER DEFAULT PRIVILEGES IN SCHEMA reporting GRANT SELECT ON TABLES TO "my-readonly-user";
It will only apply to 1 single database (the current one). If I login with the readonly user and switch to some databases, I don't see any tables except in the first database.
A cluster holds many databases, which hold many schemas. Schemas (even
with the same name) in different DBs are unrelated. Granting
privileges for a schema only applies to this particular schema in the
current DB (the current DB at the time of granting).
https://stackoverflow.com/a/24923877/1409047
Because of that limitation, it seems to make the grant snippet from above more complex. Should I iterate all my databases somehow and run the snippet? How would I do the database switch? Is it even possible in plain SQL (as required by Vault's API)? Anyone did this before?
Note: doing the same in MySQL only requires 2 lines of code, using wildcards *.* which is not supported in Postgres afaik:
CREATE USER '{{name}}'#'10.0.0.0/255.0.0.0' IDENTIFIED BY '{{password}}';
GRANT SELECT, SHOW DATABASES, SHOW VIEW ON *.* TO '{{name}}'#'10.0.0.0/255.0.0.0';
It is a deliberate design decision that an SQL statement cannot affect objects in a database different from the one that you are connected to.
Yes, you will have to iterate through all databases in your cluster and run your script there.
Note that there is a mistake in your script: you shouldn't grant a read-only user USAGE on sequences, else they can modify the sequence value. SELECT is fine.
What I would do is create a read_only_group (with NOLOGIN) and grant all these permissions to that role. Then, when there is a request for a read-only user, create a user and add it to that group, so that it inherits the group privileges. Don't grant anything to the user itself, so that you can easily DROP it when it is no longer needed.
If you want to grant readonly access to an user using only a Postgres script, you can do that:
CREATE EXTENSION IF NOT EXISTS dblink;
DO
$$
DECLARE nome_banco TEXT;
DECLARE template_conexao TEXT;
DECLARE string_conexao TEXT;
DECLARE nome_usuario TEXT;
BEGIN
template_conexao = 'user=foo password=bar dbname=';
nome_usuario = 'baz';
FOR nome_banco IN
SELECT datname FROM pg_database
WHERE datistemplate = false
LOOP
string_conexao = template_conexao || nome_banco;
perform dblink_exec(string_conexao, 'GRANT CONNECT ON DATABASE "' || nome_banco || '" TO ' || nome_usuario);
perform dblink_exec(string_conexao, 'GRANT SELECT ON ALL TABLES IN SCHEMA public TO ' || nome_usuario);
END LOOP;
END
$$
--DROP EXTENSION IF EXISTS dblink;
It connects to each database, and executes the script; it can be easily adapted for other situations where you need to execute a database-local command on all of the databases.
Remember that, for security purposes, after executing the script above, you should drop the dblink extension created by the script above, unless, of course, you are already using the extension for other purposes.

PostgreSQL - Grant DEFAULT PRIVILEGES database-wide and revoke them just for a specific schema

I am experiencing a weird and (to me) inexplicable behaviour related to DEFAULT PRIVILEGES. It seems default privileges cannot be revoked just for a specific schema once they have been granted database-wide.
I am currently testing this with PostgreSQL 10.5 on CentOS.
Let's say there are 3 users:
admin Owner of the database. Used to manipulate the STRUCTURE of the database (CREATE, DROP, TRUNCATE...)
manager Used for DATA manipulation (INSERT, UPDATE, DELETE)
reader Used to read DATA (basically SELECT)
The idea is that:
admin will be the owner of the database and all the objects contained into it
manager will be used for data manipulation across all schemas but public (only user admin can modify data in public schema)
reader will be able to read everything.
To make things easier, this will rely on default privileges, so that newly created objects (schemas, tables, views, functions, etc.) will all have the correct permissions.
This is the first time I am trying something like that instead of a fine-grained permissions policy based on multiple users for all different schemas and apparently this setup should be very straightforward.
It turns out I am missing something.
Here is a simple test script. User admin is the owner of db database and all those commands are issued being connected to it as admin:
-- 1. User manager inherits from user "reader"
GRANT reader TO manager;
-- 2. Allow connections to the database to our users, but not PUBLIC
REVOKE ALL ON DATABASE db FROM PUBLIC;
GRANT CONNECT ON DATABASE db TO reader;
-- 3. Revoke default privileges from PUBLIC
ALTER DEFAULT PRIVILEGES REVOKE ALL ON SCHEMAS FROM PUBLIC;
ALTER DEFAULT PRIVILEGES REVOKE ALL ON TABLES FROM PUBLIC;
ALTER DEFAULT PRIVILEGES REVOKE ALL ON SEQUENCES FROM PUBLIC;
ALTER DEFAULT PRIVILEGES REVOKE ALL ON FUNCTIONS FROM PUBLIC;
-- 4. Grant default reading privileges to user "reader"
ALTER DEFAULT PRIVILEGES GRANT USAGE ON SCHEMAS TO reader;
ALTER DEFAULT PRIVILEGES GRANT SELECT ON TABLES TO reader;
ALTER DEFAULT PRIVILEGES GRANT SELECT ON SEQUENCES TO reader;
ALTER DEFAULT PRIVILEGES GRANT EXECUTE ON FUNCTIONS TO reader;
-- 5. Grant Defauly writing privileges to user "manager"
ALTER DEFAULT PRIVILEGES GRANT INSERT, UPDATE, DELETE ON TABLES TO manager;
ALTER DEFAULT PRIVILEGES GRANT USAGE ON SEQUENCES TO manager;
-- 6. Reinit "public" schema
DROP SCHEMA public;
CREATE SCHEMA public;
-- 7. HERE COMES THE WEIRD STUFF, the two following statements don't have any effect at all
ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE INSERT, UPDATE, DELETE ON TABLES FROM manager;
ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE USAGE ON SEQUENCES FROM manager;
This can be easily verified like that:
-- Execute as user "admin":
CREATE TABLE public.t (id serial PRIMARY KEY, dummy integer)
-- Execute as user "manager" (it should not be allowed, but it is!)
DELETE FROM public.t;
I know I could circumvent this using some trigger functions, but the point of the question is whether this is something normal and expected, some sort of bug or am I missing something?
I have been thinking about it and the most elegant solution I could come up with relies on an Event Trigger.
Of course it does not answer my question directly, meaning that I am still wondering why default privileges cannot be used like that, but at least this meets the initial requirement of set-and-forget that default privileges would have provided.
Create a function that revokes unwanted privileges and returns an event_trigger:
CREATE FUNCTION reset_privileges() RETURNS event_trigger AS $$
BEGIN
IF EXISTS (SELECT true FROM pg_event_trigger_ddl_commands() WHERE schema_name = 'public') THEN
REVOKE INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public FROM manager;
REVOKE USAGE ON ALL SEQUENCES IN SCHEMA public FROM manager;
END IF;
END;
$$ LANGUAGE plpgsql;
Create an actual EVENT TRIGGER (This requires superuser privileges!):
CREATE EVENT TRIGGER reset_public_schema_privileges
ON ddl_command_end WHEN TAG IN (
'CREATE TABLE',
'CREATE TABLE AS',
'CREATE VIEW',
'CREATE MATERIALIZED VIEW',
'CREATE FUNCTION'
) EXECUTE PROCEDURE reset_privileges();
The function checks whether the newly created object(s) are in the public schema and eventually revokes all the unwanted privileges from the user manager.
It does not even bother to filter those objects, but rather it revokes the privileges for ALL TABLEs, VIEWs and FUNCTIONs in the public schema. Of course it can be easily customised using the object_identity field provided by pg_event_trigger_ddl_commands and a more refined logic inside the function.
According to the manual for ALTER DEFAULT PRIVILEGES:
Default privileges that are specified per-schema are added to whatever the global default privileges are for the particular object type. This means you cannot revoke privileges per-schema if they are granted globally (either by default, or according to a previous ALTER DEFAULT PRIVILEGES command that did not specify a schema). Per-schema REVOKE is only useful to reverse the effects of a previous per-schema GRANT.
(This is even more explicit in the examples given on that manual page.)
So I think what is happening is that in step 5 of your script, you are setting the default privilege to grant DELETE on the tables of all schemas (as a global default):
ALTER DEFAULT PRIVILEGES GRANT INSERT, UPDATE, DELETE ON TABLES TO manager;
But in step 7 you are revoking from the public schema specifically. This revocation has no effect on the global grant, so the DELETE (and other) privileges will still be granted:
ALTER DEFAULT PRIVILEGES IN SCHEMA public REVOKE INSERT, UPDATE, DELETE ON TABLES FROM manager;
I think I would either (a) bite the bullet and add default privileges for each schema (which isn't "fire-and-forget" but is more explicit) or (b) challenge why I need the public schema to exist, aiming to remove it to simplify this situation.

Amazon Redshift Grants - New table can't be accessed even though user has grants to all tables in schema

I have a bit of a funny situation in Amazon Redshift where I have a user X who has grant select on all tables in schema public, but once a new table is created, this grant doesn't seem to apply to the new table. Is this normal behaviour? If yes, how does one deal with it such that the schema level grants are maintained. Thank you.
Executing the following command as super user (master):
alter default privileges
for user staging_user
in schema staging
grant select on tables
to reporting_user;
will allow reporting_user to select data from all future tables created by staging_user in schema staging.
In Redshift tables and views do not automatically inherit the permissions of their parent schema. Your newly created tables are only accessible to the user who created them, and the superuser.
In a recent patch to Redshift a new feature to grant default privileges was implemented that addresses this issue.
Alter Default Privileges
The following code snippet will grant select privileges only for all future tables in the sales schema to the sales_admin group. If you want this to apply to existing tables in a schema you will need to combine it with a second grant statement.
alter default privileges in schema sales grant select on tables to group sales_admin;
This is a normal behavior. Only the object owner/superuser have permission to use the object by default.
http://docs.aws.amazon.com/redshift/latest/dg/r_Privileges.html
You can add grant command to your create table statement and grant needed privileges for the user.
When we first spotted new tables not appearing in our reporting tool, I discovered a quick workaround is to re-execute the following SQL statement for the groups/users impacted:
ALTER DEFAULT PRIVILEGES IN SCHEMA <SCHEMANAME> GRANT SELECT ON TABLES TO GROUP <USER/GROUPNAME>;

Grant all on a specific schema in the db to a group role in PostgreSQL

Using PostgreSQL 9.0, I have a group role called "staff" and would like to grant all (or certain) privileges to this role on tables in a particular schema. None of the following work
GRANT ALL ON SCHEMA foo TO staff;
GRANT ALL ON DATABASE mydb TO staff;
Members of "staff" are still unable to SELECT or UPDATE on the individual tables in the schema "foo" or (in the case of the second command) to any table in the database unless I grant all on that specific table.
What can I do make my and my users' lives easier?
Update: Figured it out with the help of a similar question on serverfault.com.
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA foo TO staff;
You found the shorthand to set privileges for all existing tables in the given schema. The manual clarifies:
(but note that ALL TABLES is considered to include views and foreign tables).
Bold emphasis mine. serial columns are implemented with nextval() on a sequence as column default and, quoting the manual:
For sequences, this privilege allows the use of the currval and nextval functions.
So if there are serial columns, you'll also want to grant USAGE (or ALL PRIVILEGES) on sequences
GRANT USAGE ON ALL SEQUENCES IN SCHEMA foo TO mygrp;
Note: IDENTITY columns in Postgres 10 or later use implicit sequences that don't require additional privileges. (Consider upgrading serial columns.)
What about new objects?
You'll also be interested in DEFAULT PRIVILEGES for users or schemas:
ALTER DEFAULT PRIVILEGES IN SCHEMA foo GRANT ALL PRIVILEGES ON TABLES TO staff;
ALTER DEFAULT PRIVILEGES IN SCHEMA foo GRANT USAGE ON SEQUENCES TO staff;
ALTER DEFAULT PRIVILEGES IN SCHEMA foo REVOKE ...;
This sets privileges for objects created in the future automatically - but not for pre-existing objects.
Default privileges are only applied to objects created by the targeted user (FOR ROLE my_creating_role). If that clause is omitted, it defaults to the current user executing ALTER DEFAULT PRIVILEGES. To be explicit:
ALTER DEFAULT PRIVILEGES FOR ROLE my_creating_role IN SCHEMA foo GRANT ...;
ALTER DEFAULT PRIVILEGES FOR ROLE my_creating_role IN SCHEMA foo REVOKE ...;
Note also that all versions of pgAdmin III have a subtle bug and display default privileges in the SQL pane, even if they do not apply to the current role. Be sure to adjust the FOR ROLE clause manually when copying the SQL script.
My answer is similar to this one on ServerFault.com.
To Be Conservative
If you want to be more conservative than granting "all privileges", you might want to try something more like these.
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO some_user_;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO some_user_;
The use of public there refers to the name of the default schema created for every new database/catalog. Replace with your own name if you created a schema.
Access to the Schema
To access a schema at all, for any action, the user must be granted "usage" rights. Before a user can select, insert, update, or delete, a user must first be granted "usage" to a schema.
You will not notice this requirement when first using Postgres. By default every database has a first schema named public. And every user by default has been automatically been granted "usage" rights to that particular schema. When adding additional schema, then you must explicitly grant usage rights.
GRANT USAGE ON SCHEMA some_schema_ TO some_user_ ;
Excerpt from the Postgres doc:
For schemas, allows access to objects contained in the specified schema (assuming that the objects' own privilege requirements are also met). Essentially this allows the grantee to "look up" objects within the schema. Without this permission, it is still possible to see the object names, e.g. by querying the system tables. Also, after revoking this permission, existing backends might have statements that have previously performed this lookup, so this is not a completely secure way to prevent object access.
For more discussion see the Question, What GRANT USAGE ON SCHEMA exactly do?. Pay special attention to the Answer by Postgres expert Craig Ringer.
Existing Objects Versus Future
These commands only affect existing objects. Tables and such you create in the future get default privileges until you re-execute those lines above. See the other answer by Erwin Brandstetter to change the defaults thereby affecting future objects.

PostgreSQL: making a schema restricted/unchangable?

We like our production environment with a restricted/unchangable schema -- the development side can be owned by the developers and changed as they like -- and we like to vet changes as they are promoted.
I'm wondering if this may be a solution to making that happen:
postgres% create proddb with owner=postgres;
unixside% pg_restore --dbname=devdb [--schema-only] --no-owner proddb
/* grants to users on schema objects appear to remain intact */
/* here's the magic, I hope... */
postgres% revoke create on schema public from public;
postgres% grant usage on schema public to produser(s);
Some testing seems to show that a user in this new proddb can interact with tables normally (with appropriate grants) and cannot alter the schema (alter table, create table, drop table, etc). But I'm paranoid and very new to Postgres, so...
Q: Is this correct?
Q: Am I missing anything?
Thanks muchly.
Yes, that is correct. The only addition is that the owner of a table can always delete or modify it. So it may not work if you have existing tables in the schema.
Discovered a missing element: sequences.
The user was finding errors in his scripts; similar errors appeared in the logs:
ERROR: permission denied for sequence <sequence>
The production schema showed that although sequences were created, they were owned by postgres and no explicit grants were given to the users. As per the GRANT documentation:
Granting permission on a table does not automatically extend permissions to any sequences used by the table, including sequences tied to SERIAL columns. Permissions on sequence must be set separately.
Our fix (verbose for this demonstration) was to find all sequences:
unixside% pg_dump --schema-only proddb > proddb.schema
unixside% grep -i 'create sequence' proddb.schema
...and apply appropriate grants (select to prevent table scans, update to prevent the above errors):
postgres% grant select,update on <sequence> to produser(s);
So far, the user says it's working and errors to the log have stopped...
I forget what version PostgreSQL added the syntax, but one of the easiest ways to administer permissions in PostgreSQL is through the "GRANT foo, priv ON ALL something IN SCHEMA" syntax.
BEGIN;
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA my_schema TO my_role;
GRANT USAGE ON ALL SEQUENCES IN SCHEMA my_schema TO my_role;
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA my_schema TO my_role;
COMMIT;
Very handy for making sure that permissions are always set correctly.
The EXECUTE for FUNCTIONS may seem spooky, but shouldn't be unless your functions were created with the SECURITY DEFINER attribute (and if you are using SECURITY DEFINER, you'd better be cautious since you're playing around with the PostgreSQL version of a "setuid" function). If you space out your TABLES across different SCHEMAS based on the expected permissions, then this becomes a pretty handy convention when used with the search_path variable.
ALTER ROLE my_role SET search_path = my_schema, auth_schema, public;
-- Avoid using the public schema (pretty please)
Where auth_schema has a collection of tables that my_role shouldn't have direct read or write privileges on. Assigning privs to GROUPS is also useful.
Here are some relevant docs:
http://developer.postgresql.org/pgdocs/postgres/sql-grant.html
Don't forget you can use "\h GRANT" in psql to easily figure out the syntax or remember what can be done on all objects in a schema (search for "IN SCHEMA").