Permissions

Background

In any general web based system we will generally need to restrict access to various resources depending on the context within which they are accessed. We would like to implement such a system for Paneris projects built using webmacro. The system should handle most cases that we come across without extension (just configuration).

Datadictionary

We already have a mechanism (the datadictionary) which facilitates access to database objects within a webmacro framework. It is a combination of a set of 5 or so datadictionary tables (which basically hold application-orientated metadata about tables/fields) and a number of wrapper objects which represent fields or records - (org.paneris.util.)ResultsField and ResultsSet. These give access to the metadata about, and the actual data within, a particular field/record. It is designed to run on any (SQL) database.

In particular it allowed us to create set of generic handlers which allow us to list, display and edit database tables and records (the administration handlers - Administration, AdministrationEdit, AdministrationUpdate, etc). It also gives us automagical access to a number of properties of each table/field from the database in both our handler code and templates.

The main restriction which the datadictionary enforces is that all tables must have a primary key which is an integer.

Note that because each table/field has an entry in the datadictionary, it affectively gives us a way to do pointers in a database! We can 'point' to a table or field using the (integer) key for that table/field's entry in the relevant datadictionary table.

It therefore seems natural to extend this system to cover permissions as well. This implies that we will primarily be interested in access to database tables and records. Since most resources will be represented in some way in the database this should not be a problem. If different resources need to protected in some way, the mechanism proposed below can be extended/altered.

 

Mechanism

We will implement security in the following way:

Each handler will have responsibility for controlling access to all resources used by that handler.

So, nothing will have access control automatically. However, we can construct a set of helper objects/functions which can be called within handlers to implement the most common types of access control. In particular the generic administration handlers mentioned above will use these objects/functions in a "default" manner to prevent people trying to alter the database using the admin handlers rather than going through the project specific handlers.

User Class

The main object which will give us access to the default permissions behaviour will be a User object. There will be a org.paneris.util.User, or somesuch, which can be overridden in specific projects

The idea is that User objects will be made available to handlers via the webmacro ResourceBroker, extending the default UserProvider (which will cache it for use later on in the same session). We will test our servlet session to see if we have set the User object to use, and if not require the user to identify herself (usually via a standard username/password login), giving us a User object. This will be accomplised with a default (static) login class which handles this for most standard situations. Note that it is intended that when a User object is created, we will work out there and then which groups they belong to.

The functions that the base class it will provide are:

class User {
int getID();
int[] getGroups();
bool canRead(int tableID);
bool canWrite(int tableID);
bool canRead(int tableID, int recordID);
bool canWrite(int tableID, int recordID);
}

and variants allowing a tablename String rather than int, and DDRecord as the only argument to any of these functions. The point is, though, that we can test read/write access to a table/record. To me, this means:

  Table

Record

Can Read Can list/display all/any record(s) Can display this record
Can Write Can update/delete any record and add a record Can update or delete this record.
If the record we are talking about is our special record number '0' (which is the datadictionary shorthand for 'create a new record') then write permission means we can add a record to this table.

Tables Supporting the User Class

We need 4 tables to work out what users and groups are, and which users/groups are members of (other) groups. They will be used to set up the User object.

  1. There is a users table which lists each user and their pertinent information. It only needs to have a userid, but if you also give it "username" and "password" fields then you will be able to make use of the default login class!
  2. There is a groups table which lists each group and its pertinent information. It could be as simple as a lookup to get a group name, but could obviously hold more information.
  3. There are 2 tables which shows us which group(s) the user belongs. They associate, respectively, either a user's id or a group's id to a group. If a group, G1 is a member of another group G2 then any user who is a member of G1 is also a member of G2 (we must be careful about loops). Call these tables users_in_groups and groups_in_groups.

 

Default Implementation

This is all well and good but we want to come up with an implementation which we will be able to configure and use primarily using the datadictionary mechanism. An aim of this is that it will handle most permissions related tasks we need to do regularly by default. One way to do this is to create 4 tables which show which users/groups can access which tables/records. These might look something like:

user_table_permissions

id table_key user_key create_new permissions_key
integer pointer (in DD) to a table pointer to a user record boolean - can we write to record '0' Read or Write
25 orders AccountingManager false (but can probably do it as a member of a group) Read
27 inventory Administrators false Write

user_record_permissions

id table_key delegate_permissions_to permissions_mask_key
integer pointer to a table a pointer to a field in the table, which must be a foreign key to another table Read or Write
25 orders

project (the project_manager "owns" an order with Read - permissions_mask_key - permissions)

Read

group_table_permissions

id table_key group_key create_new permissions_key
integer pointer (in DD) to a table pointer to a group record boolean - can we write to record '0' Read or Write
25 orders Overseers false Read
52 orders Administrators false Write
27 inventory Administrators true Write
34 inventory Supplier true Read

group_record_permissions

id table_key delegate_permissions_to permissions_mask_key
integer pointer to a table a pointer to a field in the table, which must be a foreign key to another table Read or Write
25 orders Buyer Read
52 orders Seller Write

 

How they work

So how can we use all these tables to work out if a user/group has read/write access to a table/record?

1) If a table has no permissions on it (from the 4 tables directly above) then it is read/write for everyone. As soon as there is at least one, then we must work out if each user can have access or not.

2) To work out permissions for a table, we look at entries for this table in user_table_permissions. If we are one of the users listed in user_table_permissions we have the associated permissions.We also look in group_table_permissions to see if we are a member of any of the groups with permissions on this table. If one of these gives us write permission we have read/write, if one has read we have read permissions, otherwise we cannot access the table. (We could probably work this out and stick it in TableMetaData once per table...)

3) For record permissions, the idea is that we can work out which user or group "owns" (has some access permissions to) a record by looking at specially named fields in it pointing to a user or group record (let's choose 'owneruser' and 'ownergroup' for the special names). If we don't have fields in our table which explicitly name the owner(s) of the record in this manner then we can follow a link into another record and see which user or group owns that record. This is recursive so we've got to be careful of loops. In detail:

  1. If we wish to see if a user U has access to a record R in table T then we first see if T has a field 'owneruser'. If it does then it either points to us (as a user) or it doesn't, in which case we either have some permissions or we don't.
  2. If there isn't a owneruser field, we can try to find out who owns R by following a link field into another record: If there is an entry for our table T in user_record_permissions then we look at delegate_permissions_to which names a field (F) in R - this field must be a link to another table T1. We take the record R1 that F in R points to and decide whether U has access to record R1 in T1. If so, U has access to R in T.

    Note that there can be more than one entry for each table in user_record_permissions, so we have tree of tables+permissions to search.

  3. The permissions that U has are the most restrictive that we encounter (in permissions_mask_key) on the path to the leaf of the tree (i.e. if we encounter one read, the permission is read, otherwise write).
  4. In the case where we want to see if the user has write permissions for record 0 in table T (i.e. he can write a new record in T) we look at create_new in user_table_permissions (i.e. its really like a permissions for table operation - we don't go into other tables)

For instance, which group owns a particular orders record? If orders has a "groupowner" field, then any member of the group that this points to has read/write permissions to that record. In the case of our orders table, there isn't one (trust me!), so we look in group_record_permissions. We see that we can follow both the Buyer and Seller fields to try and find the owner. I happen to know that these fields both point to a record in an organsiations table. This does have a "groupowner" field. For instance, in one record in the order table (order123), the Buyer is a link to an organisation (Keyhaven) and Seller is a link to Paneris. These organisations are members of the groups "KeyhavenGroup" and "PanerisGroup" respectively. Hence any member of KeyhavenGroup can see this record (they can see any orders sent to them) but cannot alter it. Similary, anyone in PanerisGroup can read and alter this record. In this case it might be better to give read/write permissions only the person who created the order rather than to the whole PanerisGroup group, so we should put an "userowner" field in the table and alter the permissions for "Seller" to be read.

 

In Terms of the User object...

So using the definitions above, we can work out what our functions return:

bool canRead(int tableID);

True if there are no permissions for this table OR the user has read/write permissions for this table OR a group which the user belongs to has read/write permissions to this table

bool canWrite(int tableID);

True if there are no permissions for this table OR the user has write permissions for this table OR a group which the user belongs to has write permissions to this table

bool canRead(int tableID, int recordID);

True if the user has read or write permissions for the table OR the user has read or write permissions for this record OR a group which the user belongs to has read or write permissions to this record

bool canWrite(int tableID, int recordID);

True if the user has write permissions for the table OR the user has write permissions for this record OR a group which the user belongs to has write permissions to this record

Thoughts

This proposal only gives us a base level of security. I think it covers most cases that we have encountered before, but does not cover all cases. If this system is inadequate for a particular application you can simply override or subclass the User functions to your own ends.

Should we fix the names of fields to look for owners in (userowner and groupowner) or allow (require?) these to be defined explicitly for each table?

Any obvious problems/improvements?