For data access controls, typically you will want specific privileges for select, insert, update and delete on each table. You may also want separate admin privileges that allow you to grant those rights.
At the functional level, you will probably have an execute privilege for each callable function, and you will probably want similar privileges for individual applications and components of applications. Eg, to allow the user to execute the role_manager component of admintool, you would probably create a privilege called exec_admintool_roleman
.
The hardest part of this is figuring out how you will securely manage these privileges. A useful, minimal policy is to not allow anyone to assign a role that they themselves have not been assigned.
A sensible basic division of schema responsibilities would be as follows:
Access Functions are required for: - Global context only (lookup-data, eg privileges, roles, etc) - Personal and Global Context (personal data, persons, assignments, etc) - Project and Global (projects, project_details) - All 3 (assignments) Determining privilege in Global Context: User has priv X, if X is in role_privileges for any role R, that has been assigned to the user. Role privileges are essentially static so may be loaded into memory as a shared variable. When the user connects, the privileges associated with their roles may be loaded into a session variable. Shared initialisation code: role_privs ::= shared array of privilege bitmaps indexed by role. Populate role_privs with: select bitmap_array_setbit(role_privs, role_id, privilege_id) from role_privileges; Connection initialisation code: global_privs ::= session privileges bitmap Clear global_privs and then initialise with: select bitmap_union(global_privs, role_privs[role_id]) from person_roles where person_id = connected_user; i_have_global_priv(x): return bitmap_testbit(global_privs, x);
This gives us the basic structure of each function, and identifies what must be provided by session and system initialisation to support those functions. It also allows us to identify the overhead that Veil imposes.
In the case above, there is a connect-time overhead of one extra query to load the global_privs bitmap. This is probably a quite acceptable overhead as typically each user will have relatively few roles.
If the overhead of any of this seems too significant there are essentially 4 options:
veil.veil_init_fns
.The newer approach has a number of advantages:
Initialisation functions veil_init(doing_reset bool) are critical elements. They will be called by automatically by Veil, when the first in-built Veil function is invoked. Initialisation functions are responsible for three distinct tasks:
The boolean parameter to veil_init (which is passed to registered initialisation functions) will be false on initial session startup, and true when performing a reset (veil_perform_reset()).
Shared variables are created using share(name text). This returns a boolean result describing whether the variable already existed. If so, and we are not performing a reset, the current session need not initialise it.
Session variables are simply created by using them. It is worth creating and initialising all session variables to "fix" their data types. This will prevent other functions from misusing them.
If the boolean parameter to an initialisation fuction is true, then we are performing a memory reset, and all shared variables should be re-initialised. A memory reset will be performed whenever underlying, essentially static, data has been modified. For example, when new privileges have been added, we must rebuild all privilege bitmaps to accommodate the new values.
Authentication should use a secure process in which no plaintext passwords are ever sent across the wire. Veil does not provide authentication services. For your security needs you should probably check out pgcrypto.
Initialising a user session is generally a matter of initialising bitmaps that describe the user's base privileges, and may also involve setting up bitmap hashes of their relational privileges. Take a look at the demo (The Veil Demo Application) for a working example of this.
When dealing with relational contexts, it is not always possible to keep all privileges for every conceivable relationship in memory. When this happens, your access function will have to perform a query itself to load the specific data into memory. If your application requires this, you should:
You may be able to trade-off between the overhead of connection functions and that of access functions. For instance if you have a relational security context based upon a tree of relationships, you may be able to load all but the lowest level branches of the tree at connect time. The access function then has only to load the lowest level branch of data at access time, rather than having to perform a full tree-walk.
Caching can be very effective, particularly for nested loop joins. If you are joining A with B, and they both have the same access rules, once the necessary privilege to access a record in A has been determined and cached, we will be able to use the cached privileges when checking for matching records in B (ie we can avoid repeating the fetch).