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.)
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.
I've added a user to a Firebird 2.1 instance using gsec, but now I wanted to grant SELECT on all tables to this new user. I could find how to grant this permission on specific tables, but not to them all:
GRANT SELECT ON TABLE table TO USER user;
If I try to use the new user I get the following error on isql:
no permission for read/select access to TABLE table
Is there a way to do that on Firebird 2.1?
Something like this:
EXECUTE BLOCK
AS
DECLARE VARIABLE tablename VARCHAR(32);
BEGIN
FOR SELECT rdb$relation_name
FROM rdb$relations
WHERE rdb$view_blr IS NULL
AND (rdb$system_flag IS NULL OR rdb$system_flag = 0)
INTO :tablename DO
BEGIN
EXECUTE STATEMENT ('GRANT SELECT ON TABLE ' || :tablename || ' TO USER TEST');
END
END
I could find how to grant this permission on specific tables, but not to them all
You can grant specific privileges to all users, already existing and yet to be created. That is made by granting privilege to "PUBLIC" pseudo-user. Or you could grant privileges to some ROLE, then grant this ROLE to given users, then during database connection specify the option, that the user impersonate this ROLE in this session.
However there is no syntax to grant privileges to all generators or all procedures or all views or all tables, etc.
See the GRANT statement syntax in documentation.
What you can do though, is creating a simple script (anonymous and volatile aka EXECUTE BLOCK or persistent and named as STORED PROCEDURE) and that script of yours would be querying system tables to list all non-system ( COALESCE(RDB$SYSTEM_FLAG,0)=0 ) tables (together with views, or tables but not views: check if RDB$RELATION_TYPE is 0 or is 0 or 1) and then form a sequence of GRANT statements and execute them one by one.
See documentation about EXECUTE BLOCK and FOR SELECT ... INTO ... DO ... and EXECUTE STATEMENT ( or maybe FOR EXECUTE STATEMENT also, but I do not think it already was there in FB 2.1. There is unofficial wiki tracking which statements were available since which FB versions, but it is not thorough and may contain errors: http://firebirdsql.su/doku.php ).
https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-security-privs.html#fblangref25-security-privs-grant
https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref-appx04-relations.html
https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-psql-coding.html#fblangref25-psql-execstmt
https://www.firebirdsql.org/file/documentation/reference_manuals/fblangref25-en/html/fblangref25-dml-execblock.html
I have a postgres instance with a user root that has full admin privileges.
I have two databases db1 and db2.
For every database, I would like to have two users dbN_user and dbN_admin. dbN_user will be the used by my application, and dbN_admin will be used for migrations that change the table structure.
No rows are ever deleted by the application, and I would like to enforce that with user privileges.
db1_user should be able to connect to db1, and be able to SELECT, INSERT and UPDATE, but not DELETE.
db1_admin should have additional privileges to DELETE, CREATE TABLE, ALTER TABLE.
What are the SQL statements to set this up?
dbN_admin would be the owner of the objects, so that user would have all privileges automatically.
You need to GRANT the privileges for dbN_user on the tables and other objects themselves, not on the database.
Just add the correct GRANT statements after the CREATE TABLE statements in the SQL script that populates the database.
You need to GRANT the USAGE privilege on the schema that contains the objects to dbN_user as well.
There is the possibility to define default privileges in a database:
ALTER DEFAULT PRIVILEGES FOR dbN_admin
GRANT SELECT, INSERT, UPDATE ON TABLES
TO dbN_user;
This will grant the privileges automatically whenever dbN_admin creates a new table (but it does not affect tables created before the ALTER DEFAULT PRIVILEGES command).
admin:
create user db1_admin;
create schema app_relations;
alter schema app_relations owner to db1_admin;
app:
create user db1_user;
grant CONNECT ON DATABASE db1 to db1_user; --only if you have restricted connections on db previously
grant usage on schema app_relations to db1_user;
grant select,insert,update on all tables in schema app_relations to db1_user;
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").