Creating Sakai user records from an authentication service

This is a first attempt to document an advanced feature in Sakai 2.5. Please feel free to comment or edit.

Some institutions may want to authenticate users from an enterprise service such as Kerberos or LDAP, but then store the authenticated user's data in Sakai's native database tables rather than providing it externally. In an effort to support this, in Sakai 2.4 the UserDirectoryProvider interface included "createUserRecord" and "updateUserAfterAuthentication" Boolean properties. Unfortunately, the logic used was untested and flawed, and so those properties were dropped from the API in Sakai 2.5.

Instead, such institutions should use a provider that implements the new AuthenticatedUserProvider interface. If this interface's "getAuthenticatedUser" is given a legitimate login ID (not necessarily the same as the eventual user EID) and a password, the integration service should both authenticate and return a user record. Internally, that user record might be supplied by other integration code (using the usual UserDirectorProvider methods) or by searching the Sakai default user database (using the UserDirectoryService). Your "getAuthenticatedUser" method can even create or update the Sakai-stored user record internally – with a little work!

The AuthenticatedUserProviderTest integration test code includes a test of such functionality. Here's how the problem could be approached in a real-world provider:

public class MyUserDirectoryProvider implements UserDirectoryProvider, AuthenticatedUserProvider {
  private static Log log = LogFactory.getLog(MyUserDirectoryProvider.class);
  private UserDirectoryService userDirectoryService;
  private SecurityService securityService;
  private boolean userUpdatedAfterAuthentication;
  private boolean userCreatedAfterAuthentication;

  public boolean authenticateUser(String eid, UserEdit user, String password) {
    // This should never be called since we implement the new interface.
    throw new RuntimeException("authenticateUser unexpectedly called");
  }

  public UserEdit getAuthenticatedUser(String loginId, String password) {
    // DO WHATEVER AUTHENTICATION CHECK YOU NEED AND RETURN AN EID FOR
    // FURTHER USE....
    String userEid = getEidForLoginIdAndPassword(loginId, password);

    if (userEid != null) {
      return createOrUpdateUserAfterAuthentication(userEid, password);
    } else {
      return null;
    }
  }

  /**
   * Framework APIs don't always support both internal core utility services
   * (which just do their specialized job) and application-facing support services (which
   * require authorization checks). UserDirectoryService's user record modification methods
   * were originally written for application support, and so they check the permissions of the
   * current user. During the authentication process, there is no current user, and so
   * if we want to modify the Sakai-stored user record, we need to push a SecurityAdvisor
   * to bypass the checks.
   */
  private UserEdit createOrUpdateUserAfterAuthentication(String eid, String password) {
    // User record is created by provider but stored by core Sakai.
    UserEdit user = null;
    try {
      user = (UserEdit)userDirectoryService.getUserByEid(eid);

      // If you want your provider to update the existing user record,
      // do it here. Otherwise, just return what's already locally
      // stored.
      if (userUpdatedAfterAuthentication) {
        try {
          securityService.pushAdvisor(new SecurityAdvisor() {
            public SecurityAdvice isAllowed(String userId, String function, String reference) {
              if (function.equals(UserDirectoryService.SECURE_UPDATE_USER_ANY)) {
                return SecurityAdvice.ALLOWED;
              } else {
                return SecurityAdvice.NOT_ALLOWED;
              }
            }
          });
          user = userDirectoryService.editUser(user.getId());

          // SET WHATEVER FIELDS YOU NEED TO UPDATE HERE....
          // user.setLastName(user.getLastName() + ", Jr.");

          userDirectoryService.commitEdit(user);
        } catch (Exception e) {
          log.warn(e);
        } finally {
          securityService.clearAdvisors();
        }
      }
    } catch (UserNotDefinedException e) {
      // If you want your provider to create a new Sakai user
      // record, do so here.
      if (userCreatedAfterAuthentication) {
        try {
          securityService.pushAdvisor(new SecurityAdvisor() {
            public SecurityAdvice isAllowed(String userId, String function, String reference) {
              if (function.equals(UserDirectoryService.SECURE_ADD_USER)) {
                return SecurityAdvice.ALLOWED;
              } else {
                return SecurityAdvice.NOT_ALLOWED;
              }
            }
          });

          // DECIDE ON YOUR INITIAL USER FIELDS HERE....
          // String firstName = "Joe";
          // String lastName = "Hill";
          // String email = "joe@myschool.edu";
          // String userType = "Student";

          user = (UserEdit)userDirectoryService.addUser(null, eid,
            firstName, lastName, email, null, userType, null);
        } catch (Exception e1) {
          log.warn(e1);
        } finally {
          securityService.clearAdvisors();
        }
      } else {
        user = null;
      }
    }
    return user;
  }

  public void setUserDirectoryService(UserDirectoryService userDirectoryService) {
    this.userDirectoryService = userDirectoryService;
  }

  public void setSecurityService(SecurityService securityService) {
    this.securityService = securityService;
  }

  public void setUserUpdatedAfterAuthentication(boolean userUpdatedAfterAuthentication) {
    this.userUpdatedAfterAuthentication = userUpdatedAfterAuthentication;
  }

  public void setUserCreatedAfterAuthentication(boolean userCreatedAfterAuthentication) {
    this.userCreatedAfterAuthentication = userCreatedAfterAuthentication;
  }

  // OTHER UserDirectoryProvider METHODS FOLLOW:
  // ...
}

In this case, the corresponding "components.xml" might contain something like:

<bean id="org.sakaiproject.user.api.UserDirectoryProvider"
    class="edu.myschool.MyUserDirectoryProvider"
    singleton="true">
  <property name="userDirectoryService" ref="org.sakaiproject.user.api.UserDirectoryService" />
  <property name="securityService" ref="org.sakaiproject.authz.api.SecurityService" />
  <property name="userUpdatedAfterAuthentication" value="false" />
  <property name="userCreatedAfterAuthentication" value="true" />
</bean>