Grouper rules are configurable declarative scripts which run at certain times and perform actions on the registry.  They are similar to hooks though you dont have to write Java, and it does not require a change to a config file to enable a rule (i.e. anyone with authority in the folder hierarchy could enable a rule).

Use cases

Composite-ng: If an entity is no longer a member of the employee group, remove them from the group for application X

Disabled-date activation: If a student is no longer a member of the course X group, then add a membership to the course wiki group with end date in one week (note, this assumes that if the student is out of the course group, they fall out of the wiki group, another variation is to set an end date on an existing membership)

Composite-org: If an entity falls out of any group in the IT organization groups (meaning not a central IT employee anymore), then remove them from group X

Inherited permissions: If a group is created under folder a:b, then apply privileges to the group of READ,UPDATE to group a:security:admins

Rule structure

The rule structure is custom for Grouper since we want it to be performant and secure, however it is inspired from drools.  There are several parts to a rule:

Rule check

The check component will see if the rule should continue to the "if condition".  The check part is an enum class: edu.internet2.middleware.grouper.rules.RuleCheckType

Look at the javadoc or source for the most recent check types, currently they are:

Here is an example of setting a rule check:

AttributeAssign attributeAssign = groupA.getAttributeDelegate().assignAttribute(
    RuleUtils.ruleAttributeDefName()).getAttributeAssign();

attributeAssign.getAttributeValueDelegate().assignValue(RuleUtils.ruleCheckTypeName(), RuleCheckType.membershipRemove.name());

attributeAssign.getAttributeValueDelegate().assignValue(RuleUtils.ruleCheckOwnerNameName(), "stem:b");

The second part of the check is the owner.  This can either be set by name or id. 

Rule data

The rule will be an attribute of a grouper object (group, stem, etc).  There are attributes on the assignment which configure the params

    //add a rule on stem:a saying if you are out of stem:b, then remove from stem:a
    AttributeAssign attributeAssign = groupA
      .getAttributeDelegate().assignAttribute(RuleUtils.ruleAttributeDefName()).getAttributeAssign();

    attributeAssign.getAttributeValueDelegate().assignValue(
        RuleUtils.ruleActAsSubjectSourceIdName(), "g:isa");
    attributeAssign.getAttributeValueDelegate().assignValue(
        RuleUtils.ruleActAsSubjectIdName(), "GrouperSystem");
    attributeAssign.getAttributeValueDelegate().assignValue(
        RuleUtils.ruleCheckOwnerNameName(), "stem:b");
    attributeAssign.getAttributeValueDelegate().assignValue(
        RuleUtils.ruleCheckTypeName(),
        RuleCheckType.membershipRemove.name());
    attributeAssign.getAttributeValueDelegate().assignValue(
        RuleUtils.ruleIfConditionEnumName(),
        RuleConditionEnum.thisGroupHasImmediateMember.name());
    attributeAssign.getAttributeValueDelegate().assignValue(
        RuleUtils.ruleThenElName(),
        "${ruleUtils.removeMember(thisGroupId, memberId}");

sadf

Daemon component

If the rule is not scripted, then we have the opportunity to run it in daemon mode at the time the rule was added or changed, or periodically (nightly/weekly) to reduce data corruptions.  Some rules might not want this to happen (e.g. on group create set permissions, if you do this nightly then you cant remove permissions)

Error handling

If the rule execution fails for some reason, it should be logged (which could include emailing administrators), but it probably should not affect the transaction of the operation that triggered the rule.  Maybe this can be a setting on a per rule basis and where applicable (e.g. if it is a flattened membership rule trigger, then there is no transaction since the rule fires post commit anyways.

Act as

Note that the subject source should be set before the subject id or identifier (if the id or identifier arent unique).  Anyways, you can act as yourself, though I dont know why you would want to do that since if you leave the institution the rule might break.  You can configure in the grouper.properties what the act as rules are, similar to the grouper WS act as.

# Rules users who are in the following group can use the actAs field to act as someone else
# You can put multiple groups separated by commas.  e.g. a:b:c, e:f:g
# You can put a single entry as the group the calling user has to be in, and the grouper the actAs has to be in
# separated by 4 colons
# e.g. if the configured values is:       a:b:c, e:f:d :::: r:e:w, x:e:w
# then if the calling user is in a:b:c or x:e:w, then the actAs can be anyone
# if not, then if the calling user is in e:f:d, then the actAs must be in r:e:w.  If multiple rules, then
# if one passes, then it is a success, if they all fail, then fail.
rules.act.as.group = etc:rulesActAsGroup

Validation

There are certain validation constraints to make a rule valid.  i.e. you need some check, you need some then, you need an act as subject, etc.  So each time you change a rule attribute value, all the attributes are validated, and the attribute "ruleValid" is managed by that hook.  If the rule attributes are not valid, you will get a ruleValid value of something like: "INVALID: Rule check type required", if they are valid, then the value will be "T".  Only rules with a value of T will be processed.  The attribute stores this state so the rules dont have to be validated each time they are read from the DB, and so the user can get some feedback.

TODO: a daemon should validate rules daily, and ones which arent valid should be logged (notified)

TODO: when processing all rules, filter out ones which arent valid

Logging

You can turn debug logging on to see information about rules which fire.  log4j.properties

 log4j.logger.edu.internet2.middleware.grouper.rules = DEBUG

If you want to only log certain rules, you can specify them in the grouper.properties.  (and you need to set the RulesEngine to INFO level at least)

# uuids (comma separated) of the attribute assign record which is the rule type to the owner object
# e.g. SELECT gaagv.attribute_assign_id FROM grouper_attr_asn_group_v gaagv WHERE gaagv.attribute_def_name_name LIKE '%:rule' AND gaagv.group_name = 'stem:a'
# make sure log info level is set for RuleEngine
# log4j.logger.edu.internet2.middleware.grouper.rules.RuleEngine = INFO
rules.attributeAssignTypeIdsToLog = 446bb6b3bbd8417b9a3e386b3bc894c1

You will see log messages like this:

2010-08-21 15:24:13,032: [main] INFO  RuleEngine.fireRule(248) - Rules engine processing rulesBean: group: stem:b, membership:
Membership[createTime=1282418648019,creatorUuid=8b10ad84a2ab4e4d912aeca154866bbc,depth=0,listName=members,listType=list,
memberUuid=ddbbbb1615964f109e4b5f85c05098f7,groupId=291dbf3b736e42de9985a70e2ac11177,type=immediate,
uuid=4f249fd2636247a78158fc358aa58a32:bb46e541e12049618c199e162056e715], subject: Subject id: test.subject.0, sourceId: jdbc, ,
found 1 matching rule definitions, ruleDefinition should fire: attributeAssignTypeId: 446bb6b3bbd8417b9a3e386b3bc894c1,
sourceId: g:isa, subjectId: GrouperSystem, checkOwnerName: stem:b, checkType: membershipRemove,
ifConditionEnum: thisGroupHasImmediateEnabledMembership, thenEl: ${ruleUtils.removeMemberFromGroupId(ownerGroupId, memberId)}, ,
EL variables: membershipId(4f249fd2636247a78158fc358aa58a32:bb46e541e12049618c199e162056e715),groupId(291dbf3b736e42de9985a70e2ac11177),
groupName(stem:b),ruleUtils,ownerGroupId(b38004ccf99d44f08f5a0971153ad6a9),subjectId(test.subject.0),memberId(ddbbbb1615964f109e4b5f85c05098f7),
checkOwnerName(stem:b),sourceId(jdbc),, elResult: true, shouldFire count: 1

sdf