wiki:BluePrintAuthorization

Version 28 (modified by Fran Boon, 11 years ago) ( diff )

Caching

BluePrint for Authorization

User Stories

  • A Developer needs to be able to restrict access to a Module:
    • s3.modules in 01_modules.py
    • Add Controller check as well as menu check.
    • 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 : 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)
                        }
                ),
            ),
            ...
        )
        
  • 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
      • we'd need to patch other functions, such as sync, which would be hard to maintain
    • 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
        • 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
  • 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 as permissions_id
      • 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
            """
        
            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, 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)
    
        # Check if table is restricted
        table = db[tablename]
        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 in roles:
            #            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:
                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?
      • can filter out some groups?
    • Have a single checkbox for 'Restrict access' which then opens out the 2 fields.
  • A Developer needs to be able to restrict access to a Field:
    • 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 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

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
  • A Developer 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 (& 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)
  • A Developer 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 (& 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)
  • A Developer should be able to restrict access to records to just those which the person 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
  • If access to a record is restricted then access to messages relating to that record should also be restricted
    • i.e. block subscription
    • 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

BluePrintAuthenticationAccess

BluePrints

Note: See TracWiki for help on using the wiki.