= !BluePrint for Authorization = == Restrictions == === Module restriction === * ~~s3.modules in {{{01_modules.py}}}~~ * Add Controller check as well as menu check. * {{{appadmin.py}}} & {{{dvi.py}}} have these already * Configure permissions in {{{000_config.py}}} instead of {{{01_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) } ), ), ... ) }}} === Function restriction === * ~~Decorator function : @auth.requires_membership("Administrator")~~ * doesn't support OR, doesn't support NOT * We need to write a function which handles OR & NOT: {{{shn_role_check(1, 2, not=(3))}}} * 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 * Option1: Use an {{{auth_permission}}} table similar to Web2Py 'full' but just for tables & with {{{group_id}}} as {{{multiple=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()}}} in {{{00_utils.py}}} === Record restriction === * Add 2 reusable {{{multiple=True}}} fields to each table which needs this: {{{reader_id}}} & {{{writer_id}}} combined as {{{permissions_id}}} for easy reuse within table definitions * Full backward compatibility since they default to None * For List views modify {{{shn_accessible_query()}}} (called from {{{shn_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 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)) 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) * 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 * For single records modify {{{shn_has_permission()}}} (called from {{{shn_create()}}}, {{{shn_delete()}}}, {{{shn_read()}}} & {{{shn_update()}}}, 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, cache=(cache.ram, 60)) except: memberships = None roles = [] for membership in memberships: roles.append(membership.group_id) 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: record = None if "deleted" in table.fields: # 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 return authorised elif 1 in roles: # Admin is always authorised to view undeleted data (deleted data accessible through alternate UI) authorised = True return authorised # Check the record's auth fields if not record: record = db(table.id == record_id).select(table.reader_id, table.writer_id, limitby=(0, 1)).first() 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 == "0" or int(restriction) in roles: # restriction 0 is anonymous 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 }}} * 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. === Field restriction === * In model (for access by all controllers, such as sync): {{{ 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)) except: memberships = None roles = [] for membership in memberships: roles.append(membership.group_id) if 1 not in roles and deployment_settings.auth.roles["MyRole"] not in roles: table.field.readable = False }}} * NB If doing this then the roles checks inside {{{shn_has_permission()}}} & {{{shn_accessible_fields()}}} should be modified to read this global value instead of more DAL queries (even cached)! === Location restriction === e.g. Country or Region * Add a special role 'Geographic' which can be added to {{{writer_id}}} (& maybe {{{reader_id}}} although less use case for this) * Patch {{{shn_has_permission()}}} (& maybe {{{shn_accessible_query()}}}) to spot this special case &, if no other roles match, then do a lookup in another table (or deployment_settings dict) * 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}}} (& maybe {{{reader_id}}} although less use case for this) * Patch {{{shn_has_permission()}}} (& maybe {{{shn_accessible_query()}}}) to spot this special case &, if no other roles match, then do a lookup in another table (or deployment_settings dict) * 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}}} (& maybe {{{reader_id}}} although less use case for this) * Patch {{{shn_has_permission()}}} (& maybe {{{shn_accessible_query()}}}) to spot this special case &, if no other roles match, then do a check between {{{auth.user.id}}} & {{{table.created_by}}} === 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 === 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 page~~ * ~~Action 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~~ * 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? * Option B: use the link from subscription to person & do manual check somewhere * ''tbc'' * 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()}}} in {{{modules/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). * '''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: [wiki:BluePrintAuthorization#Functionrestriction] * Option: Introduce full-blown model of Persons -> Groups (pr_group) -> Roles (auth_group) -> Permissions * Downside: Extra layer of confusion for both Admins & Developers * Upside: Very flexible for Admins to manage which users (in bulk & future members) get what access to resources (subject to sufficient roles being made available by the developer) ---- BluePrintAuthenticationAccess BluePrints