Aeacus

Screenshot of Aeacus (Simple)

Aeacus is a powerful permissions management system that's shared across any number of applications.

Boston University uses Kerberos for all authentication, so we can easily identify who has logged into an application. Determining what that person is authorized to do is then passed off to Aeacus.

We can grant permits at a very low level, perhaps noting that a particular user is authorized to add (but not edit or delete) events on a certain calendar.

Groups of permits can then be combined to form standard roles for easy administration. Aeacus automatically adds sophistication to its user interface as the permits required get more complex. The screenshot above is for the simplest interface, where all users are administrators with the same set of privileges.

Below, we see the other extreme: where each user can have access to many parts of the application (e.g., different calendars, departments, pages) with a different access level in each.

Screenshot of Aeacus (Complicated)

Code Sample

For an application using Aeacus, the code is fantastically simple. Here's a simple way to decide whether to display a "delete" button:

require_once('PrivilegeManager/PrivilegeManager.php');
AccessControl::application('the-foobar-application');

if (AccessControl::has('delete', 17)) {
    echo '<a href="delete.php?id=17">Delete</a>
}

Of course, I'll also want to verify that the user has access in delete.php before doing anything destructive:

AccessControl::assert('delete', $_GET['id']);

// Now perform the delete (assuming a performDelete() function):
performDelete($_GET['id']);

Notice that there's nothing here about handling access errors. The ::assert call will return only if the user has the right access. If not, Aeacus automatically renders an error (in HTML or JSON), which each application can customize as needed.

Checking privileges is important, of course, but how about user management? How do those beautiful pages get added to your application? Very easily:

<script src="/aeacus/main.js"></script>
<div id="privilege-manager" data-privilege-manager="the-foobar-application">
    (Optionally include some text here to display before Aeacus loads.)
</div>

That's it! Each application can register JavaScript plugins, which tweak default behaviors, but generally this will just work out of the box.

Under the Hood

With such a simple interface, what's actually happening behind the scenes? I can't just publish the entire library, since it belongs to Boston University, but here's a peek at one of the low-level functions that gets the job done.

  private function _has(/* string */ $privilege, /* string */ $subapplication = null) {
    $this->log(LOG_DEBUG, ' +------ _has?', " $privilege.$subapplication");
    if (isset($this->_cachedHas[$privilege])) {
      if (isset($this->_cachedHas[$privilege][$subapplication]) || $this->_cachedHas[$privilege][null] || ($subapplication === null && count($this->_cachedHas[$privilege]) > 0)) {
        $this->log(LOG_DEBUG, 'verified _has', " (cache) $privilege.$subapplication");
        return true;
      }
    }
    if ($this->_is_superadmin && $this->_allow_superadmin) {
      $this->log(LOG_DEBUG, 'verified  _has', "SUPERADMIN $privilege.$subapplication");
      return true;
    }
    if (isset($this->_cachedHasNot[$privilege][$subapplication])) {
      $this->log(LOG_DEBUG, 'failed! _has', " (cache) $privilege.$subapplication");
      return false;
    }

    $checkers = array();
    foreach($this->_other_permits as $p) {
      $type = $p['type'];
      $permit = $p['permit'];

      $this->log(LOG_DEBUG, '  +----- permit ', "+ «{$p['permit']->principal_type} {$p['permit']->principal}» has {$p['permit']->application}.{$p['permit']->privilege}.{$p['permit']->subapplication}");

      if (!isset($checkers[$type]))
        $checkers[$type] = PrivilegeManager::getPrincipalType($type);
      if (!isset($checkers[$type])) {
        $this->log(LOG_ERR, '', 'No matching principal type checker for type=(' . $type . ')');
        continue;
      }

      if ($permit->privilege == $privilege) {
        $this->log(LOG_DEBUG, '  +----- permit', '| privileges match');
        if ($subapplication === null || $permit->subapplication == null || $this->subapplicationsAreEqual($subapplication, $permit->subapplication, $privilege)) {
          $this->log(LOG_DEBUG, '  +----- permit', '| subapplications match');
          if ($checkers[$type]->userIs($permit->principal, $subapplication)) {
            $this->log(LOG_DEBUG, '  +----- permit', '| user matches (privilege verified)');
            $this->log(LOG_INFO, 'verified permit', '', array('privilege' => $privilege, 'subapplication' => $subapplication, 'principal' => $permit->principal, 'principal_type' => $permit->principal_type));
            $this->_cachedHas[$permit->privilege][$permit->subapplication] = true;

            return true;
          }
        }
      } else if ($permit->application == 'system' && $permit->privilege == 'SUPERADMIN') {
        if ($checkers[$type]->userIs($permit->principal, $subapplication)) {
          $this->log(LOG_INFO, 'setting', 'SUPERADMIN');
          $this->log(LOG_INFO, 'verified _has', '', array('privilege' => $privilege, 'subapplication' => $subapplication));

          $this->_is_superadmin = true;
          return true;
        }
      }
    }

    foreach($this->_verification_hook as $obj) {
      $this->log(LOG_DEBUG, '  +----- hook', ' Hook: ' . $obj);
      if ($obj->has($privilege, $subapplication)) {
        $this->log(LOG_INFO, 'verified _has', ' Hook: ' . $obj, array('privilege' => $privilege, 'subapplication' => $subapplication));
        $this->_cachedHas[$privilege][$subapplication] = true;
        return true;
      }
    }

    $this->log(LOG_DEBUG, 'failed! _has', " $privilege.$subapplication");
    $this->_cachedHasNot[$privilege][$subapplication] = true;
    return false;
  }

You surely noticed a lot of calls to log in there. Most of this logging is done at the DEBUG level, so we only see it when developing new applications where the permits are less predictable, but we also record more permanently every permit result. And when an ::assert() fails, that's a warning.