Version 22 (modified by 15 years ago) ( diff ) | ,
---|
BluePrint for Authorization
User Stories
- A Developer needs to be able to restrict access to a Module
s3.modules in01_modules.py
- Add Controller check as well as menu check.
- 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 : deployment_settings.auth.roles["AdvancedJS"], # This resource requires the 'AdvancedJS' role to create (or admin) delete : deployment_settings.auth.roles["AdvancedJS"], # This resource requires the 'AdvancedJS' role to delete (or admin) update : deployment_settings.auth.roles["AdvancedJS"], # This resource requires the 'AdvancedJS' role to update (or admin) } ), ), ... )
- Change
- A Developer needs to be able to restrict access to a Function
Decorator function : @auth.requires_membership("Administrator")- doesn't support OR (we could easily write our own function to do this, though)
- A Developer needs to be able to restrict access to a resource
- REST controller can be blocked via a Decorator
- Full security policy can be invoked, but this is painful (based on protected by default & granted manually) & untested within S3 recently
- We could check for what other functions can access data? Sync. Hard to maintain though.
- Need a new method: open by default & restricted manually
- Needs to be a DAL-level method since not all accesses go via S3XRC.
- Option1: Use an
auth_permission
table similar to Web2Py 'full' just for tables? - Option2: Set within
000_config.py
, along with module permissions? (see above example)- Means less DAL calls
- Option3: Have an onaccept which auto-populates the reader_id/writer_id fields in records?
- Means that no additional auth check at table-level needed
- Once new solution in-place:
- remove security_policy from
s3_setting
table & session (00_utils.py
) - modify
shn_action_buttons()
in00_utils.py
- remove security_policy from
- A Developer needs to be able to restrict access to a record
- Add 2 reusable
multiple=True
fields to each table which needs this:reader_id
&writer_id
combined aspermissions_id
- Full backward compatibility since they default to None
- reader_id checked with a new API function (called from shn_read() & shn_list(), but also available for other functions)
- 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)
- Option 1: 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 (Modified version of current function from models/01_crud.py) """ deleted = (table.deleted == None) try: user_id = auth.user.id _memberships = db.auth_membership memberships = db(_memberships.user_id == user_id).select(_memberships.group_id) except: memberships = None roles = [] for membership in memberships: roles.append(membership.group_id) if 1 in roles: # Admins see all data query = deleted else: # Fields with no restriction accessible = (table.reader_id == None) for role in roles: #accessible = accessible & (table.reader_id == str(role)) & (table.reader_id.like('%d|%' % role)) & (table.reader_id.like('%|%d|%' % role)) & (table.reader_id.like('%|%d' % role)) 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:
- Option 2: 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
- writer_id checked within a modified
shn_has_permission()
(called from shn_update(), etc, but also available for other functions)def shn_has_permission(name, tablename, record_id = 0): """ S3 framework function to define whether a user can access a record in manner "name" """ try: user_id = auth.user.id _memberships = db.auth_membership memberships = db(_memberships.user_id == user_id).select(_memberships.group_id) except: memberships = None roles = [] for membership in memberships: roles.append(membership.group_id) # Check if table is restricted table = db[tablename] if 1 in roles: # Admins see all tables authorised = True else: # Option 1 #restriction = db(table[name].like('%|%d|%' % role)).select() # Option 2 module, resource = tablename.split("_", 1) try: restriction = deployment_settings_modules["module"].resources["resource"]["name"] except: restriction = None if restriction: if restriction in roles: authorised = True else: authorised = False else: # No restriction authorised = True # Option 3 # - not necessary! if record_id and authorised: # Check if record is deleted record = db(table.id == record_id).select(table.deleted, table.reader_id, table.writer_id, limitby=(0, 1)).first() if record.deleted: authorised = False elif 1 in roles: authorised = True else: if name == "read": if not table.reader_id: authorised = True 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 name in ["delete", "update"]: if not table.writer_id: authorised = True 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 in roles: authorised = True else: # Something went wrong session.error = str(T("Invalid mode sent to")) + " shn_has_permission(): " + name redirect(URL(r=request, f="index")) return authorised
- Disadvantage: Slow
- UI to manage the fields.
- We expect relatively few groups per instance, so can use the checkboxes widget?
- Have a single checkbox for 'Restrict access' which then opens out the 2 fields.
- Add 2 reusable
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
- What if just some fields should be protected?
- 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
- 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
- An Admin should be able to restrict access to records to just those within a certain GIS location (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()
&shn_accessible_query()
to spot this special case &, if no other roles match, then do a lookup in another table (or deployment_settings dict)
- Patch
- Add a special role 'Geographic' which can be added to
- An Admin should be able to restrict access to records to just those within a certain organisation (or the Focal Point for the organisation)
- Add a special role 'Organisation' which can be added to
writer_id
(& maybereader_id
although less use case for this)- Patch
shn_has_permission()
&shn_accessible_query()
to spot this special case &, if no other roles match, then do a lookup in another table (or deployment_settings dict)
- Patch
- Add a special role 'Organisation' which can be added to
- 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!
- onaccept on message routing (tagging) to check if the only tags are on restricted resources...if they are then restrict the message too.
- Some tables should be writable by unauthenticated users (writable=|0|)
- Need special handling for this in shn_create/shn_update?
- Might need to differentiate the 2 (can deposit new but not edit existing)
- Might want to be have new records by unauthenticated users not be visible in lists until an admin has approved them
- Need special handling for this in shn_create/shn_update?
Note:
See TracWiki
for help on using the wiki.