Generating eduPersonAffiliation from your internal directory

This page is intended to give you some ideas about how to generate an eduPersonAffiliation attribute that is useful to SAFIRE by reusing existing information you may already have in your internal directory services.

What’s shown below are SimpleSAMLphp config snippets, but the ideas translate to pretty much all identity provider software. If you’re not using SimpleSAMLphp, hopefully the comments help you understand what is going on. All the authproc filters shown here are documented in SimpleSAMLphp’s docs.

eduPersonAffiliation

The goal is to generate an attribute containing a series (array) of values that represent how a particular end-user is affiliated with your institution. eduPersonAffiliation uses a controlled language that likely does not reflect how you refer to these people within your institution, but most organisations have some form of identity management that allows them to group different types of users together.

However, it is important to note that you do not need to be able to produce all these categories to be able to assert eduPersonAffiliation. If you only have some of the information, you should release as much as you have as accurately as possible. At the very least, every IdP should be able to assert “member” for its members.

The REFEDs ePSA usage comparison provides guidance for mapping the controlled language. In South Africa, we’ve tied this language to the data elements used in HEMIS. The table below summarises some common South Africa situations.

Role Value(s)
Undergraduate student member; student
Postgraduate student who did undergrad at same institution member; student; alum
Convocation/alumni alum
Staff member (academic, lecturer or researcher) member; employee; facility
Staff member (academic, unpaid) member; facility
Staff member (administrative, professional or management) member; employee; staff
Staff member (unskilled, trade or craft) member; employee
Staff member at affiliated institute affiliate (possibly also "member", depending on how close the ties are)
Any other member of the institution in good standing member

Nesting of eduPersonAffiliation values

The following figure shows how the most commonly used values nest together, allowing a check for an outer box to include all people covered by an inner one.

Nesting of eduPersonAffiliation values in SAFIRE
It should be apparent that this means that the vast majority of users are expected to have more than one value for eduPersonAffiliation.

Case 1: Containers / distinguished name

If you’ve organised your directory into containers that reflect the above roles, you may be able to use the distinguishedName (or entryDN in some LDAP directories) attribute to derive eduPersonAffiliation. The example below also sets eduPersonPrimaryAffiliation.

20 => array(
  'class' => 'core:PHP',
  'code' => '
    $dnou = preg_replace("/^.*,ou=(\w+),dc=example,dc=local/", "$1", $attributes["distinguishedName"][0]);
    switch (strtoupper($dnou)) {
      case "STAFF":
        $a = array("member","staff"); $p = "staff";
        break;
      case "STUDENTS":
        $a = array("member","student"); $p = "student";
        break;
      case "GUESTS":
        $a = array("affiliate"); $p = "affiliate";
        break;
      default:
        $a = array("library-walk-in"); $p = "library-walk-in";
    }
    $attributes["eduPersonAffiliation"] = $a;
    $attributes["eduPersonPrimaryAffiliation"][0] = $p;
  ',
),

Case 2: Group membership

Many organisations use groups to record roles and affiliations. These are usually exposed in LDAP as the groupMembership or memberOf attributes, which typically contain the distinguished name(s) of group object(s).

20 => array(
  'class' => 'core:PHP',
  'code' => '
    $a = array();
    /* loop through group DNs */
    foreach ($attributes["groupMembership"] as $group) {
      /* simple pattern matching */
      if (preg_match("/cn=(under|post)grads/", $group)) {
        $a[] = "member"; $a[] = "student"
      } elseif (preg_match("/cn=staff/", $group)) {
        a[] = "member"; $a[] = "employee";
      } elseif (preg_match("/cn=teaching/", $group)) {
        $a[] = "faculty";
      }
    }
    /* combinations of groups */
    if (in_array("employee", $a) and !in_array("faculty", $a)) {
      $a[] = "staff"
    }
    /* default */
    if (empty($a)) {
      $a[] = "library-walk-in";
    }
    $attributes["eduPersonAffiliation"] = $a;
  ',
),

Case 3: Single attribute

You may have an attribute that reflects a user’s role(s) in a single delimited string, in which case you could use core:PHP to split that string and set appropriate affiliation(s):

/*
 * assumes a ms-Exch-Extension-Attribute15 attribute
 * containing a comma separated list.
 * e.g. ms-Exch-Extension-Attribute15 = student,convocation
 */
20 => array(
  'class' => 'core:PHP',
  'code' => '
    $ourroles = preg_split("/\s*,\s*", $attributes["ms-Exch-Extension-Attribute15"][0]);
    $a = array();
    foreach ($ourroles as $role) {
       switch (strtoupper($role)) {
         case "STUDENT":
            $a[] = "student"; $a[] = "member"; break;
         case "ADMIN":
            $a[] = "staff"; $a[] = "employee"; $a[] = "member"; break;
         case "ACADEMIC":
            $a[] = "faculty"; $a[] = "employee"; $a[] = "member"; break;
         case "CONVOCATION":
            $a[] = "alum"; break;
         case "3RDPARTY":
            $a[] = "affiliate"; break;
       }
    }
    $attributes["eduPersonAffiliation"] = array_unique($a);
  ',
),

Case 4: External source

You may store your roles in a separate database. How to deal with this is beyond the scope of this document. However, the two examples below may give you some ideas of what is possible.

4.1: LDAP directory

The ldap:AttributeAddFromLDAP filter may assist if you need to get additional attributes from a separate LDAP directory. For example, to add an attribute from an AD global catalog, you may do something like this:

20 => array(
  'class' => 'ldap:AttributeAddFromLDAP',
  'ldap.hostname' => 'ldaps://ldap.example.ac.za:3269',
  'ldap.username' => 'simplesamlphp@example.ac.za',
  'ldap.password' => 'your password',
  'ldap.basedn' => 'ou=Users,dc=example,dc=ac,dc=za',
  'attributes' => array('eduPersonAffiliation' => 'sourceAttribute'),
  'search.filter' => '(userPrincipalName =%eduPersonPrincipalName%)',
),

4.2: SQL database

SAFIRE has developed a sqlattribs:AttributeFromSQL module that may help, either as a code example for how to develop your own module or directly as an attribute source.

20 => array(
  'class' => 'sqlattribs:AttributeFromSQL',
  'attribute' => 'eduPersonPrincipalName',
  'limit' => array('eduPersonAffiliation',),
  'replace' => false,
  'database' => array(
    'dsn' => 'mysql:host=localhost;dbname=simplesamlphp',
    'username' => 'yourDbUsername',
    'password' => 'yourDbPassword',
    'table' => 'AttributeFromSQL',
  ),
),

Which would allow you to store eduPersonAffiliation in an external SQL database, something like this:

INSERT INTO AttributeFromSQL (uid, sp, attribute, value) VALUES ('user@example.org', '%', 'eduPersonAffiliation', 'faculty');
INSERT INTO AttributeFromSQL (uid, sp, attribute, value) VALUES ('user@example.org', '%', 'eduPersonAffiliation', 'member');
INSERT INTO AttributeFromSQL (uid, sp, attribute, value) VALUES ('other@example.org', '%', 'eduPersonAffiliation', 'alum');

Case 5: Everyone is a member

As noted above, even if you have no information in your directory, you can probably still assert that everyone is a member. This, of course, assumes you only provide accounts to people who’re in good standing with your institution ;-).

20 => array(
  'class' => 'core:AttributeAdd',
  'eduPersonAffiliation' => array('member'),
),

Small organisations may be able to go further than that. For example, research agencies may be able to assert that all their members are also employees:

20 => array(
  'class' => 'core:AttributeAdd',
  'eduPersonAffiliation' => array('member', 'employee'),
),

Generating eduPersonScopedAffiliation

Once you have a valid eduPersonAffiliation, it should be fairly straightforward to generate a scoped version of this. The cleanest method is to add a schacHomeOrganization attribute and to use this wherever scoping is required.

/* add a static schacHomeOrganization attribute */
10 => array(
  'class' => 'core:AttributeAdd',
  'schacHomeOrganization' => array('example.ac.za'),
),
/* scope uid as eduPersonPrincipalName */
11 => array(
  'class' => 'core:ScopeAttribute',
  'scopeAttribute' => 'schacHomeOrganization',
  'sourceAttribute' => 'eduPersonAffiliation',
  'targetAttribute' => 'eduPersonScopedAffiliation',
),

The SAFIRE federation hub will automatically do the above if you don’t supply eduPersonScopedAffiliation. However, doing it yourself gives you much greater control over the interpretation of the scope. This can be useful if you need to provide more granular access control.

South African Identity Federation