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).
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.
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.
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. |
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.
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 |
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:
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.
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
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?