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

Testing the py-fortress RBAC0 System

The Command Line Interpreter (CLI) may be used to drive the RBAC System APIs,  to test, verify and understand a particular RBAC policy.

This document also resides here: README-CLI-AUTH

Prerequisites

Sample RBAC0 Policy

  • This tutorial covers the basics, RBAC Core: Many-to-many relationships between users, roles and perms and selective role activations.
  • py-fortress adds to the mix one non-standard feature: constraint validations on user and role entity activation.
  • The simple policy includes constraints being setup on user and role. Later we’ll demo a role timing out of the session.

Users

uid timeout begin_time end_time
chorowitz 30min

Roles

name timeout begin_time end_time
account-mgr 30min
auditor 5min

constraints are optional and include time, date, day and lock date validations

User-to-Role Assignments

user account-mgr auditor
chorowitz true true

Permissions

obj_name op_name
page456 edit
page456 remove
page456 read

Role-to-Permissions

role page456.edit page456.remove page456.read
account-mgr true true false
auditor false false true

 Getting Started

The syntax for testing py-fortress system commands:

clitest operation --arg1 --arg2 ... 

Where clitest executes a package script that maps to this module:

pyfortress.test.cli_test_auth

The operation is (pick one)

  •  auth => access_mgr.create_session
  • check => access_mgr.check_access
  • roles => access_mgr.session_roles
  • perms => access_mgr.session_perms
  • add => access_mgr.add_active_role
  • drop => access_mgr.drop_active_role
  • show => displays contents of session to stdout

Where operations => functions here: access_mgr.py

The args are ‘–‘ + attribute name + attribute value

  • –uid and –password from user.py
  • –obj_name, –op_name and –obj_id from perm.py
  • –role used for the role name

Command Usage Tips

  • The description of the commands, i.e. required and optional arguments, can be inferred via the api doc inline to the access_mgr module.
  • This program ‘pickles’ (serializes) the RBAC session to a file called sess.pickle, and places in the executable folder.  This simulates an RBAC runtime to test these commands.
  • Call the auth operation first, subsequent ops will use and refresh the session.
  • Constraints on user and roles are enforced. For example, if user has timeout constraint of 30 (minutes), and the delay between ops for existing session exceeds, it will be deactivated.

___________________________________________________________________________________

Setup an RBAC Policy Using admin_mgr CLI

To setup RBAC test data, we’ll be using another utility that was introduced here: README-CLI.md.

From the py-fortress/test folder, enter the following commands:

1. user add – chorowitz

$ cli user add --uid chorowitz --password 'secret' --timeout 30

user chorowitz has a 30 minute inactivity timeout

2. role add – account-mgr

$ cli role add --name 'account-mgr'

3. role add – auditor

$ cli role add --name 'auditor' --timeout 5

role auditor has a 5 minute inactivity timeout, more later about this…

4. user assign – chorowitz  to role account-mgr

$ cli user assign --uid 'chorowitz' --role 'account-mgr'

5. user assign – chorowitz to role auditor

$ cli user assign --uid 'chorowitz' --role 'auditor'

6. object add – page456

$ cli object add --obj_name page456

7. perm add – page456.read

$ cli perm add --obj_name page456 --op_name read

8. perm add – page456.edit

$ cli perm add --obj_name page456 --op_name edit

9. perm add – page456.remove

$ cli perm add --obj_name page456 --op_name remove

10. perm grant – page456.edit to role account-mgr

$ cli perm grant --obj_name page456 --op_name edit --role account-mgr

11. perm grant – page456.remove to role account-mgr

$ cli perm grant --obj_name page456 --op_name remove --role account-mgr

12. perm grant – page456.read  to role auditor

$ cli perm grant --obj_name page456 --op_name read --role auditor

________________________________________________________________________________

Perform cli_test_auth.py access_mgr Commands

From the py-fortress/test folder, enter the following commands:

1. auth – access_mgr.create_session – authenticate, activate roles:

 $ clitest auth --uid 'chorowitz' --password 'secret'
 uid=chorowitz
 auth
 success

Now the session has been pickled in on file system in current directory.

2. show – output user session contents to stdout:

$ clitest show
show
session
    is_authenticated: True
    user: 
    last_access: 
user
    cn: chorowitz
    constraint: 
    system: []
    roles: ['account-mgr', 'auditor']
    dn: uid=chorowitz,ou=People,dc=example,dc=com
    uid: chorowitz
    internal_id: 552c1a24-5087-4458-98f1-8c60167a8b7c
    reset: []
    sn: chorowitz

    User Constraint:
        name: chorowitz
        raw: chorowitz$30$$$$$$$
        timeout: 30
    User-Role Constraint[1]:
        name: account-mgr
        raw: account-mgr$0$$$$$$$
    User-Role Constraint[2]:
        name: auditor
        raw: auditor$5$$$$$$$
        timeout: 5
success

Displays the contents of session to stdout.

3. check – access_mgr.check_access – perm page456.read:

$ clitest check --obj_name page456 --op_name read
op_name=read
obj_name=page456
check
success

The user has auditor activated so unless timeout validation failed this will succeed.

4. check – access_mgr.check_access – perm page456.edit:

$ clitest check --obj_name page456 --op_name edit
op_name=edit
obj_name=page456
check
success

The user has account-mgr activated and this will succeed.

5. check – access_mgr.check_access – perm page456.remove:

$ clitest check --obj_name page456 --op_name remove
op_name=remove
obj_name=page456
check
success

The user has account-mgr activated and this will succeed.

6. get – access_mgr.session_perms:

$ clitest perms
perms
page456.read:0
  abstract_name: page456.read
  roles: ['auditor']
  internal_id: d6887434-050c-48d8-85b0-7c803c9fcf07
  obj_name: page456
  op_name: read
page456.edit:1
  abstract_name: page456.edit
  roles: ['account-mgr']
  internal_id: 02189535-4b39-4058-8daf-af0e09b0d235
  obj_name: page456
  op_name: edit
page456.remove:2
  abstract_name: page456.remove
  roles: ['account-mgr']
  internal_id: 10dea5d1-ff1d-4c3d-90c8-edeb4c7bb05b
  obj_name: page456
  op_name: remove
success

Display all perms allowed for activated roles to stdout confirms that user indeed can read, edit and remove from Page456.

7. drop – access_mgr.drop_active_role – auditor:

$ clitest drop --role auditor
drop
role=auditor
success

RBAC distinguishes between roles assigned or activated and privileges can be altered in the midst of a session.

8. roles – access_mgr.session_roles

$ clitest roles
roles
account-mgr:0
  raw: account-mgr$30$$$20180101$none$$$1234567
  end_date: none
  name: account-mgr
  timeout: 30
success

Notice the audit role is no longer active.

9. check – access_mgr.check_access – perm page456.read (again):

$ clitest check --obj_name page456 --op_name read
op_name=read
obj_name=page456
check
failed

The auditor role was deactivated so even though it’s assigned, user cannot perform as one.

10. add – access_mgr.add_active_role – auditor:

$ clitest add --role auditor
add
role=auditor
success

Now the user should be allowed to resume audit activities.

11. roles – access_mgr.session_roles:

$ clitest roles
roles
account-mgr:0
  raw: account-mgr$30$$$20180101$none$$$1234567
  end_date: none
  name: account-mgr
  timeout: 30
auditor:1
  raw: auditor$5$$$20180101$none$$$1234567
  timeout: 5
  name: auditor
  begin_lock_date: success
success

Notice the audit role has been activated once again.

12. check – access_mgr.check_access – perm page456.read (for the 3rd time):

$ clitest check --obj_name page456 --op_name read
op_name=read
obj_name=page456
check
success

The auditor role activated once again so user can do auditor things again.

13. Wait 5 minutes before performing the next step.

Allow enough time for auditor role timeout to occur before moving to the next step. Now, if you run the roles command, the auditor role will once again be missing.  This behavior is controlled by the ‘timeout’ attribute on either a user or role constraint.

14. check – access_mgr.check_access – perm page456.read:

$ clitest check --obj_name page456 --op_name read
op_name=read
obj_name=page456
check
failed

Because the auditor role has timeout constraint set to 5 (minutes), it was deactivated automatically from the session.

END

Using the py-fortress Command Line Interpreter

The Command Line Interpreter (CLI) drives the admin and review APIs,  allowing ad-hoc RBAC setup and interrogation.  More info in the README.

This document also resides here: README-CLI.

Prerequisites

Completed the setup described: README-QUICKSTART

Getting Started

The command syntax:

cli entity operation --arg1 --arg2 ... 

Where cli executes a package script that maps to this module:

pyfortress.test.cli

The entity is (pick one)

(These are source pointers to their locations in github)

The operation is (pick one):

  • add
  • mod
  • del
  • assign
  • deassign
  • grant
  • revoke
  • read
  • search

(These are just meta tags)

Argument Format

Consists of two dashes ‘- -‘ plus the attribute name and value pair, with a space between them.

--attribute_name value

if an attribute value contains white space,  enclose in single ‘ ‘ or double tics ” “.

--attribute_name 'some value' --attribute_name2 "still more values"

For example, a perm grant:

$ cli perm grant --obj_name myobj --op_name add --role 'my role'

This command invokes Python’s runtime with the program name, cli.py, followed by an entity type, operation name and multiple name-value pairs.

The above used –role is the only argument that isn’t an entity attribute name.  It’s used on user assign, deassign, perm grant, revoke operations.

Arguments as Lists

For multi-occurring attributes, pass in as a list of string values, separated by whitespace

The following arguments are lists

—phones

--phones '+33 401 851 4679' '1-212-251-1111' '(028) 9024 6609'

–mobiles

--mobiles ' 017x-1234567' '+44 020 7234 3456' '1-212-650-9632'

–emails

--emails 'f.lst@somewhere.com' 'myaccount@gmail.com' 'myworkaccount@company.com'

–props

--props 'name1:value1', 'name2:value2', 'name3:value3'

each value contains a name:value pair

Arguments as Constraint

Both the user and role entity support adding temporal constraint.

The following arguments comprise a single constraint

-name :  label for user, i.e uid

--name foo3

For users, this can be any safe text. For role, it must already be passed in, with the role’s name.

–timeout : 99 – set the integer timeout that contains max time (in minutes) that entity may remain inactive.

--timeout 30

30 minutes
–begin_time :  HHMM – determines begin hour entity may be activated.

--begin_time 0900

9:00 am
— end_time :  HHMM – determines end hour when entity is no longer allowed to activate.

--end_time 2359

11:59 pm
–begin_date : YYYYMMDD – determines date when entity may be activated.

--begin_date 20150101

Jan 1, 2015
–end_date :  YYMMDD – indicates latest date entity may be activated.

--end_date 20191231

Dec 31, 2019
–begin_lock_date :  YYYYMMDD – determines beginning of enforced inactive status

--begin_lock_date 20180602

Jun 2, 2018
–end_lock_date : YYMMDD –  end of enforced inactive status.

--end_lock_date 20180610

Jun 10, 2018
–day_mask : 1234567, 1 = Sunday, 2 = Monday, etc – day of week entity may be activated.

--day_mask 1246

Sun, Mon, Wed, Fri

all together

cli user mod --uid someuser --name anysafetext --timeout 30 --begin_time 0900 --end_time 2359 --begin_date 20150101 --end_date 20191231 --begin_lock_date 20180602 --end_lock_date 20180610 --day_mask 1246
cli role add --name manager --description 'manager works 8-5, M-F' --timeout 10 --begin_time 0800 --end_time 1700 --begin_date 20100101 --end_date none --day_mask 1246

A Few Tips More

  • These commands have a one-to-one mapping to the admin and review APIs.  For example, the perm grant command maps to the admin_mgr.grant function and perm search –uid calls review_mgr.user_perms.
  • The description of the commands, including required arguments, can be inferred via the api doc inline to the admin_mgr and review_mgr modules.
  • The program output echos the inputted arguments and the results.

Examples

Two sections,  one each for admin and review commands.  They’re not real-world use cases but include what’s currently working although this code is, how to say it, fresh.  🙂

admin mgr

a. user add

$ cli user add --uid chorowitz --password 'secret' --description 'added with py-fortress cli'
uid=chorowitz
description=added with py-fortress cli
user add
success

b. user mod

$ cli user mod --uid chorowitz --l my location --ou my-ou --department_number 123
uid=chorowitz
department_number=123
l=my location
ou=my-ou
user mod
success

c. user del

$ cli user del --uid chorowitz
uid=chorowitz
user del
success

d. user assign

$ cli user assign --uid chorowitz --role account-mgr
uid=chorowitz
role name=account-mgr
user assign
success

e. user deassign

$ cli user deassign --uid chorowitz --role account-mgr
uid=chorowitz
role name=account-mgr
user deassign
success

f. role add

$ cli role add --name account-mgr
name=account-mgr
role add
success

g. role mod

$ cli role mod --name account-mgr --description 'this desc is optional'
description=cli test role
name=account-mgr
role mod
success

h. role del

$ cli role del --name account-mgr
name=account-mgr
role del
success

i. object add

$ cli object add --obj_name page456
obj_name=page456
object add
success

j. object mod

$ cli object mod --obj_name page456 --description 'optional arg' --ou 'another optional arg'
obj_name=page456
ou=another optional arg
description=optional arg
object mod
success

k. object del

$ cli object del --obj_name page789
obj_name=page789
object del
success

l. perm add

$ cli perm add --obj_name page456 --op_name read
obj_name=page456
op_name=read
perm add
success

m. perm mod

$ cli perm mod --obj_name page456 --op_name read --description 'useful for human readable perm name'
obj_name=page456
op_name=read
description=useful for human readable perm name
perm mod
success

n. perm del

$ cli perm del --obj_name page456 --op_name search
obj_name=page456
op_name=search
perm del
success

o. perm grant

$ cli perm grant --obj_name page456 --op_name update --role account-mgr
obj_name=page456
op_name=update
role name=account-mgr
perm grant
success

p. perm revoke

$ cli perm revoke --obj_name page456 --op_name update --role account-mgr
obj_name=page456
op_name=update
role name=account-mgr
perm revoke
success

review mgr

a. user read

$ cli user read --uid chorowitz
 uid=chorowitz
 user read
 chorowitz
 uid: chorowitz
 dn: uid=chorowitz,ou=People,dc=example,dc=com 
 roles: ['account-mgr'] 
 ...
 *************** chorowitz *******************
 success

b. user search

 $ cli user search --uid c
 uid=c
 user search
 c*:0
     uid: canders
     dn: uid=canders,ou=People,dc=example,dc=com
     roles: ['csr', 'tester'] 
     ...
 *************** c*:0 *******************
 c*:1
     uid: cedwards
     dn: uid=cedwards,ou=People,dc=example,dc=com
     roles: ['manager', 'trainer'] 
     ...
 *************** c*:1 *******************
 c*:2
     uid: chandler
     dn: uid=chandler,ou=People,dc=example,dc=com
     roles: ['auditor'] 
     ...
 *************** c*:2 *******************
 c*:3
     uid: chorowitz
     dn: uid=chorowitz,ou=People,dc=example,dc=com
     roles: ['account-mgr'] 
     ...
 *************** c*:3 ******************* 
 success

c. role read

 $ cli role read --name account-mgr
 name=account-mgr
 role read
 account-mgr
 dn: cn=account-mgr,ou=Roles,dc=example,dc=com
 members: ['uid=cli-user2,ou=People,dc=example,dc=com', 'uid=chorowitz,ou=People,dc=example,dc=com']
 internal_id: 5c189235-41b5-4e59-9d80-dfd64d16372c
 name: account-mgr
 constraint: <model.constraint.Constraint object at 0x7fc250bd9e10>
 Role Constraint:
     raw: account-mgr$0$$$$$$$
     timeout: 0 
     name: account-mgr
 *************** account-mgr *******************
 success

d. role search

 $ cli role search --name py-
 name=py-
 role search
 py-*:0
     dn: cn=py-role-0,ou=Roles,dc=example,dc=com
     description: py-role-0 Role
     constraint: <model.constraint.Constraint object at 0x7f17e8745f60>
     members: ['uid=py-user-0,ou=People,dc=example,dc=com', 'uid=py-user-1,ou=People,dc=example,dc=com', ... ]
     internal_id: 04b82ce3-974b-4ff5-ad21-b19ecca57722
     name: py-role-0
 *************** py-*:0 *******************
 py-*:1
     dn: cn=py-role-1,ou=Roles,dc=example,dc=com
     description: py-role-1 Role
     constraint: <model.constraint.Constraint object at 0x7f17e8733128>
     members: ['uid=py-user-8,ou=People,dc=example,dc=com', 'uid=py-user-9,ou=People,dc=example,dc=com']
     internal_id: 70524da8-3be6-4372-a606-d8175e2ca63b
     name: py-role-1 
 *************** py-*:1 *******************
 py-*:2
     dn: cn=py-role-2,ou=Roles,dc=example,dc=com
     description: py-role-2 Role
     constraint: <model.constraint.Constraint object at 0x7f17e87332b0>
     members: ['uid=py-user-3,ou=People,dc=example,dc=com', 'uid=py-user-5,ou=People,dc=example,dc=com', 'uid=py-user-7,ou=People,dc=example,dc=com']
     internal_id: d1b9da70-9302-46c3-b21b-0fc45b863155
     name: py-role-2
 *************** py-*:2 *******************
 ...
 success

e. object read

 $ cli object read --obj_name page456
 obj_name=page456
 object read
 page456
 description: optional arg
 dn: ftObjNm=page456,ou=Perms,dc=example,dc=com
 internal_id: 1635cb3b-d5e2-4fcb-b61a-b8e91437e536
 obj_name: page456
 ou: another optional arg
 success

f. object search

 $ cli object search --obj_name page
 obj_name=page
 object search
 page*:0
     props: 
     obj_name: page456
     description: optional arg
     dn: ftObjNm=page456,ou=Perms,dc=example,dc=com
     ou: another optional arg
     internal_id: 1635cb3b-d5e2-4fcb-b61a-b8e91437e536
 page*:1
     obj_name: page123
     description: optional arg
     dn: ftObjNm=page123,ou=Perms,dc=example,dc=com
     ou: another optional arg
     internal_id: a823ef98-7be4-4f49-a805-83bfef5a0dfb
 success

g. perm read

 $ cli perm read --obj_name page456 --op_name read
 op_name=read
 obj_name=page456
 perm read
 page456.read
 internal_id: 0dc55181-968e-4c60-8755-e20fa1ce017d
 dn: ftOpNm=read,ftObjNm=page456,ou=Perms,dc=example,dc=com
 abstract_name: page456.read
 description: useful for human readable perm name
 obj_name: page456
 op_name: read
 success

h. perm search

$ cli perm search --obj_name page
 obj_name=page
 perm search
 page*.*:0
     abstract_name: page456.read
     op_name: read
     internal_id: 0dc55181-968e-4c60-8755-e20fa1ce017d
     obj_name: page456
     dn: ftOpNm=read,ftObjNm=page456,ou=Perms,dc=example,dc=com
     description: useful for human readable perm name
 page*.*:1
     roles: ['account-mgr']
     abstract_name: page456.update
     op_name: update
     internal_id: 626bca86-014b-4186-83a6-a583e39868a1
     obj_name: page456
     dn: ftOpNm=update,ftObjNm=page456,ou=Perms,dc=example,dc=com 
 page*.*:2
     roles: ['account-mgr']
     abstract_name: page456.delete
     op_name: delete
     internal_id: 6c2fa5fc-d7c3-4e85-ba7f-5e514ca4263f
     obj_name: page456
     dn: ftOpNm=delete,ftObjNm=page456,ou=Perms,dc=example,dc=com
 success

i. perm search (by role)

 $ cli perm search --role account-mgr
 perm search
 account-mgr:0
     abstract_name: page456.update 
     obj_name: page456
     op_name: update
     roles: ['account-mgr']
     dn: ftOpNm=update,ftObjNm=page456,ou=Perms,dc=example,dc=com
     internal_id: 626bca86-014b-4186-83a6-a583e39868a1
 account-mgr:1
     abstract_name: page456.delete
     obj_name: page456
     op_name: delete
     roles: ['account-mgr']
     dn: ftOpNm=delete,ftObjNm=page456,ou=Perms,dc=example,dc=com
     internal_id: 6c2fa5fc-d7c3-4e85-ba7f-5e514ca4263f
 success

j. perm search (by user)

 $ cli perm search --uid chorowitz
 perm search
 chorowitz:0
     dn: ftOpNm=update,ftObjNm=page456,ou=Perms,dc=example,dc=com
     internal_id: 626bca86-014b-4186-83a6-a583e39868a1
     roles: ['account-mgr']
     abstract_name: page456.update
     obj_name: page456
     op_name: update
 chorowitz:1
     dn: ftOpNm=delete,ftObjNm=page456,ou=Perms,dc=example,dc=com
     internal_id: 6c2fa5fc-d7c3-4e85-ba7f-5e514ca4263f
     roles: ['account-mgr']
     abstract_name: page456.delete
     obj_name: page456
     op_name: delete
 success

END

Next up, Testing the py-fortress RBAC0 System

 

Distal Biceps Disruption

Last week while helping my wife load a household appliance we were donating into her aunt’s pickup was the sickening sound of my right bicep detaching itself from the elbow distal tendon.  The pain was bad, of course, but the realization of the extent of the injury was worse.  Suddenly plans of completing a 3rd consecutive DirtyKanza were nixed.  In addition to a surgical reattachment, performed yesterday, there’s several months of recovery and rehab before I can return to riding once again.

In situations like this one must focus on the positives.

  • Injury to right arm and I’m left-handed.
  • We have health insurance and can take the steps necessary for a full recovery.
  • Support of a wonderful family, friends and employer.
  • I can still code.
  • Inside trainer to maintain conditioning on order.

There’s not much value in thinking about the negatives or what-ifs.  Life has a way of throwing curve balls.  Find a way of knocking the cover off it anyway.

As far as what’s next.  I already mentioned the trainer which will be a way to maintain conditioning during the lull.

IMG_20180322_135309_552

Once the splint comes off and the brace is opened enough to hold on I am going to try and get some rides in (despite doctor’s orders) and we’ll see what happens come June 2nd.

DK200-Mud-Anon

photo courtesy of http://www.swiftwick.com/sw/cycling/kanza-does-not-care-

 

 

Introducing a pythonic RBAC API

py-fortress is a Python API implementing Role-Based Access Control level 0 – Core.  It’s still pretty new so there’s going to be some rough edges that will need to be smoothed out in the coming weeks.

To try it out, clone its git repo and use one of the fortress docker images for OpenLDAP or Apache Directory.  The README has the details.

py-fortress git repo

The API is pretty simple to use.

Admin functions work like this

# Add User:
admin_mgr.add_user(User(uid='foo', password='secret'))

# Add Role:
admin_mgr.add_role(Role(name='customer'))

# Assign User:
admin_mgr.assign(User(uid='foo'), Role(name='customer'))

# Add Permission:
admin_mgr.add_perm_obj(PermObj(obj_name='shopping-cart'))
admin_mgr.add_perm(Perm(obj_name='shopping-cart', op_name='checkout'))

# Grant:
admin_mgr.grant(Perm(obj_name='shopping-cart', op_name='checkout'),Role(name='customer')) 

Access control functions

# Create Session, False means mandatory password authentication.
session = access_mgr.create_session(User(uid='foo', password='secret'), False)

# Permission check, returns True if allowed:
result = access_mgr.check_access(session, Perm(obj_name='shopping-cart', op_name='checkout'))

# Get all the permissions allowed for user:
perms = access_mgr.session_perms(session)

# Check a role:
result = access_mgr.is_user_in_role(session, Role(name='customer'))

# Get all roles in the session:
roles = access_mgr.session_roles(session)

 

In addition, there’s the full compliment of review apis as prescribed by RBAC.  If interested, look at the RBAC modules:

Each of the modules have comments that describe the functions, along with their required and optional attributes.

Try it out and let me know what you think.  There will be a release in the near future that will include some additional tooling.  If it takes off, RBAC1 – RBAC3 will follow.

Why I Ride

Say what you will about cycling, but it affords time for thoughtful contemplation.

Why am I doing this?  There are plenty of reasons not, starting with it being hard compared to other forms of transportation.

That the roadways don’t accommodate — we’re at best an annoyance, leading to spats and scuffles of varying severities.

It’s not convenient to commute this way requiring time consuming preparation.

Not a particularly time effective form of transportation — much faster to get into a car and drive.

Complications on arrival not shared with motorist; attired in such a way that is comically out of place of today’s societal norms.

Summing up the pros/cons, it can be hard to make a convincing argument for daily commuting on a bike.

So why do it?  Before that can be answered we have to delve into this issue a bit deeper.  What are the cons of commuting by car?

  1. The average automobile spews about 5 metric tons of carbon dioxide into the atmosphere a year.1
  2. Driving increases stress levels and encourages a sedentary lifestyle.
  3. The cost to maintain the nation’s highways, roads, bridges and streets is hard to calculate but is probable > $100 billion US.2
  4. The yearly cost to maintain an automobile about $9,000 US.3

Back to how riding affords time to think — more questions to ponder…

  1. What happens when everyone on the planet is driving a car?  (How much longer can the atmosphere absorb the greenhouse emissions before lasting consequences)
  2. How much longer can governments afford to spend sizable portions of tax revenues maintaining roadways?
  3. When will petroleum run out and what then?

More riding, more thoughts… at the turn of the century (twentieth), the internal combustion engine (and its supply chain) was perfected, cycling was widespread and automobiles rarely seen on the roads.

We all know what happens next, but what if otherwise?  The bicycle the target form of personal transportation and the automobile for public and commercial usage only.  Cyclist in the majority; living close by their place of worship, study, work, entertainment, etc…  Commuters would be traveling slower and have to talk to one another — maybe better for politics and settling disputes.

What would our environment look like — still polluted with carbons?  What of our hospitals — full of unfit patients?  What of our cities — divided by giant, ugly roadways or connected by scenic paths?

Is there a middle road?  Meanwhile I ride and long for the day everyone follows…

BDB-2017-PV

Footnotes

1. Greenhouse Gas Emissions from a Typical Passenger Vehicle
2. What is the federal government’s annual investment in transportation improvements?
3. Annual Cost of Ownership`

Why I love LDAPCon

This post is loosely based on a lightning talk last week in Brussels.  We had a few minutes to fill and I felt compelled to spill my guts, despite having nothing prepared.

For those that have never heard about LDAPCon, it’s a biennial event, first held in ’07, with rotating venues, always in interesting places.  The talks are a 50/50 split between technology providers and usages.

You can check out this year’s talks, along with sides — here.

It’s not a ‘big’ conference — attendance hovers between 70 and 80.  It doesn’t last very long — about two days.  There’s very little glitz or glory.  You won’t find the big vendors with their entourages of executives and marketing reps in the expo.  Nor are there giveaways, flashy parties or big name entertainers.  For the most part the media and analysts ignore it; participants don’t get much exposure to the outside world.  Everyone just sits in a single, large conference room for the duration and listens to every talk (gasp).

So what is it about this modest little gathering that I love so much?

Not my first rodeo.  The end of my career is much closer than its beginning, and I’ve been to dozens of conferences over the decades.  Large, small and everything in between.  For example, I’ve attended JavaOne twelve times and been to half a dozen IBM mega conferences.

Let’s start with relevance.  Contrary to what you may think LDAP is not going away.  It’s not sexy or exciting.  Depending on your role in technology you may not even have heard of it (although I can guarantee that your information is housed within its walls).  But it’s useful.  If you’re interested in security you better understand LDAP.  If you choose not to use it you better have good reasons.  Ignore at your peril.

I’ve been working with LDAP technology (as a user) for almost twenty years.  When I first started, back in the late ’90’s there was a fair amount of hype behind it.  Over the years that hype has faded of course.  As it faded, I found myself alone in the tech centers.  In other words, I was the only one who understood how it worked, and why it was needed.  As the years passed, I found my knowledge growing stale.  Without others to bounce ideas there’s little chance for learning. You might say I was thirsting for knowledge.

My first LDAPCon was Heidelberg in ’11.  It was as if I had found an oasis after stumbling about in the desert alone for years.  AH — at last others who understand and from whom I can learn and work with.

Many conferences are rather impersonal.  This is understandable of course, because the communities aren’t well established or are so large that it would be impossible to know everyone, or even a significant minority.

The leaders of these large technology communities are more like rock stars than ordinary people.  Often (not always) with oversized egos fed by the adoration of their ‘fans’.  This is great if you are seeking an autograph or inspiration, but not so much if you’re wanting help or validation of ideas.

Not the case at LDAPCon.  You’ll still find the leaders and architects, but not the egos.  Rather, they understand the importance of helping others find their way and encourage interaction and collaboration.

Sprinkle in with these leaders earnest newcomers.  Much like when I arrived in Heidelberg the pattern repeats.  These newcomers bring energy and passion that fuels the ecosystem and helps to stave off obsolescence.  There is a continuous stream of ideas coming forth ensuring the products and protocols remain relevant.

The newcomers are welcomed with open arms and not ignored.  This creates a warm atmosphere for collaboration.  New ideas are cherished not shunned.  Newcomers are elevated not marginalized.

Not a marketing conference.  You won’t find booths (like at a carnival) where passersby are cajoled and enticed by shiny lights and glitzy demos.  Where on the last day they warily pack up their rides and go to the next stop on the circuit.

Not a competitive atmosphere, rather collaborative.  Here is where server vendors like Forgerock, Redhat, Microsoft, Symas, and others meet to work together on common goals, improving conditions for the community.  They don’t all show up to every one, but are certainly welcome when they do.

Here, on the last day, there is some sadness.  We go and have some beer together, share war stories (one last time) and make plans for the future.

The next LDAPCon will probably again be held in Europe.  Perhaps Berlin or Brno.

I can hardly wait.

20171021_155827

2017 Dirty Kanza Finish Line

Note: This post is about my second Dirty Kanza 200 experience on June 3, 2017.


It’s broken into seven parts:

Part I – Prep / Training

Part II – Preamble

Part III – Starting Line

Part IV – Checkpoint One

Part V – Checkpoint Two

Part VI – Checkpoint Three

Part VII – Finish Line


Regroup

I went looking for Derrick but couldn’t find him.  A woman, found out later his wife…

“Are you John?” she asked.

I replied with my name and didn’t make the connection.  I’d forgotten the color of his support team and he got my name wrong so that made us even.

He caught up ten miles later, by then chasing the fast chicks.  I called out as they zoomed past, wished them well.  This is how it works.  Alliances change according to the conditions and needs from one moment to the next.

A lone rider stopped at the edge of downtown — Rick from Dewitt, Arkansas.  He was ready for takeoff.

“You headed out, how bout we team up?”  I asked matter-of-factly.  The deal was struck and then there were two.

Eventually, maybe twenty miles later, we picked up Jeremy, which made three.  It worked pretty well.  Not much small talk, but lots of operational chatter.  You’d thought we were out on military maneuvers.

  • “Rocks on left.”
  • “Mud — go right!”
  • “Off course, turning around.”
  • “Rough! Slowing!”

There were specializations.  For example, Jeremy was the scout.  His bike had fat tires and so he’d bomb the downhills, call back to us what he saw, letting us know of the dangers.  Rick did most of the navigating.  I kept watch on time, distance and set the pace.

By this time we were all suffering and made brief stops every ten miles or so.  We’d agreed that it was OK, had plenty of time, and weren’t worried.

Caught up with Derrick six miles from home.  Apparently he couldn’t keep up with the fast chicks either, but gave it the college try, and we had a merry reunion.

We rolled over the finish line somewhat past 2:00 am.

IMG_5452

Rick and I crossing the FL

Here’s the official video feed:

https://results.chronotrack.com/athlete/index/e/29334039

And the unofficial one:

My support team was there along with a smattering of hearty locals to cheer us and offer congratulations.

Jeremy, Rick and I had a brief moment where we congratulated each other before LeLan handed over our Breakfast Club finishers patches and I overheard Rick in his southern drawl…

“I don’t care if it does say breakfast club on there.”

Next were the hugs and pictures with my pit crew and I was nearly overcome with emotion.  Felt pretty good about the finish and I don’t care if it says breakfast club on there either.


Acknowledgements

In addition to my pit crew…

My wife Cindy deserves most of the credit.  She bought the bike four years ago that got me all fired up again about cycling.  Lots of times when I’m out there riding I should be home working.  Throughout this she continues to support without complaint.  Thanks baby, you’re the best, I love you.

Next, are the guys at the bike shop — Arkansas Cycle and Fitness, my support team back home in Little Rock.  They tolerate abysmal mechanical abilities, patiently listen to requirements, and teach when need be (often).  Time and again the necessary adjustments were made to correct the issues I was having with the bike.  They’ve encouraged and cheered, offered suggestions on routes, tactics, training, nutrition, hydration and everything else related to the sport of endurance cycling.

Finally, my cycling buddies — the Crackheads.  Truth be known they’re probably more trail runners than cyclists, but they’re incredible athletes, from whom I’ve learned much about training for these types of endurance events.  In the summertime, when the skeeters and chiggers get too bad for Arkansas trail running, they come out and ride which makes me happy.


Screen Shot 2017-06-20 at 12.01.19 AM


The End

2017 Dirty Kanza Checkpoint Three

Note: This post is about my second Dirty Kanza 200 experience on June 3, 2017.


It’s broken into seven parts:

Part I – Prep / Training

Part II – Preamble

Part III – Starting Line

Part IV – Checkpoint One

Part V – Checkpoint Two

Part VI – Checkpoint Three

Part VII – Finish Line


Don’t Worry Be Happy

My thoughts as I roll out of Eureka @ 3:30pm…

  • Thirty minutes at a checkpoint is too long, double the plan, but was overheated and feel much better now.
  • I’m enjoying myself.
  • It’s only a hundred miles back to Emporia, I could do that in my sleep.
  • What’s that a storm cloud headed our way?  It’s gonna feel good when it gets here.

Mud & Camaraderie

That first century was a frantic pace and there’s not much time or energy for team building.  We help each other out, but it’s all business.

The second part is when stragglers clump into semi-cohesive units.   It’s only natural and in any case, foolish to ride alone.  A group of riders will always be safer than one, assuming everyone does their job properly.  Each new set of eyes brings another brain to identify and solve problems.

There’s Jim, who took a few years off from his securities job down in Atlanta, Georgia to help his wife with their Montessori school, and train for this race.  He and I teamed up during the first half of the third leg.  As the worst of the thunderstorms rolled over.

Before we crossed the US hiway 54, a rider was waiting to be picked up by her support team.  Another victim of muddy roads, a derailleur twisted, bringing an early end to a long day.  We stopped, checked and offered encouragement as a car whizzed by us.

“That’s a storm chaser!!”, someone called out, leaving me to wonder just how bad these storms were gonna get.

Derrick, is an IT guy from St. Joseph, Missouri, riding a single-speed bike on his way to a fifth finish, and with it a Goblet commemorating 1000 miles of toil.

We rode for a bit at the end of the third, right at dusk.  My GPS, up to now worked flawlessly had changed into the nightime display mode and I could no longer make out which lines to follow, missed a turn and heard the buzzer telling me I’d veered off course.

I stopped and pulled out my cue sheets.  Those were tucked safely and sealed to stay nice and dry.  What, I forgot to seal, its pages wet, stuck together and useless?

I was tired and let my mind drift.  Why didn’t I bring a headlamp on this leg?  I’d be able to read the nav screen better.  And where is everybody?  How long have I been on the wrong path?  Am I lost?

Be calm.  Get your focus and above all think.  What about the phone, maps are on it too.  It’s almost dead but plenty of reserve power available.

Just then Derrick’s dim headlight appeared in the distance.  He stopped and we quietly discussed my predicament.  For some reason his GPS device couldn’t figure that turn out either.  It was then we noticed tire tracks off to our right, turned and got back on track, both nav devices mysteriously resumed working once again.

Jeremy is the service manager at one of the better bike shops in Topeka, Kansas.  He’s making a third attempt.  Two years ago, he broke down in what turned into a mudfest.  Last year, he completed the course, but twenty minutes past due and didn’t make the 3:00 am cutoff.

His bike was a grinder of sorts with some fats.  It sounded like a Mack truck on the downhills, but geared like a mountain goat on the uphills.  I want one of them bikes.  Going to have to look him up at that bike shop one day.

Last year I remembered him lying at the roadside, probably ten maybe fifteen miles outside of Emporia.

“You alright?”, we stopped and asked.  It was an hour or more past midnight and the blackest of night.

“Yeah man, just tired, and need to rest a bit.  You guys go on, I’m fine”, he calmly told us.

There’s the guy from Iowa, who normally wouldn’t be at the back-of-the-pack (with us), but his derailleur snapped and he’d just converted to a single-speed as I caught up with him, and his buddy.  This was a first attempt for both.  They’d been making good until the rains hit.

Or the four chicks, from where I do not know, who were much faster than I, but somehow kept passing me.  How I would get past them again remains a mystery.

Also, all of the others, whose names can’t be placed, but the stories can…

Storms

281712095_10228226691750303_5428997242501586469_n

Seven miles into that third leg came the rain.  It felt good, but introduced challenges.  The roads become slippery and a rider could easily go down.  They become muddy and the bike very much wants to break down.

Both are critical risk factors in terms of finishing.  One’s outcome much worse than the other.

Fortunately, both problems have good solutions.  The first, slow down the descents, pick through the rocks, pools of mud and water — carefully.  If in doubt stop and walk a section, although I never had to on this day, except for that one crossing with peanut butter on the other side.

By the way, these pictures that I’m posting are from the calmer sections.  It’s never a good idea to stop along a dangerous roadside just to take one.  That will create a hazard for the other riders, who then have to deal with you in their pathways which limits their choices for a good line.  When the going is tricky, keep it moving, if possible to do so safely.

The second problem means frequent stops to flush the grit from the drivetrains.  When it starts grinding, it’s time to stop and flush.  Mind the grind.  Once I pulled out two centimeter chunks of rocks lodged in the derailleurs and chain guards.

Use whatever is on hand.  River, water, bottles, puddles.  There was mud — everywhere.  In the chain, gears and brakes.  It’d get lodged in the pedals and cleats of our shoes making it impossible to click in or (worse) to click out.  I’d use rocks to remove other rocks or whatever is handy and/or expedient.  It helps to be resourceful at times like this.  That’s not a fork, it’s an extended, multi-pronged, mud and grit extraction tool.

The good folks alongside the road were keeping us supplied with plenty of water.  It wasn’t needed for hydration, but for maintenance.  I’d ask before using it like this, to not offend them.  Pouring their bottles of water over my bike, but they understood and didn’t seem to mind.

We got rerouted once because the water crossing decided it wanted to be a lake.  This detour added a couple of miles to a ride that was already seven over two hundred.

The rain made for slow but I was having a good time and didn’t want the fun to end.

Enjoy this moment.  Look over there, all the flowers growing alongside the road.  The roads were still muddy but the fields were clean and fresh, the temperatures were cool.

18839605_10211238844279637_2474816412111977923_o

wild flowers along the third leg

Madison (once again)

Rolled in about 930p under the cover of night.

DBcdbilXkAA5Knz

930p @ Madison CP3

After all that fussing over nameplates in the previous leg and found out it was mounted incorrectly.  It partially blocked the headlight beam and had to be fixed.

I was in good spirits, but hungry, my neck ached, and my bike was in some serious need of attention.  All of this was handled with calm efficiency by Kelly & Co.

Kyle, who’s an RN, provided medical support with pain relievers and ice packs.  They knew I liked pizza late in the race and had a slice or two. It may not sound like much now, but gave me the needed energy boost, from something that doesn’t get squeezed out of a tube.

As soon as we got the nameplate fixed it was time to get the drivetrain running smoothly once again.

All the while, Kelly and Mom were assisting and directing.  There’s the headlamp needing to be mounted, fresh battery packs, change to the clear lens on the glasses, socks, gloves, cokes, energy drinks, refilling water tanks, electrolytes, gels and more.  There’s forty-some to go, total darkness, unmarked roads.  Possibly more mud on the remaining B roads.  Weather forecast clear and mild.

Let’s Finish This

“Who are you riding with?”, someone called out as I was leaving.

“Derrick and I are gonna team up”, I called back, which was true, that was the plan as we rolled into town.  Now I just had to find him.  Madison was practically deserted at this hour, its checkpoint regions, i.e. red, green, blue, orange, were spread out, and what color did he say he was again??

Twenty two minutes spent refueling at checkpoint three and into the darkness again.  That last leg started @ 10 pm with 45 miles to go.  I could do that in my sleep, may need to.

Screen Shot 2017-06-17 at 9.32.30 PM

Next Post: Part VII – Finish Line