Version 67 (modified by 14 years ago) ( diff ) | ,
---|
BluePrint for Authorization
Roles
- Roles are stored in the {{auth_group}}.
- These have no links to the groups in
pr_group
. - We are currently adopting a simplistic 3-tier approach of Person -> Role -> Permissions.
- We consider that the 4-tier approach of Person -> Group -> Role -> Permissions is unnecessarily complex for users, despite giving strong flexibility & the potential for advanced admins to move persons into roles in bulk & including future members of the group.
- Roles for the currently logged-in user are cached in the session for easy access throughout Model, Controllers & Views.
- Replace
auth.has_membership(group)
in default modules menu in01_modules.py
- Replace
In models/00_utils.py
:
def shn_sessions(): ... roles = [] try: user_id = auth.user.id _memberships = db.auth_membership memberships = db(_memberships.user_id == user_id).select(_memberships.group_id, cache=(cache.ram, 60)) # 60s cache for membership in memberships: roles.append(membership.group_id) except: # User not authenticated therefore has no roles other than '0' pass session.s3.roles = roles
Caching
Cache should be invalidated as part of the onaccept for auth_group.update()
- This can be done by storing a key in the session & changing the key (Dominic to expand...)
Restrictions
Module restriction
s3.modules in01_modules.py
- Add Controller check as well as menu check.
appadmin.py
&dvi.py
have these already- need modifying to use
session.s3.roles
- need modifying to use
- Configure permissions in
000_config.py
instead of01_modules.py
- Change
deployment_settings.modules
from a list of strings to a Storage()# Need to define Roles in dict for visibility by modules before inserted into DB deployment_settings.auth.roles = { 1 : "Administrator", 2 : "Authenticated", .... 6 : "AdvancedJS", } deployment_settings_modules = Storage( gis = Storage( name_nice = "Mapping", description = "Situation Awareness & Geospatial Analysis", readable = None, # All Users (inc Anonymous) can see this module in the default menu & access the controller writable = None, # All Authenticated users can edit resources which aren't specially protected module_type = 2, # Used to locate the module in the default menu resources = Storage( apikey = { read : "|1|", # This resource is only visible to Administrators }, layer_js = { create : "|%d|" % deployment_settings.auth.roles["AdvancedJS"], # This resource requires the 'AdvancedJS' role to create (or admin) delete : "|%d|" % deployment_settings.auth.roles["AdvancedJS"], # This resource requires the 'AdvancedJS' role to delete (or admin) update : "|%d|" % deployment_settings.auth.roles["AdvancedJS"], # This resource requires the 'AdvancedJS' role to update (or admin) } ), ), ... )
- Change
Function restriction
Decorator function : @auth.requires_membership("Administrator")- doesn't support OR, doesn't support NOT
- not efficient now we have
session.s3.roles
- We need a function which efficiently handles OR, AND & NOT. With roles cached in session, this should be easy:
def myfunction(): roles = session.s3.roles if not 1 in roles or (2 in roles and 3 in roles) and not 4 in roles: # redirect out ...
- Developer can put in additional manual restrictions or alternate routes, as-required
Resource restriction
- Bad: REST controller can be blocked via a Decorator
- we'd need to patch other functions, such as sync, which would be hard to maintain
- Bad: Full security policy (native web2py) can be invoked, but this is painful (based on protected by default & granted manually) & untested within S3 recently
- Need a new method: open by default & restricted manually (see below for proposed implementation)
- Option1: Use an
auth_permission
table similar to Web2Py 'full' but just for tables & withgroup_id
asmultiple=True
- Option2: Set within
000_config.py
, along with module permissions (see above example)- Means less DAL calls
- Option3: Have an onvalidation which auto-populates the
reader_id
/writer_id
fields in records- Means that no additional auth check at table-level needed
- Means that the security model isn't held all in one place, so not as easy to maintain?
- If done at validation time then it means that no extra DAL calls are done when creating resources (just extra field(s) within the 1 DAL call)
- Once new solution in-place:
- remove security_policy from
s3_setting
table & session (00_utils.py
) - modify
shn_action_buttons()
in00_utils.py
- Check Views/Controllers which hide admin_id when simple security policy active
- remove security_policy from
- Option1: Use an
Record restriction
- Add 2 reusable
multiple=True
fields to each table which needs this:reader_id
&writer_id
combined aspermissions_id
for easy reuse within table definitions- Full backward compatibility since they default to None
- For List views modify
shn_accessible_query()
(called fromshn_list()
&shn_search()
)- combine with the
deleted==True
check- makes it easier to then replace that check with an 'inactive' field which is a date instead of a boolean, so that records can be set to expire (as well as giving us easy access to know when a record was deleted)
- Preferred Option: Do the check alongside deleted as part of a big JOIN
def shn_accessible_query(table): """ Return a filter of those records which are readable by the currently logged-in user - deleted records are filtered - records to which they don't have permissions are filtered """ if "deleted" in table.fields: deleted = (table.deleted == None) else: deleted = 1 roles = session.s3.roles if 1 in roles: # Admins see all data query = deleted elif "reader_id" not in table.fields: # No record-level permissions query = deleted else: # Fields with no restriction accessible = (table.reader_id == None) for role in roles: accessible = accessible & (table.reader_id.like('%|%d|%' % role)) query = deleted & accessible return query def user_function(): ... table = db[tablename] available = shn_accessible_query(table) query = available & query ...
- Advantages:
- Combines the deleted into single API call
- Single JOIN for optimal DB performance (Assumption needs testing)
- Advantages:
- Alternate Option: Do the check in Python after the initial query has returned
- Advantage: Might have better performance than complex DB string?
- Disadvantage: More records pulled from DB than necessary
- combine with the
- For single records modify
shn_has_permission()
(called fromshn_create()
,shn_delete()
,shn_read()
&shn_update()
, but also available for other functions)- Where possible, we merge this with the reading of the record to have a single DAL hit instead of 1 for the permissions check & another for the actual read by modifying the calling functions appropriately:
modules/sahana.py
defines a new SQLFORM2() class which takes a record instead of a record_idCrudS3
modifiesCrud.create()
to take a record instead of a record_id & callsSQLFORM2()
CrudS3
modifiesCrud.update()
to take a record instead of a record_id & callsSQLFORM2()
def shn_has_permission(name, tablename, record_id = 0): """ S3 framework function to define whether a user can access a record in manner "name" - if checking a table (record_id = 0), then it returns Boolean - if checking a record (record_id > 0), then it returns the record or None """ roles = session.s3.roles table = db[tablename] # Check if table is restricted (Options 1 & 2) if 1 in roles: # Admins see all tables authorised = True else: # Option 1 #restriction = db(db.auth_permission.table_name == tablename).select(db.auth_permission.group_id, limitby=(0, 1), cache=(cache.ram, 60)).first().group_id #if restriction: # authorised = False # restrictions = re.split("\|", restriction)[1:-1] # Assume we generally have fewer restrictions than roles # for restriction in restrictions: # if restriction == "0" or int(restriction) in roles: # restriction 0 is anonymous # authorised = True #else: # authorised = True # Option 2 module, resource = tablename.split("_", 1) try: restriction = deployment_settings_modules["module"].resources["resource"]["name"] except: restriction = None if restriction: authorised = False restrictions = re.split("\|", restriction)[1:-1] # Assume we generally have fewer restrictions than roles for restriction in restrictions: if restriction == "0" or int(restriction) in roles: # restriction 0 is anonymous authorised = True else: # No restriction authorised = True # Options 3: #if record_id: # Options 1 & 2: if record_id and authorised: _fields = table.fields # We pull back the full record rather than just the fields needed to check permisisons, # so that the caller doesn't need to make a 2nd DAL call to access the data record = db(table.id == record_id).select(limitby=(0, 1)).first() if "deleted" in _fields: # Check if record is deleted if record.deleted: record = None return record if 1 in roles: # Admin is always authorised to view undeleted data (deleted data accessible through alternate UI) return record if not "reader_id" in _fields: # No record-level permissions (we assume that reader_id & writer_id fields always present/absent together via use of 'permissions_id') return record # Check the record's auth fields if name == "read": if not table.reader_id: return record else: authorised = False restrictions = re.split("\|", table.reader_id)[1:-1] # Assume we generally have fewer restrictions than roles for restriction in restrictions: if restriction in roles: authorised = True elif restriction == "2" and "created_by" in _fields: # 'Creator' restriction if auth.user.id == table.created_by: authorised = True if not authorised: record = None return record elif name in ["delete", "update"]: if not table.writer_id: return record else: authorised = False restrictions = re.split("\|", table.writer_id)[1:-1] # Assume we generally have fewer restrictions than roles for restriction in restrictions: if restriction == "0" or int(restriction) in roles: # restriction 0 is anonymous authorised = True elif restriction == "2" and "created_by" in _fields: # 'Creator' restriction if auth.user.id == table.created_by: authorised = True if not authorised: record = None return record else: # Something went wrong session.error = str(T("Invalid mode sent to")) + " shn_has_permission(): " + name redirect(URL(r=request, f="index")) if record_id: return None else: return authorised
- Where possible, we merge this with the reading of the record to have a single DAL hit instead of 1 for the permissions check & another for the actual read by modifying the calling functions appropriately:
- UI to manage the fields.
- We expect relatively few groups per instance, so can use the checkboxes widget?
- can filter out some groups?
- Have a single checkbox for 'Restrict access' which then opens out the 2 fields.
- We expect relatively few groups per instance, so can use the checkboxes widget?
Field restriction
- In model (for access by all controllers, such as sync):
if 1 not in roles and deployment_settings.auth.roles["MyRole"] not in roles: table.field1.readable = False table.field2.writable = False
Location restriction
e.g. Country or Region
- Add a special role 'Geographic' which can be added to
writer_id
(& maybereader_id
although less use case for this)- Patch
shn_has_permission()
(& maybeshn_accessible_query()
) to spot this special case &, if no other roles match, then do a lookup in another table (or deployment_settings dict)
- Patch
- NB If doing this through a table, then need to ensure that this table is protected appropriately
Organisation restriction
- This could be all members of the Organisation or just the 'Focal Point'
- Add a special role 'Organisation' which can be added to
writer_id
(& maybereader_id
although less use case for this)- Patch
shn_has_permission()
(& maybeshn_accessible_query()
) to spot this special case &, if no other roles match, then do a lookup in another table (or deployment_settings dict)
- Patch
- NB If doing this through a table (such as a person's organisation), then need to ensure that this table is protected appropriately
Author restriction
Only allow a user to update records which they created.
- Add a special role 'Creator' which can be added to
writer_id
(& maybereader_id
although less use case for this)- Patch
shn_has_permission()
(& maybeshn_accessible_query()
) to spot this special case &, if no other roles match, then do a check betweenauth.user.id
&table.created_by
- Patch
Anonymous authoring
Some tables should be writable by unauthenticated users
- Need to differentiate the 2 (can deposit new but not edit existing)
- Function
shn_has_permission()
above handles this by the admin setting the relevant permission to|0|
- Might want to be have new records by unauthenticated users not be visible in lists until an admin has approved them
- Add a new field 'approved', which default to 'False' for unauthenticated users
- Filter for this:
query = query & (table.approved == True)
- Q: Do we keep this in individual controllers
- manual & need to check additional functions like Sync
- Q: Do we do check globally within
shn_accessible_query()
&shn_has_permission
- means extra logic every CRUD check when most resources don't need this
- Q: Do we keep this in individual controllers
Visibility of Options
Ideally options which a user doesn't have permission for should be hidden from them.
Modules are hidden from Modules menu & front pageAction buttons in tables only show 'Details' for unauthenticated users, but 'Update'/'Delete' for authenticated ones- ideally would distinguish per record if some records restricted (We already pull the data for the rows, so need to ensure SQL query includes relevant fields & that we act upon them)
- Controller menus should be adjusted (currently done manually => harder maintenance)
- Views should be adjusted (currently done manually => harder maintenance)
Specific Examples
- A Person's Contacts shouldn't be visible by default.
- Authenticated is OK
- Simply add the Authenticated group (2) to the table (or records in the table if using Option 3)
This requires all authenticated users to be added to the 'Authenticated' group
- Authenticated is OK
- A Person's Subscriptions shouldn't be visible by default.
- Admin or themselves is OK
- Option A: restore the web2py default of adding 1 group per user!
- In
models/00_settings.py
:auth.settings.create_user_groups = True
- Check using
auth.user_group(auth.user.id)
- Filter these out of our views?
- In
- Option B: use the link from subscription to person & do manual check somewhere
- tbc
- Option A: restore the web2py default of adding 1 group per user!
- We may want to allow people to be subscribed to things by others.
- This would need a 'Subscriber Admin' role
- We could give this role to all registered users by default if we Hook into the registration onaccept
- Currently this requires modifying
shn_register()
inmodules/sahana.py
- Replace with a registration_onvalidation and a registration_onaccept hook, which operate with secured copies of the registration data (e.g. no password contained).
- Currently this requires modifying
- We could give this role to all registered users by default if we Hook into the registration onaccept
- This would need a 'Subscriber Admin' role
- Admin or themselves is OK
- Messaging: If access to a record is restricted then access to Messages relating to that record should also be restricted
- unless routed somewhere visible as well!
- block subscription
- onvalidation on message routing (i.e. tagging) to check if the only tags are on restricted resources...if they are then restrict the message too.
- onvalidation not onaccept so that only 1 DAL update() is done
- FRP: a request that has been submitted by an IP user can be completely changed as well as cancelled/deleted by IP users belonging to the same organisation, but only until a WFP user has confirmed the request - after that the IP users can only change certain fields in the record, and not delete the record anymore.
- Field in requests table 'confirmed' which is writable=False unless role==WFP
- onvalidation for requests does a check whether confirmed is set &, if it is, only allows the relevant fields to be changed & denies deletion
- FRP: if a user has a role which would normally be granted permission to a resource has another role, then deny them access instead.
shn_role_check
covers this: BluePrintAuthorization
- FRP:
- An 'IP User' is permitted to submit requests on behalf of his organisation.
- Hide 'Create Request' menu item unless this role present
- Restrict the 'create' right on the Requests table to members of this role
- In View request_create provide jQuery which hides the
or_organisation field
for people with this role. - Match this server-side by providing a create_onvalidation hook which says that if someone in this role then set the Organisation to theirs.
- An 'IP Admin' is an 'IP User' who can, in addition, add new 'IP Users' to their organisation.
- just has the IP_Admin role added to menu unhiding
- just has the IP_Admin role added to restriction
- just has the IP_Admin role added to jQuery hide
- just has the IP_Admin role added to create_onvalidation
- Hide 'Add IP User' menu item unless this role present
- this takes you to a specialised auth_membership form with the IP_User role preselected (can be done in jQuery hiding the selected real dropdown & putting text up in visible part instead)
- Restrict the 'create' right on the
auth_membership
table to members of this role - Provide onvalidation on auth_membership to set the group_id to 'IP_User' if this role is set
- A 'FAC User' is an 'IP Admin' for all organisations, but without 'IP User' permissions except for their own organisation (=WFP).
- just has the FAC_User role added to menu unhiding
- just has the FAC_User role added to restriction
- just has the FAC_User role added to jQuery hide
- just has the FAC_User role added to create_onvalidation
- takes to a normal form without the jQuery hiding (can be identical form just the jQuery doesn't activate for this role)
- just has the FAC_User role added to restriction
- just has the FAC_User role added to onvalidation
- A 'FAC Admin' is a 'FAC User' with additional 'IP User' permissions for all organisations.
- just has the FAC_Admin role added to menu unhiding
- just has the FAC_Admin role added to restriction
- has nothing special (jQuery doesn't activate for this role)
- has nothing special (no special onvalidation)
- takes to a normal form without the jQuery hiding (can be identical form just the jQuery doesn't activate for this role)
- just has the FAC_Admin role added to restriction
- just has the FAC_Admin role added to onvalidation
- An 'IP User' is permitted to submit requests on behalf of his organisation.
Note:
See TracWiki
for help on using the wiki.