Hooks Proof of Concept

Hooks Introduction
Getting started with hooks
Hooks Example - Assign a Unix id to each new group

This document is about a proof of concept on hooks for a group change and a membership change.

The current progress is a working unit test for each, and a Grouper UI example for a group change (member change is in progress).

Grouper UI group example

I added a hook to the Grouper UI which is a veto hook which will not allow a group to be created which is not in the "penn" folder (which is a subfolder of root).

To implement this, it requires a group hook class:

/*
 * @author mchyzer
 * $Id: GroupHooksImplExample.java,v 1.1.2.1 2008/06/11 06:19:38 mchyzer Exp $
 */
package edu.internet2.middleware.grouper.ui.hooks;

import org.apache.commons.lang.StringUtils;

import edu.internet2.middleware.grouper.GrouperConfig;
import edu.internet2.middleware.grouper.hooks.GroupHooks;
import edu.internet2.middleware.grouper.hooks.beans.HooksGroupPreInsertBean;
import edu.internet2.middleware.grouper.hooks.HookVeto;
import edu.internet2.middleware.grouper.internal.dao.GroupDAO;


/**
 * test implementation of group hooks for test
 */
public class GroupHooksImplExample extends GroupHooks {

  /**
   * @see edu.internet2.middleware.grouper.hooks.GroupHooks#groupPreInsert(edu.internet2.middleware.grouper.hooks.beans.HooksGroupPreInsertBean)
   */
  @Override
  public void groupPreInsert(HooksGroupPreInsertBean preInsertBean) {

    GroupDAO groupDAO = preInsertBean.getGroupDao();
    String name = StringUtils.defaultString((String)groupDAO.getAttributes().get(GrouperConfig.ATTR_NAME));
    if (!name.startsWith("penn:")) {
      throw new HookVeto("hook.veto.group.name.prefix", "group must be in the 'penn' top level folder");
    }
  }

}

 And it requires a configuration in the grouper.properties:

#implement edu.internet2.middleware.grouper.hooks.GroupHooks
hooks.group.class=edu.internet2.middleware.grouper.ui.hooks.GroupHooksImplExample

 The result when trying to add a group not in that folder, is:

 Grouper UI membership example

 I added a hook to the Grouper UI (unfinished... was getting too fancy) for a membership veto that if the group type is grouperLoader, do not allow group memberships.  However, if the user is a wheel group user, then allow it but put a warning on screen

Here is the starting point for the Java:

/*
 * @author mchyzer
 * $Id: MembershipHooksImplExample.java,v 1.1.2.1 2008/06/11 06:19:38 mchyzer Exp $
 */
package edu.internet2.middleware.grouper.ui.hooks;

import edu.internet2.middleware.grouper.Group;
import edu.internet2.middleware.grouper.GroupType;
import edu.internet2.middleware.grouper.GroupTypeFinder;
import edu.internet2.middleware.grouper.SchemaException;
import edu.internet2.middleware.grouper.hooks.MembershipHooks;
import edu.internet2.middleware.grouper.hooks.beans.GrouperBuiltinContextType;
import edu.internet2.middleware.grouper.hooks.beans.GrouperContextType;
import edu.internet2.middleware.grouper.hooks.beans.HooksContext;
import edu.internet2.middleware.grouper.hooks.beans.HooksMembershipPreAddMemberBean;
import edu.internet2.middleware.grouper.hooks.HookVeto;


/**
 * test implementation of group hooks for test
 */
public class MembershipHooksImplExample extends MembershipHooks {

  /**
   * @see edu.internet2.middleware.grouper.hooks.MembershipHooks#membershipPreAddMember(edu.internet2.middleware.grouper.hooks.beans.HooksMembershipPreAddMemberBean)
   */
  @Override
  public void membershipPreAddMember(HooksMembershipPreAddMemberBean preAddMemberBean) {
    HooksContext hooksContext = preAddMemberBean.getHooksContext();
    GrouperContextType grouperContextType = hooksContext.getGrouperContextType();

    //only care about this if not grouper loader
    if (!grouperContextType.equals(GrouperBuiltinContextType.GROUPER_LOADER)) {

      //if the act as user is is in the wheel group, then just admonish
      if (hooksContext.isSubjectActAsInGroup("penn:etc:sysAdminGroup")) {

        //add warning to system

      } else {

        Group group = preAddMemberBean.getGroup();
        GroupType groupType = null;
        try {
          groupType = GroupTypeFinder.find("grouperLoader");
        } catch (SchemaException se) {
          throw new RuntimeException(se);
        }
        if (group.hasType(groupType)) {
          throw new HookVeto("hook.veto.loader.membership", "the membership of this group is automatically managed and does not permit manual changes");
        }

      }



    }
  }

}

 Here is the grouper.properties config:

#implement edu.internet2.middleware.grouper.hooks.MembershipHooks
hooks.membership.class=edu.internet2.middleware.grouper.ui.hooks.MembershipHooksImplExample

 No screen shot yet

Issues

 Everything went pretty smoothly, but...

  1. To implement a hook, it will require some understanding about Grouper.  We should post a bunch of examples.  The problem is we have business objects, data transfer objects, and data access objects.  Sometimes logic goes through any of these paths.  So I added hooks to the data access layer (layer on top of hibernate) so that all operations can be hooked.  Sometimes there will be a reference to the business object (e.g. Group), but it doesnt really make sense in some cases (like on an insert, there is no group uuid yet, so the Group object is not created yet, and even if it were, most methods would be invalid).
  2. On memberships, the DAO hook will probably not be the useful one.  Information like group name or subject id is not even available, it would have to be queried in a lot of cases.  In some cases it is known, but it is weird to have it there sometimes and not others
  3. I added a high level membership hook (addMember) which will give the information about what the group name is, subjectId, etc.  This is most likely the one that can be used for veto operations.  The low level one would most likely be used for auditing.  The weird thing about the addMember hook is that some of the objects are business objects, and some are DTOs (see the BaseMemberOf object which encapsulates one member addition).  I think people will figure it out...  but for the record, I'm not completely bought in to the necessity of having so many data layers.  (smile)

Implementation of grouper code details

 This is implemented in a 1.4 hooks branch in cvs.

e.g. the delete and load methods are just wrappers around the ones in hibernate of the same name.  ByObject just separates up the namespace a bit (there are also ByHql, and ByCriteria)

HibernateSession.callbackHibernateSession(GrouperTransactionType.READ_WRITE_OR_USE_EXISTING,
        new HibernateHandler() {

          public Object callback(HibernateSession hibernateSession) {
            ByObject byObject = hibernateSession.byObject();
            byObject.delete( byObject.load( Hib3GrouperSessionDAO.class, _s.getId() ) );
            return null;
          }

    });

* Each hook will have its own bean (these arent finished by any means, but look at add member for decent example).  This is to encapsulate the params passed to the hook (and to facilitate an easy way to get data back from a hook if needbe).  This way if any params change, existing implementors will be less likely to have to change their code

try{
  group = parent.addChildGroup(extension,displayExtension );

} catch(HookVeto hookVeto) {

  //this action was vetoed, put explanation on screen, and go back
  Message.addVetoMessageToScreen(request, hookVeto);
  return mapping.findForward(FORWARD_CreateAgain);

} catch(GroupAddException e) {
  String name = parent.getName() + GrouperHelper.HIER_DELIM + extension;
  request.setAttribute("message", new Message(
    "groups.message.error.add-problem",new String[] {e.getMessage()}, true));
  return mapping.findForward(FORWARD_CreateAgain);
}

* There was an issue of setters affecting the API, and I think we can fix that.  e.g. for group I added a save() method which needs to now be called after setting the description, name, etc.  However, there are methods which arent affected (e.g. Grouper.addMember() does not require a save).   Only javabean setters.

//see if there is a hook class
MembershipHooks membershipHooks = (MembershipHooks)GrouperHookType.MEMBERSHIP.hooksInstance();

if (membershipHooks != null) {
  HooksMembershipPreAddMemberBean hooksMembershipPreUpdateHighLevelBean =
    new HooksMembershipPreAddMemberBean(new HooksContext(), mof);

  membershipHooks.membershipPreAddMember(hooksMembershipPreUpdateHighLevelBean);
}

* For the low level hooks, the implementation is similar but even easier (since it is central)...  each DAO implements this interface, so those methods just need to be implemented.  e.g. in Hib3GroupDAO

/**
   * @see edu.internet2.middleware.grouper.internal.dao.hib3.Hib3DAO#onPreSave(edu.internet2.middleware.grouper.hibernate.HibernateSession)
   */
  @Override
  public void onPreSave(HibernateSession hibernateSession) {
    super.onPreSave(hibernateSession);

    //see if there is a hook class
    GroupHooks groupHooks = (GroupHooks)GrouperHookType.GROUP.hooksInstance();

    if (groupHooks != null) {
      HooksGroupPreInsertBean hooksGroupPreInsertBean = new HooksGroupPreInsertBean(new HooksContext(), this);
      groupHooks.groupPreInsert(hooksGroupPreInsertBean);
    }

  }

This will kick in wherever a group is saved