Who put ABAC in my RBAC?

Readers know that Attribute-based Access Control (ABAC) is a bit of an obsession with me.  It stems from the want to have something like an ABAC system in my little bag of tricks.  An authorization engine that scales to everyday usage, without proprietary, bloated or cumbersome baggage to weigh it down.

So I comment, lament and nothing seems to come of it.

Until I leaned that ABAC can be combined with RBAC.

We like RBAC, use it in our everyday applications, but it has some serious shortcomings, and we don’t know what to do about them.

ABAC also good.  It’s adaptable, but lacks meaningful standards, we struggle during implementations, and are left wanting more.

Now, let’s somehow combine the two.  Hopefully allowing the strengths of each to be preserved while eliminating their shortcomings.

What would such a system look like?

  1. Simple apis that are easy to understand and use.
  2. Standard data and api formats, something that can be shared between all of my apps and systems.
  3. Flexible decision expressions allowing unlimited instance data types and values to be considered.

How would this system work?

Standards-based RBAC adheres to the NIST model, later becoming an ANSI standard — INCITS 359.  Long story short, RBAC allows attributes to be applied during two separate phases of the access control decision:

1. User-Role Activation – instance data used to constrain whether an assigned role is eligible to be considered in the access control decision, i.e. permission check, that happens later.  For example, user may only activate the cashier role at store 314.

2. Role-Permission Activation – these constraints apply during the permission check itself.  An example is the action may only be performed if account #456789.

Apache Fortress 2.0.2 now supports type 1.  For a test drive, there’s this rbac-abac-sample in Github.  Have a look under-the-hood section of the README.

 

Towards an Attribute-Based Role-Based Access Control System

[Link to the Apache Fortress RBAC-ABAC-SAMPLE project on Github]

We’ve all heard the complaint, RBAC doesn’t work.  It leads to Role Explosion, defined as an inordinate number of roles in a production environment.  Nobody knows who must be assigned to what because there are hundreds if not thousands of them.

What’s a system implementor to do?  We could give Attribute-Based Access Control a try, but that has its own problems and we need not go there again.

There’s another way.  RBAC allows the usage of dynamic attributes.

  • Recent standards include dynamic policies, most notably, ANSI INCITS 494 RBAC Policy-Enhanced
  • The existence of entities to conveniently apply dynamic policies, e.g. User-Role and Role-Permission.
  • No language discouraging the usage of dynamic attributes alongside RBAC in the standard.

Indeed, dynamic attributes are encouraged if not prescribed.  Here’s where I should be pointing to evidence substantiating my arguments.

http://lmgtfy.com/?q=NIST+and+ANSI+and+RBAC+and+attributes

This brings us to Apache Fortress and a new enhancement to use dynamic attributes.

What is Apache Fortress?

Both followers of this blog (wife and boss) know about Apache Fortress.  Especially my wife.  It’s the itch that leads me to three years of work in a garage, alongside two of my brothers, who got dragged in also.

It’s also an implementation of the classic RBAC specification – ANSI INCITS 359.  If anything’s prone to exploding roles, it’s Apache Fortress.

How are we going to stop the dang exploding?

Described in a JIRA ticket  yesterday, and checked into the Apache Fortress Core Repo last night.  The idea is best explained with a story.

The Tale of Three Stooges and Three Branches

Once upon a time there were three branches, North, South and East managed by The Three Stooges that worked there, Curly, Moe and Larry.

They were nice blokes, but a tad unruly, and so we try to keep them separated.  Curly works in the East, Moe the North and Larry runs amok in the South.  All three are Tellers, but each may also substitute as coin Washers at the other two.

All is well because each Branch has only one Teller.  It’s never good when two Stooges combine without one being in charge.

Here are the Users and their Role assignments:

Curly: Teller, Washer

Moe: Teller, Washer

Larry: Teller, Washer

By now we know where this storyline’s headed.  How do we prevent one going off-script, wandering into another branch, activating Teller, and running slipshod?

The classic Role explosion theory goes like…

Create Roles by Location with User-Role assignments:

Curly: TellerEast, WasherNorth, WasherSouth

Moe: TellerNorth, WasherEast, WasherSouth

Larry: TellerSouth, WasherNorth, WasherEast

This works pretty good with three branches and two roles but what about the real-world?  How many branches will the medium-sized bank have, a thousand?  How many types of roles, at least ten?  If we follow the same Role-by-Location pattern there’d be over 10,000 Roles to manage!  We may be keeping our Stooges in check, but at the IT team’s expense.  Our roles have indeed exploded.  What now?

Time for something different, back to the earlier discussion over using attributes.  Let’s try controlling role activation by location, but store the required attributes on the user object itself.

User-Properties to store Role-Locale constraints:

Curly: Teller:East, Washer:North, Washer:South

Moe: Teller:North, Washer:East, Washer:South

Larry: Teller:South, Washer:North, Washer:East

What just happened here?  It kind of looks the same but it’s not.  We go back to only needing two Roles, but have added dynamic policies, Role-Locale, to properties stored on the User.  Our medium-sized bank only needs 10 roles not 10,000.

Now, when the security system logs in a User (createSession), it pushes its physical location attribute into the runtime context, e.g. North, South or East, along with the already present Userid attribute.  The security system compares that physical location, along with its corresponding properties stored on the User, to determine access rights, specifically which Roles may be activated into their Session.

Sprinkle in a policy that defines the role to constraint relationships.

Global Config Properties store Role-Constraint mappings:

Teller:Locale

Washer:Locale

That way when the security system activates roles it knows to perform the extra check on a particular role, and which attribute to verify.

In addition to location, we can constrain role activation by project, organization, customer, account balance, hair color, favorite ice cream, and any other form of instance data imaginable.  There may be multiple types of constraints applied to any or all roles in the system.  It truly is a dynamic policy mechanism placed on top of a traditional Role-Based Access Control System.

With this minor change to the security system, our IT guys return to the good life without worrying about exploding roles or what the Stooges are up to.  🙂

The End

 

 

New Sheriff in Town

And it don’t need no stinking badges.  Yeah, I’m mixing clichés, happens sometimes when coding long hours in a stretch.  🙂

What I’m talking about is a new access management system, released to PyPI yesterday for the first time.

py-fortress on PyPI

Considering we just started coding a couple of months ago that’s pretty good progress.

What is it?  A toolkit designed for Python3, with APIs that developers can use to do security in an RBAC-compliant way.  Today, it requires an LDAP server to store the policies, but a file-based backend will soon be ready.  No, I’m not recommending files in production, but it’s fine for getting started, within dev envs.

You can check the py-fortress project here:

py-fortress README on GitHub

Inside are links to some documents to help get started.  The quickstart doesn’t require cloning the project GIT repo, but you’ll need a Linux machine, Python3, PIP and Docker engine installed.  Everything else is covered.

Why would you want it?  That’s a long story.  It starts with an imperative to follow standards, in security processes, like authentication and authorization.  That there’s value in committing to best-practices, in this case ANSI RBAC.

It may help to know that this effort is backed by my employer — Symas.

Eventually, this code may end up inside of an Apache project, like Apache Fortress.  Or, it might land somewhere else.  It’s too early to know.  What’s certain is that it’ll remain open, available to use and learn from.

Programming in Python using the py-fortress RBAC APIs

py-fortress implements standards-based RBAC in Python. There have been numerous postings lately here about it.

Prerequisites

Very soon we’ll have a release of py-fortress that works with just the file system. This is to lower complexity of testing and developing with this package. In the meantime, an LDAP server has to be running on the network, so follow the instructions in the quickstart below to use a Docker container, if you don’t already have one installed.

0. Prepare your python module for usage by importing:
from pyfortress import (
    # model
    User,
    Role,
    Perm,
    PermObj,
    # apis:
    review_mgr, 
    admin_mgr, 
    access_mgr,
    #exception handling:
    FortressError,
    global_ids
)

Access Mgr APIs – Create Session, Check Access, Session Perms

These are used to check the policies at runtime. For example, to authenticate is create_session and authorization is check_access here.

  1. Now test signing on the RBAC way:
    def test_create_session(self):
        """
        create session
        """
        print('test_create_session')
        
        try:
            session = access_mgr.create_session(User(uid='foo1', password='secret'), False)
            if not session:
                print('test_create_session fail')
                self.fail('test_create_session fail')
            else:
                print('test_create_session pass')
                pass                        
        except FortressError as e:
            self.fail('test_create_session failed, exception=' + e.msg)

    The session will then be held on to by the client for subsequent calls like check_access and session_perms

  2. Here’s how to check a single permission:
    def test_check_access(self):
        """
        create session and check perm
        """
        print('test_check_access')
        
        try:
            session = ... obtained earlier
            result = access_mgr.check_access(session, Perm(obj_name='ShoppingCart', op_name='add'))
            if not result:
                print('test_check_access fail')
                self.fail('test_check_access fail')
            else:
                print('test_check_access pass')
                pass                        
        except FortressError as e:
            self.fail('test_check_access failed, exception=' + e.msg)
  3. Retrieve all of the permissions as a list:
    def test_session_perms(self):
        """
        create session and get perms for user
        """
        print('test_check_access')
        
        try:
            session = ... obtained earlier
            perms = access_mgr.session_perms(session)
            if not perms:
                print('test_session_perms failed')
                self.fail('test_session_perms failed')
            
            for perm in perms:
                print_perm(perm, 'session_perms: ')
            pass                        
        except FortressError as e:
            self.fail('test_session_perms failed, exception=' + e.msg)

Admin and Review APIs – Create, Read, Update, Delete

These are for programs that manage and search the data. For example admin guis, conversion programs, reporting apps.

  1. Add a user:
    def test_add_user(self):
        """
        Add a basic user
        """
        print('test_add_user')
        
        try:
            admin_mgr.add_user(User(uid='foo1', password='secret'))
            print('test_add_user success')                        
        except FortressError as e:
            self.fail('test_add_user failed, exception=' + e.msg)
  2. Add a role:
    def test_add_role(self):
        """
        Add a basic role
        """
        print('test_add_role')        
        try:
            admin_mgr.add_role(Role(name='Customer'))
            print('test_add_role success')                        
        except FortressError as e:
            self.fail('test_add_role failed, exception=' + e.msg)
  3. Add an object:
    def test_add_obj(self):
        """
        Add a basic perm object
        """
        print('test_add_obj')
        
        try:
            admin_mgr.add_object(PermObj(obj_name='ShoppingCart'))
            print('test_add_obj success')                        
        except FortressError as e:
            self.fail('test_add_obj failed, exception=' + e.msg)
  4. Add a perm:
    def test_add_perm(self):
        """
        Add a basic perm
        """
        print('test_add_perm')
        
        try:
            admin_mgr.add_perm(Perm(obj_name='ShoppingCart', op_name='add'))
            print('test_add_perm success')                        
        except FortressError as e:
            self.fail('test_add_perm failed, exception=' + e.msg)
  5. Assign a user:
    def test_assign_user(self):
        """
        Assign a user to a role
        """
        print('test_assign_user')
        
        try:
            admin_mgr.assign(User(uid='foo1'), Role(name='Customer'))
            print('test_assign_user success')                        
        except FortressError as e:
            self.fail('test_assign_user failed, exception=' + e.msg)
  6. Grant a permission:
    def test_grant_perm(self):
        """
        Grant a permission to a role
        """
        print('test_grant_perm')
        
        try:
            admin_mgr.grant(Perm(obj_name='ShoppingCart', op_name='add'), Role(name="Customer"))
            print('test_grant_perm success')                        
        except FortressError as e:
            self.fail('test_grant_perm failed, exception=' + e.msg)

 

7. Read a user:

def test_read_user(self):
    """
    Read a user that was created earlier. Expects a unique uid that points to an existing user.
    """
    print_test_name()
    try:
        out_user = review_mgr.read_user(User(uid='foo1'))
        print_user(out_user)
    except FortressError as e:            
        print_exception(e)
        self.fail()

thows exception if user is not present

8. Search for users with a matching uid:

def test_search_users(self):
    """
    Search for users that match the characters passed into with wildcard appended.  Will return zero or more records, one for each user in result set.
    """
    print_test_name()
    try:
        users = review_mgr.find_users(User(uid='foo*'))
        for user in users:
            print_user(user)
    except FortressError as e:
        print_exception(e)
        self.fail()

returns a list of type user

9. Search for users assigned a role:

def test_assigned_users(self):
    """
    Search for users that are assigned a particular role.  Will return zero or more records, one for each user in result set.
    """
    print_test_name()
    try:
        uids = review_mgr.assigned_users(Role(name='Customer'))
        for uid in uids:
            print_test_msg('uid=' + uid)
    except FortressError as e:
        print_exception(e)
        self.fail()

returns a list of type string contain the user id

10. Search for users who have a permission:

def test_perm_users(self):
    """
    Search for users that have been authorized a particular permission.  Will return zero or more records, of type user, one for each user authorized that particular perm.
    """
    print_test_name()
    try:
        users = review_mgr.perm_users(Perm(obj_name='ShoppingCart', op_name='add'))
        for user in users:
            print_user(user)
    except FortressError as e:
        print_exception(e)
        self.fail()

returns a list of type user

11. Read a role:

def test_read_role(self):
    """
    The read role expects the role name to point to an existing entry and will throw an exception if not found or other error occurs.
    """
    print_test_name()        
    try:
        out_role = review_mgr.read_role(Role(name='Customer'))
        print_role(out_role)                        
    except FortressError as e:
        print_exception(e)
        self.fail()

returns a single entity of type role

12. Search for roles matching a particular name:

def test_search_roles(self):
    """
    Search for roles that match the characters passed into with wildcard appended.  Will return zero or more records, one for each user in result set.
    """
    print_test_name()        
    try:
        roles = review_mgr.find_roles(Role(name='Customer*'))
        for role in roles:
            print_role(role)                        
    except FortressError as e:
        print_exception(e)
        self.fail()

returns a list of type role

13. Search for roles assigned to a user:

def test_assigned_roles(self):
    """
    Return the list of roles that have been assigned a particular user.  Will return zero or more records, of type constraint, one for each role assigned to user.
    """
    print_test_name()        
    try:
        constraints = review_mgr.assigned_roles(User(uid='foo1'))
        for constraint in constraints:
            print_test_msg('role name=' + constraint.name)                        
    except FortressError as e:
        print_exception(e)
        self.fail()

returns a list of type role_constraint

14. Search for roles who have granted a particular permission:

def test_perm_roles(self):
    """
    Return the list of roles that have granted a particular perm.  Will return zero or more records, containing the role names, one for each role assigned to permission.
    """
    print_test_name()        
    try:
        names = review_mgr.perm_roles(Perm(obj_name='ShoppingCart', op_name='add'))
        for name in names:
            print_test_msg('role name=' + name)                        
    except FortressError as e:
        print_exception(e)
        self.fail()

returns a list of strings containing the role names

15. Read an object:

def test_read_obj(self):
    """
    The ob_name is the only required attribute on a fortress object. Will throw an exception if not found.
    """
    print_test_name()        
    try:
        out_obj = review_mgr.read_object(PermObj(obj_name='ShoppingCart'))
        print_obj(out_obj)                        
    except FortressError as e:
        print_exception(e)
        self.fail()

return a single entity of type object

16. Search for objects matching obj_name field:

def test_search_objs(self):
    """
    Search for ojects that match the characters passed into with wildcard appended.  Will return zero or more records, one for each user in result set.
    """
    print_test_name()        
    try:
        objs = review_mgr.find_objects(PermObj(obj_name='ShoppingCart*'))
        for obj in objs:
            print_obj(obj)                        
    except FortressError as e:
        print_exception(e)
        self.fail()

returns a list of type object

17. Read a permission entry:

def test_read_perm(self):
    """
    Permissions require obj_name and op_name, obj_id is optional.  This will throw an exception if not found.
    """
    print_test_name()        
    try:
        out_perm = review_mgr.read_perm(Perm(obj_name='ShoppingCart', op_name='add'))
        print_perm(out_perm)                        
    except FortressError as e:
        print_exception(e)
        self.fail()

returns a single entity of type permission

18, Search for perms:

def test_search_perms(self):
    """
    Search for perms that match the characters passed into with wildcard appended.  Will return zero or more records, one for each user in result set.
    """
    print_test_name()        
    try:
        perms = review_mgr.find_perms(Perm(obj_name='ShoppingCart*', op_name='*'))
        for perm in perms:
            print_perm(perm)                        
    except FortressError as e:
        print_exception(e)
        self.fail()

returns a list of type perm

19. Search for perms by role:

def test_role_perms(self):
    """
    Search for perms that have been granted to a particular role.  Will return zero or more records, of type permission, one for each grant.
    """
    print_test_name()        
    try:
        perms = review_mgr.role_perms(Role(name='Customer'))
        for perm in perms:
            print_perm(perm)                        
    except FortressError as e:
        print_exception(e)
        self.fail()

returns a list of type permission

20. Search for perms by user:

def test_user_perms(self):
    """
    Search for perms that have been authorized to a particular user based on their role assignments.  Will return zero or more records, of type permission, one for each perm authorized for user.
    """
    print_test_name()        
    try:
        perms = review_mgr.user_perms(User(uid='foo1'))
        for perm in perms:
            print_perm(perm)                        
    except FortressError as e:
        print_exception(e)
        self.fail()

returns a list of type permission

END