Question

I've a problem and haven't find any clues so far. I'll try to explain it the best I can, but feel free to ask for more details!

Context

I'm working with Postgres 9.2.4 on Windows, and I need to implement some kind of quota administration for each user.

As far as I've read, there's no such built-in functionality, and most answers points to use file system's quota administration capabilities.

There's one single database, and each user will have his own schema.

The approach I've taken includes the separation of data files for each user on different locations by having different tablespaces, one for each user, being the user the owner of his tablespace (so I can apply the quota configuration on a per folder basis).

This led me to the problem I'm facing...

Problem

It happens that, when creating a table, the user is able to select the pg_default tablespace to store the data.

To add to my confusion, if later I change the tablespace to the one owned by the user, and then try to switch it back to the pg_default tablespace, a permission denied error is thrown.

To clarify the sequence here is some sample code:

-- Creates the table in the default tablespace
CREATE TABLE test_schema.test_table ( ) 
TABLESPACE pg_default;

-- Changes the tablespace to the one owned by the user
ALTER TABLE test_schema.test_table
SET TABLESPACE user_tablespace;

-- Tries to set back the pg_default tablespace (throws permission denied to pg_default tablespace)
ALTER TABLE test_schema.test_table
SET TABLESPACE pg_default;

All these commands were executed using a user login without administrative privileges. The pg_default tablespace is owned by the postgres login (administrative account).

My guess is that it has something to do with the database tablespace, which is set to use the pg_default tablespace.

Question

It is possible to constraint a user to only create objects in their owned tablespace?

Was it helpful?

Solution

If you use disk quota then you give yourself an awful lot of work. There is, in fact, an approximate solution in PostgreSQL, with some minor tinkering and no need to make a large number of tablespaces (schemas will still be a good idea to give every user his/her own namespace).

The function pg_total_relation_size(regclass) gives you the total disk space used for a table, including its indexes and TOAST tables. So scan pg_class and sum up:

CREATE VIEW user_disk_usage AS
  SELECT r.rolname, SUM(pg_total_relation_size(c.oid)) AS total_disk_usage
  FROM pg_class c, pg_roles r
  WHERE c.relkind = 'r'
    AND c.relowner = r.oid
  GROUP BY c.relowner;

This gives you the total disk space used by each owner, irrespective of where tables are located. It is presented as a view definition here for use below.

To make this work in a reasonably accurate fashion you need to regularly VACUUM ANALYZE your database. If you have low traffic periods (e.g. 3am-5am daily, or Sunday) run it then using a scheduled job with user postgres. Create a function for that job that does the VACUUM and then the quota check:

CREATE FUNCTION user_quota_check() RETURNS void AS $$
DECLARE
  user_data record;
BEGIN
  -- Vacuum the database to get accurate disk use data
  VACUUM FULL ANALYZE;

  -- Find users over disk quota
  FOR user_data IN SELECT * FROM user_disk_usage LOOP
    IF (user_data.total_disk_usage > <<your quota>>) THEN
      EXECUTE 'REVOKE CREATE ON SCHEMA ' || <<user''s schema name>> || ', PUBLIC FROM ' || user_data.rolname;
      -- REVOKE INSERT privileges too, unless you work with BEFORE INSERT triggers on all tables
    END IF;
  END LOOP;
END; $$ LANGUAGE plpgsql;
REVOKE ALL ON FUNCTION user_quota_check() FROM PUBLIC;

If the owner goes over the quota you can REVOKE CREATE on all relevant schemas, typically only the schema assigned to the user and the public schema, such that no new tables can be created. You should also REVOKE INSERT on all tables but this is easily circumvented because the owner can GRANT INSERT right back. That, however, could be cause for more drastic action against the user. Preferably you will create a before insert trigger on every table in the database, using a daily sweep just like the one above.

A user will still have SELECT privileges so he/she can still access data. More interestingly, DELETE and TRUNCATE will allow the user to free disk space and remedy the lock-out. The privileges can then be re-instated using something similar to the above function:

CREATE FUNCTION reclaim_disk_space() RETURNS void AS $$
DECLARE
  disk_use bigint;
BEGIN
  -- Vacuum current_user's tables.
  -- Slow and therefore adequate punishment for going over quota.
  VACUUM FULL VERBOSE ANALYZE;

  -- Now re-instate privileges if enough space was reclaimed.
  SELECT total_disk_usage INTO disk_use
  FROM user_disk_usage
  WHERE rolname = session_user;
  IF (disk_use < <<your quota>>) THEN
    EXECUTE 'GRANT CREATE ON SCHEMA ' || <<user''s schema name>> || ', PUBLIC TO ' || user_data.rolname;
    -- GRANT INSERT privileges too, unless you work with BEFORE INSERT triggers on all tables
    RAISE NOTICE 'Disk use under quota limit. Privileges restored.';
  ELSE
    RAISE NOTICE 'Still using too much disk space. Free up more space.';
  END IF;
END; $$ LANGUAGE plpgsql;      

The locked-out user can call this function him-/herself after having deleted sufficient data to go under the quota limit.

You can add more sophisticated features, such as having a table listing quotas per user (instead of an overall quota) and comparing actual use against that quota, issuing a RAISE NOTICE on an insert trigger when going over 80% of quota (this requires every table to have a before insert trigger, which can easily be done by the postgres user in a regular sweep of new tables, same trigger can be used to deny inserts if over the quota), repeating that notice every hour (so record when last notice was issued), etc.

This solution is approximate because the quota are not checked in real-time. This is possible (run the user_quota_check() on every insert, modified to check just the tables of the session_user) but most likely too much overhead to make it interesting. Run user_quota_check() overnight to have daily management of quotas. And manually flog any user using up too much space during the day.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top