From e355a460ffed85d4b3424c71d9351238a4e0e600 Mon Sep 17 00:00:00 2001
From: "William G. Thompson, Jr" <wgthom@gmail.com>
Date: Tue, 28 May 2019 21:00:43 -0400
Subject: [PATCH] initial docs import

---
 docs/201/201.1.rst                            | 174 ++++++++++++
 docs/201/201.2.rst                            | 114 ++++++++
 docs/201/201.3.rst                            | 148 +++++++++++
 docs/201/201.4.rst                            | 127 +++++++++
 docs/201/201.5.rst                            |  85 ++++++
 ...01-3-4.pspng-epa.grouper-loader.properties |  10 +
 .../201/examples/201-3-5.attribute-filter.xml |  66 +++++
 ...01-4-4.pspng-epe.grouper-loader.properties |   9 +
 .../201/examples/201-4-5.attribute-filter.xml |  66 +++++
 docs/201/index.rst                            |  18 ++
 docs/401/401.1.rst                            | 249 ++++++++++++++++++
 docs/401/401.2.rst                            | 236 +++++++++++++++++
 docs/401/401.3.rst                            | 224 ++++++++++++++++
 docs/401/401.4-example-solution.rst           |  26 ++
 docs/401/401.4.rst                            |  67 +++++
 docs/401/appendix.rst                         | 217 +++++++++++++++
 .../examples/401.1.3-pspng-config.properties  |  90 +++++++
 .../examples/401.2.2-pspng-config.properties  | 100 +++++++
 docs/401/examples/401.2.3-general-authn.xml   | 181 +++++++++++++
 .../401/examples/401.2.3-mfa-authn-config.xml |  88 +++++++
 docs/401/examples/401.2.4-athletics-dept.txt  |  15 ++
 docs/401/examples/401.2.5-banner-netids.txt   |   5 +
 .../401.3.2-grouper-loader.properties         | 118 +++++++++
 .../401.3.2-grouper.client.properties         | 112 ++++++++
 docs/401/index.rst                            |  22 ++
 docs/401/intro.rst                            |  18 ++
 docs/Makefile                                 |  19 ++
 docs/conf.py                                  | 182 +++++++++++++
 docs/index.rst                                |  25 ++
 docs/make.bat                                 |  35 +++
 30 files changed, 2846 insertions(+)
 create mode 100644 docs/201/201.1.rst
 create mode 100644 docs/201/201.2.rst
 create mode 100644 docs/201/201.3.rst
 create mode 100644 docs/201/201.4.rst
 create mode 100644 docs/201/201.5.rst
 create mode 100644 docs/201/examples/201-3-4.pspng-epa.grouper-loader.properties
 create mode 100644 docs/201/examples/201-3-5.attribute-filter.xml
 create mode 100644 docs/201/examples/201-4-4.pspng-epe.grouper-loader.properties
 create mode 100644 docs/201/examples/201-4-5.attribute-filter.xml
 create mode 100644 docs/201/index.rst
 create mode 100644 docs/401/401.1.rst
 create mode 100644 docs/401/401.2.rst
 create mode 100644 docs/401/401.3.rst
 create mode 100644 docs/401/401.4-example-solution.rst
 create mode 100644 docs/401/401.4.rst
 create mode 100644 docs/401/appendix.rst
 create mode 100644 docs/401/examples/401.1.3-pspng-config.properties
 create mode 100644 docs/401/examples/401.2.2-pspng-config.properties
 create mode 100644 docs/401/examples/401.2.3-general-authn.xml
 create mode 100644 docs/401/examples/401.2.3-mfa-authn-config.xml
 create mode 100644 docs/401/examples/401.2.4-athletics-dept.txt
 create mode 100644 docs/401/examples/401.2.5-banner-netids.txt
 create mode 100644 docs/401/examples/401.3.2-grouper-loader.properties
 create mode 100644 docs/401/examples/401.3.2-grouper.client.properties
 create mode 100644 docs/401/index.rst
 create mode 100644 docs/401/intro.rst
 create mode 100644 docs/Makefile
 create mode 100644 docs/conf.py
 create mode 100644 docs/index.rst
 create mode 100644 docs/make.bat

diff --git a/docs/201/201.1.rst b/docs/201/201.1.rst
new file mode 100644
index 0000000..c51bb59
--- /dev/null
+++ b/docs/201/201.1.rst
@@ -0,0 +1,174 @@
+====================================
+GTE 201.1 Basis and Reference Groups
+====================================
+
+-------------------
+Learning Objectives
+-------------------
+
+* Create and manage reference and basis groups
+* Understand the difference between reference groups and basis groups
+* Implement lifecycle requirements for subject attributes
+
+--------------
+Lab Components
+--------------
+
+* Grouper
+* `Grouper Deployment Guide`_
+
+--------
+Overview
+--------
+
+Often the best source of data for building institutional meaningful cohorts is a
+combination of arcane employee/payroll/student codes from multiple source systems.
+To leverage the power of Grouper these groups should be brought in as raw **basis groups**. 
+
+Basis groups are used by the IAM analyst to construct institutional meaningful
+cohorts that are required for access policy.  Access policy does not reference
+basis groups directly, rather the basis groups are used to build up reference
+groups.  This indirection provides the IAM analyst the ability to adjust to
+changing source systems and business practices while keeping reference groups
+and access policy relatively stable. Basis groups are typically only visible to
+the IAM analyst, and would not normally be reflected out to applications and
+directories.
+
+Reference groups tend to be organized in particular folder locations for convenience
+and ease of use, but what makes a group a reference group is not its name or folder
+location, but rather its intended use, definition and scope, and data management
+expectations.
+
+A **reference group** is a set of subjects that is largely intended to be used by
+reference within access policy.  Reference groups can be thought of as labels or
+tags that identify institutional meaningful cohorts. In this way, they can also
+be viewed as subject attributes from an ABAC perspective. Access policies often
+require cohorts organized via institutional affiliation (faculty, staff, student),
+a particular office or department (president's office, finance division, chaplain),
+program (chemistry students), and even residence or class year. All of these are 
+good examples of reference groups.
+
+This module will focus on creating and using basis and reference groups related to
+students.
+
+----------------
+Exercise 201.1.1
+----------------
+
+*Create an all student reference group to be used in access policy and the all
+students mailing list*
+
+Reference groups for student by class year already exist. These are being
+used for class year mailing lists.  Membership in these are updated
+automatically by loader jobs:
+
+* `ref:student:class2019`
+* `ref:student:class2020`
+* `ref:student:class2021`
+* `ref:student:class2022`
+
+#. Create a new reference group representing all students, `ref:student:students`.
+#. Add the class year reference groups as direct members to `students`.  How
+   many students are there?  Filter by *indirect membership*.
+#. You remember that recently graduated students have a grace period of 6 months
+   during which they retain full student access.  Add `ref:student:class2018` to
+   `ref:student:students`, and set the membership end date to Dec. 31, 2018.  How
+   many students are there now?
+
+   .. note::
+
+        In this case, recently graduated students are still considered to be students
+        for the purpose of access control.  If recent graduates only retained a few
+        services, it might make more sense to add these former students to individual
+        allow policies for the services in question.
+
+----------------
+Exercise 201.1.2
+----------------
+
+*Other Students*
+
+You remember that not all students have class years assigned.  This includes part-time
+students, employees taking courses, and non-matriculated students.  Fortunately data 
+about these students is available in the SIS and a basis group has already been created
+for us.
+
+#. Add `basis:student:student_no_class_year` to `ref:student:students`.  How many
+   students are there, now?
+
+----------------
+Exercise 201.1.3
+----------------
+
+*Exchange Students*
+
+You campus participates in an exchange program with a sister school.  Students
+from the sister school can take classes at your institution, but never have
+official records in your SIS.  They do however, have a local NetID.  Registration
+is done directly with the registrar and the student's home institution maintains
+the student records.
+
+#. Add `basis:student:exchange_students` to `ref:student:students`.  How many
+   students are there now?
+
+----------------
+Exercise 201.1.4
+----------------
+
+*Transfer Students*
+
+Students who transfer into your campus often need access to systems well ahead
+of SIS data being fully updated.
+
+#. Create a new basis group, `basis:student:transfer_student`.
+#. Add `transfer_student` to `students` with an expiration 60 days out.
+#. Add the following accounts to `transfer_student`:
+
+    * agrady901
+    * alee467
+    * ascott776
+
+#. Check how many students there are, now.  The number of students did not go up
+   by 3 as you might have expected.  Why?  One of the transfer students was
+   already a member of `students`.  Trace the membership on each of the transfer
+   students to determine which accounts already had the `students` subject
+   attribute, and why.
+
+----------------
+Exercise 201.1.5
+----------------
+
+*Change of Status*
+
+Students who leave for a variety of reasons are given a 32 day grace period
+during which they retain student access.  Basis groups for these already exist.
+They include:
+
+* `basis:student:expelled_32_days`
+* `basis:student:resigned_32_days`
+* `basis_student_transferred_32_days`
+
+#. Add these basis groups to `students`.  How many students are there, now?
+
+----------------
+Exercise 201.1.6
+----------------
+
+*Leave of Absence Students*
+
+Student may also obtain a leave of absence for a variety of reasons.  These
+students may or may not return, but retain student access for an extend period
+of time.  Basis groups for leave of absence students already exists:
+
+* `basis:student:loa_4_years` (leave of absence within the last 4 years)
+
+#. Add `loa_4_years` to `students`.  How many students are there, now?
+
+
+
+
+
+
+
+
+.. _Grouper Deployment Guide: https://spaces.at.internet2.edu/display/Grouper/Grouper+Deployment+Guide+Work+-TIER+Program
diff --git a/docs/201/201.2.rst b/docs/201/201.2.rst
new file mode 100644
index 0000000..8e9e125
--- /dev/null
+++ b/docs/201/201.2.rst
@@ -0,0 +1,114 @@
+
+==============================
+GTE 201.2 Access Policy Groups
+==============================
+
+-------------------
+Learning Objectives
+-------------------
+
+* Translate a natural language policy group into digital policy using access policy groups.
+* Understand the difference between policy groups and reference groups.
+
+--------------
+Lab Components
+--------------
+
+* Grouper
+* `Grouper Deployment Guide`_
+
+--------
+Overview
+--------
+
+`NIST SP 800-162`_ describes how natural language policy, that is access policy
+stated in common language, must be converted to digital policy for any access
+control mechanism to effectively operate.  Digital policy is manifest in
+Grouper via access policy groups. Subject membership in an access policy group
+be indirect and represents a precomputed access policy decision based on subject
+attributes (i.e. the subject’s membership in various reference groups).
+
+An **access policy** group is a composite group whose membership is composed of
+an include group (i.e. the allow group) minus an exclude group (i.e. the deny
+group).  Subject membership in both the allow group and the deny group should be
+indirect (i.e. through reference groups) and have a clear mapping to the natural
+language policy.  When exceptions to policy are necessary, locally scoped
+reference groups should be added.
+
+Limiting policy groups to indirect membership assignments via reference groups
+ensures that as subject attributes change, effective membership is up to date and
+access control decisions are correct.  It also enables the direct mapping from
+natural language policy to digital policy and vice versa.  Individual exceptions to
+policy, while not expressly recommended, can be accommodated by adding subjects
+directly to the allow/deny groups.
+
+Membership within an access policy group is often kept in sync directly with a target
+service or an intermediary like an LDAP based enterprise directory service.
+Services can also query Grouper directly for membership assignment.
+
+----------------
+Exercise 201.2.1
+----------------
+
+*Application folder structure*
+
+#. Create `app:vpn:vpn_authorized`.
+#. Create `app:vpn:vpn_allow`.
+#. Create `app:vpn:vpn_deny`.
+#. Make `vpn_authorized` a composite of `vpn_allow` minus `vpn_deny`.
+
+----------------
+Exercise 201.2.2
+----------------
+
+*Create digital policy from natural language policy*
+
+Natural language policy is "all faculty, staff have access to vpn, unless denied
+by CISO or the account is in a closure state".  Reference groups are already
+available.
+
+#. Add `ref:employee:fac_staff` to `vpn_allow`.
+#. Add `ref:security:locked_by_ciso` to `vpn_deny`.
+#. Add `ref:iam:closure` to `vpn_deny`.
+
+----------------
+Exercise 201.2.3
+----------------
+
+*Update policy to also allow institutional review board members access to VPN*
+
+New natural language policy is "all faculty, staff and members of the institutional
+review board have access to vpn, unless denied by CISO or the account is in a closure
+state".
+
+#. Add `org:irb:ref:irb_members` to `vpn_allow`.
+#. Add *jsmith* to `org:irb:ref:irb_members`.
+#. Trace membership for *jsmith* from `vpn_authorized`.
+#. View the audit log on `vpn_allow`.
+
+----------------
+Exercise 201.2.4
+----------------
+
+*Create security groups for policy*
+
+#. Create `ref:app:vpn:etc` folder.
+#. Create `ref:app:vpn:etc:vpn_admins` group.
+#. Assign **ADMIN** privilege to `vpn_admins` for `ref:app:vpn`.
+#. Inherit privileges to all sub folders (and objects).
+
+    #. Navigate to `app:vpn`.
+    #. :guilabel:`More` |rightarrow| :guilabel:`Privileges inherited to objects in folder`
+    #. Click :guilabel:`Add Members`, and add `vpn_admins`.
+    #. Add admin privileges for folder, group, and attributes.
+
+#. Navigate to `ref:app:vpn:ref:vpn_allow`.
+#. Click :guilabel:`Privileges` |rightarrow| :guilabel:`Actions` |rightarrow| :guilabel:`Trace Priviliges`.
+
+
+
+.. |rightarrow| unicode:: U+2192
+
+.. _NIST SP 800-162: https://csrc.nist.gov/publications/detail/sp/800-162/final
+.. _Grouper Deployment Guide: https://spaces.at.internet2.edu/display/Grouper/Grouper+Deployment+Guide+Work+-TIER+Program
+
diff --git a/docs/201/201.3.rst b/docs/201/201.3.rst
new file mode 100644
index 0000000..9d861aa
--- /dev/null
+++ b/docs/201/201.3.rst
@@ -0,0 +1,148 @@
+
+===================================
+GTE 201.3 ACM1 eduPersonAffiliation
+===================================
+
+-------------------
+Learning Objectives
+-------------------
+
+* Understand ACM1 and how to implement subject attribute management with policy
+  groups
+* Configure PSPNG to reflect group membership (aka subject attributes) into
+  OpenLDAP
+* Configure Shibboleth to release **eduPersonAffiliation**
+
+
+--------------
+Lab Components
+--------------
+
+* Grouper
+* PSPNG
+* OpenLDAP
+* Shibboleth
+
+--------
+Overview
+--------
+
+`Grouper Deployment Guide`_ access control model 1 is all about subject attribute
+management.  This model is useful for cases where there exists a loose relationship
+between the institution and the service provider.  Assuming both are in a
+federation like InCommon, and a locally defined notion of eduPersonAffiliation_ is
+sufficient for access control, a broad set of services can be enabled fairly easily.
+
+.. warning::
+
+    This access control model is based on making subject attributes directly
+    available to services and allowing the service to make access control decisions
+    based on those attributes.  This approach has several shortcomings:
+
+    * The subject attributes provided often lack sufficient **context** to make
+      informed access control decisions.
+    * Managing changes to policy is difficult.
+    * Policy decisions become opaque.
+
+    Consider a hypothetical Learning Management System (LMS) that consumes
+    **eduPersonAffiliation** attributes from subjects and grants access to a course
+    management module based on whether an account has the *faculty* affiliation
+    present.  At first glance, this seems like a reasonable decision.  However:
+
+    * There are faculty who do not teach courses, and should probably not have
+      access to this module (*lack of context*).
+    * There are non-faculty instructors who teach courses who do need access to
+      this module (*lack of context*).
+    * Correcting either of the above issues is non-trivial.  Updating instructor
+      accounts to assert the *faculty* affiliation may be permissible for the LMS,
+      but what impact will it have on other services that employ ACM1?  Removing
+      the *faculty* affiliation from faculty who don't teach courses is even more
+      likely to cause issues (*managing changes to policy is difficult*).
+    * Exceptions may be negotiated by configuring the IdP to release different
+      affiliations based on the service provider requesting authentication (*policy
+      decisions become opaque*).
+    * Alternatively, exceptions may be handled by configuring them directly at
+      the service provider (*policy decisions become opaque*). 
+
+----------------
+Exercise 201.3.1
+----------------
+
+*Create app folder to master eduPersonAffiliation*
+
+#. Create folder `app:eduPersonAffiliation`.
+#. Create groups `...:eduPersonAffiliation:ePA_student|staff|...` to represent
+   eduPersonAffiliation values. 
+
+----------------
+Exercise 201.3.2
+----------------
+
+*Add reference groups that constitute local policy for eduPersonAffiliation values*
+
+    Therefore each institution will decide the criteria for membership in each
+    affiliation classification.  What is desirable is that a reasonable person
+    should find an institution's definition of the affiliation plausible.
+
+#. Add `ref:student:students` to `...:eduPersonAffiliation:ePA_student`.
+
+----------------
+Exercise 201.3.3
+----------------
+
+*Create "member"*
+
+The "member" affiliation MUST be asserted for people carrying one or more of
+the following affiliations: *faculty* or *staff* or *student* or *employee*.
+
+.. note:
+
+    Holders of the affiliation *alum* are not typically "members" since they
+    are not eligible for the full set of institutional privileges enjoyed by
+    faculty, staff and students.
+
+#. Create `app:eduPersonAffiliation:ePA_member`.
+#. Add `...:ePA_faculty|staff|student|employee` to `...:ePA_member`.
+
+----------------
+Exercise 201.3.4
+----------------
+
+*Configure PSPNG to reflect ePA values to LDAP*
+
+#. Assign PSPNG *provision_to* attribute to `ePA_student` with a value of
+   **pspng_affiliations**.
+#. Configure PSPNG to sync group membership to LDAP values for 
+   **eduPersonAffiliation**.
+
+   .. literalinclude:: examples/201-3-4.pspng-epa.grouper-loader.properties
+        :language: properties
+        :caption: grouper-loader.properties
+        :linenos:
+
+----------------
+Exercise 201.3.5
+----------------
+
+*Releasing ePA in SAML*
+
+The demo shibboleth IdP has been configured to release the ePA attribute to
+the demo SP.  The relevant configuration is below:
+
+.. literalinclude:: examples/201-3-5.attribute-filter.xml
+    :language: xml
+    :caption: attribute-filter.xml
+    :lines: 16-42
+    :emphasize-lines: 9
+    :linenos:
+
+    
+
+
+
+
+
+
+
+.. _eduPersonAffiliation: https://www.internet2.edu/media/medialibrary/2013/09/04/internet2-mace-dir-eduperson-201203.html#eduPersonAffiliation
+.. _Grouper Deployment Guide: https://spaces.at.internet2.edu/display/Grouper/Grouper+Deployment+Guide+Work+-TIER+Program
diff --git a/docs/201/201.4.rst b/docs/201/201.4.rst
new file mode 100644
index 0000000..2b96bac
--- /dev/null
+++ b/docs/201/201.4.rst
@@ -0,0 +1,127 @@
+
+===================================
+GTE 201.4 ACM2 eduPersonEntitlement
+===================================
+
+-------------------
+Learning Objectives
+-------------------
+
+* Understand ACM2 model and how to implement attribute based access control
+* Implement grouper security model
+* Configure PSPNG to convert membership to **eduPersonEntitlement** values in LDAP
+* Configure Shibboleth to release specific **eduPersonEntitlement** values to SP
+
+
+--------------
+Lab Components
+--------------
+
+* Grouper
+* OpenLDAP
+* Shibboleth
+* `Grouper Deployment Guide`_
+* `eduPerson Object Class Specification`_ 
+
+
+--------
+Overview
+--------
+
+`Grouper Deployment Guide`_ access control model 2 is all about subject attribute
+based access control.  ACM2 is applicable across a broad range of services where
+access control policy can be based on subject attributes, the policy decision can
+be precomputed, and simple subject attributes are sufficient to drive the enforcement
+point.
+
+""""
+ACM2
+""""
+
+Implementing ACM2 can be distilled to these basic steps:
+
+#. Convert natural language policy to reference groups and policy groups
+#. Provisioning to LDAP-- Grouper group |rightarrow| LDAP attribute via PSPNG
+#. Release **eduPersonEntitlement** value in SAML authentication response
+
+
+----------------
+Exercise 201.4.1
+----------------
+
+*Create policy for wiki application*
+
+#. Create folder `app:wiki`.
+#. Create policy groups `app:wiki:service:policy:wiki_authorized|allow|deny`.
+#. Edit composite `wiki_authorized` to make it `wiki_allow` minus `wiki_deny`.
+
+----------------
+Exercise 201.4.2
+----------------
+
+*Create security group*
+
+#. Create folder `app:wiki:security`.
+#. Create security group `app:wiki:security:wiki_admin`.
+#. Add **ADMIN** privileges to `wiki_admin` for `app:wiki:service`, and inherit
+   to all child objects (folders, groups, and attributes).
+
+----------------
+Exercise 201.4.3
+----------------
+
+*Add reference groups to policy*
+
+#. Add `ref:student:students` to `app:wiki:service:policy:wiki_allow`.
+#. Add `ref:iam:global_deny` to `app:wiki:service:policy:wiki_deny`.
+
+----------------
+Exercise 201.4.4
+----------------
+
+*Configure PSPNG to reflect policy to eduPersonEntitlement in LDAP*
+
+#. Assign PSPNG attribute, **provision_to**  to `wiki_authorized` with a value
+   of *pspng_entitlements*.
+#. Configure PSPNG to convert membership to ePE value of http://sp.example.org/wiki
+   and review in LDAP.  The relevent configuration is below:
+
+   .. literalinclude:: examples/201-4-4.pspng-epe.grouper-loader.properties
+        :language: properties
+        :caption: grouper-loader.properties
+        :linenos:
+
+----------------
+Exercise 201.4.5
+----------------
+
+*Configure Shib to release ePE value for our SP*
+
+The demo shibboleth IdP has been configured to release the ePE attribute to
+the demo SP.  The relevant configuration is below:
+
+.. literalinclude:: examples/201-4-5.attribute-filter.xml
+    :language: xml
+    :caption: attribute-filter.xml
+    :lines: 16-42
+    :emphasize-lines: 17
+    :linenos:
+
+----------------
+Exercise 201.4.6
+----------------
+
+*(Thought exercise!) Create accounts at target SP*
+
+Use policy groups to create/manage accounts at target SP.
+
+* Native grouper SP specific provisioning components
+* RabbitMQ based provisioning / deprovisioning
+* midPoint
+
+
+
+.. |rightarrow| unicode:: U+2192
+
+.. _eduPerson Object Class Specification: http://software.internet2.edu/eduperson/internet2-mace-dir-eduperson-201602.html
+.. _Grouper Deployment Guide: https://spaces.at.internet2.edu/display/Grouper/Grouper+Deployment+Guide+Work+-TIER+Program
diff --git a/docs/201/201.5.rst b/docs/201/201.5.rst
new file mode 100644
index 0000000..d228427
--- /dev/null
+++ b/docs/201/201.5.rst
@@ -0,0 +1,85 @@
+
+======================================
+GTE 201.5 ACM3 Subject to Role Mapping
+======================================
+
+-------------------
+Learning Objectives
+-------------------
+
+* Understand ACM3 and how to use grouper policy groups  with application specific roles
+* Implement delegated access control
+* Configure attestation
+
+--------------
+Lab Components
+--------------
+
+* Grouper
+* `Grouper Deployment Guide`_
+
+--------
+Overview
+--------
+
+In applications with sophisticated RBAC capabilities, fine-grained permission
+sets are typically configured via an administrative interface within the
+application itself.  These permission sets are then associated with a role name
+that can be mapped to a set of users.  In this model, the user to role mapping
+is done in Grouper by pairing a normal access control group with the role name
+defined at the target service.  The policy indicating which subjects are mapped
+to application roles (permissions sets) can be attribute based or a simple
+access control list, or some combination of both.
+
+ACM3 is implemented using Grouper as follows:
+
+* Subject |rightarrow| Role assignment is made in Grouper.  Access control policies are used to represent Roles.
+* Fine-grained permission sets are managed at the target service and assigned a Role Name
+* Grouper access control groups are mapped to target service Role Name, completing the User |rightarrow| Role mapping
+* PAP split between Grouper and target service, PDP and PEP at service
+
+----------------
+Exercise 201.5.1
+----------------
+
+*Create application folder and group set*
+
+Use wizard template (or gsh script) to create new application folder/group set.
+
+#. Create `app:cognos:service:security:cg_adv_manager`.
+#. Create `app:cognos:service:ref` folder.
+#. Create `app:cognos:service:policy` folder.
+#. Create `app:congos:service:policy:cg_adv_report_reader|allow|deny`.
+#. Create `app:congos:service:policy:cg_adv_report_writer|allow|deny`.
+
+----------------
+Exercise 201.5.2
+----------------
+
+*Add reference groups to policy*
+
+#. Add `ref:dept:advancement` to `cg_adv_report_reader_allow`.
+
+----------------
+Exercise 201.5.3
+----------------
+
+*Create app specific reference group for advancement report writers*
+
+#. Create `app:congos:service:ref:advancement_report_writer`.
+#. Add `...:ref:advancement_report_writers` to `...:cg_adv_report_writer_allow`.
+#. Add read/update privileges to `cg_adv_manager` to `cg_adv_report_writer_allow`.
+
+----------------
+Exercise 201.5.4
+----------------
+
+*Add attestation*
+
+#. Add attestation requirement for `advancement_report_writer`.
+
+
+
+.. |rightarrow| unicode:: U+2192
+
+.. _Grouper Deployment Guide: https://spaces.at.internet2.edu/display/Grouper/Grouper+Deployment+Guide+Work+-TIER+Program
diff --git a/docs/201/examples/201-3-4.pspng-epa.grouper-loader.properties b/docs/201/examples/201-3-4.pspng-epa.grouper-loader.properties
new file mode 100644
index 0000000..94f4438
--- /dev/null
+++ b/docs/201/examples/201-3-4.pspng-epa.grouper-loader.properties
@@ -0,0 +1,10 @@
+changeLog.consumer.pspng_affiliations.class = edu.internet2.middleware.grouper.pspng.PspChangelogConsumerShim
+changeLog.consumer.pspng_affiliations.type = edu.internet2.middleware.grouper.pspng.LdapAttributeProvisioner
+changeLog.consumer.pspng_affiliations.quartzCron = 0 * * * * ?
+changeLog.consumer.pspng_affiliations.ldapPoolName = demo
+changeLog.consumer.pspng_affiliations.provisionedAttributeName = eduPersonAffiliation
+changeLog.consumer.pspng_affiliations.provisionedAttributeValueFormat = ${group.extension.replace('ePA_', '')}
+changeLog.consumer.pspng_affiliations.userSearchBaseDn = ou=people,dc=internet2,dc=edu
+changeLog.consumer.pspng_affiliations.userSearchFilter = uid=${subject.id}
+changeLog.consumer.pspng_affiliations.allProvisionedValuesPrefix=*
+
diff --git a/docs/201/examples/201-3-5.attribute-filter.xml b/docs/201/examples/201-3-5.attribute-filter.xml
new file mode 100644
index 0000000..0867564
--- /dev/null
+++ b/docs/201/examples/201-3-5.attribute-filter.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+    This file is an EXAMPLE policy file.  While the policy presented in this 
+    example file is illustrative of some simple cases, it relies on the names of
+    non-existent example services and the example attributes demonstrated in the
+    default attribute-resolver.xml file.
+    
+    Deployers should refer to the documentation for a complete list of components
+    and their options.
+-->
+<AttributeFilterPolicyGroup id="ShibbolethFilterPolicy"
+        xmlns="urn:mace:shibboleth:2.0:afp"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="urn:mace:shibboleth:2.0:afp http://shibboleth.net/schema/idp/shibboleth-afp.xsd">
+
+    <!-- Release some attributes to an SP. -->
+    <AttributeFilterPolicy id="example1">
+        <PolicyRequirementRule xsi:type="Requester" value="https://grouperdemo/shibboleth" />
+
+        <AttributeRule attributeID="cn">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="eduPersonAffiliation">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="eduPersonPrimaryAffiliation">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="eduPersonEntitlement">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="eduPersonPrincipalName">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="eduPersonScopedAffiliation">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="employeeNumber">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="givenName">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="mail">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="surname">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="uid">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+    </AttributeFilterPolicy>
+</AttributeFilterPolicyGroup>
+
diff --git a/docs/201/examples/201-4-4.pspng-epe.grouper-loader.properties b/docs/201/examples/201-4-4.pspng-epe.grouper-loader.properties
new file mode 100644
index 0000000..7c5ef95
--- /dev/null
+++ b/docs/201/examples/201-4-4.pspng-epe.grouper-loader.properties
@@ -0,0 +1,9 @@
+changeLog.consumer.pspng_entitlements.class = edu.internet2.middleware.grouper.pspng.PspChangelogConsumerShim
+changeLog.consumer.pspng_entitlements.type = edu.internet2.middleware.grouper.pspng.LdapAttributeProvisioner
+changeLog.consumer.pspng_entitlements.quartzCron = 0 * * * * ?
+changeLog.consumer.pspng_entitlements.ldapPoolName = demo
+changeLog.consumer.pspng_entitlements.provisionedAttributeName = eduPersonEntitlement
+changeLog.consumer.pspng_entitlements.provisionedAttributeValueFormat = ${group.name.equalsIgnoreCase('app:wiki:service:policy:wiki_authorized') ? 'http://sp.example.org/wiki' : 'urn:mace:example.edu:' + group.extension}
+changeLog.consumer.pspng_entitlements.userSearchBaseDn = ou=people,dc=internet2,dc=edu
+changeLog.consumer.pspng_entitlements.userSearchFilter = uid=${subject.id}
+changeLog.consumer.pspng_entitlements.allProvisionedValuesPrefix=*
diff --git a/docs/201/examples/201-4-5.attribute-filter.xml b/docs/201/examples/201-4-5.attribute-filter.xml
new file mode 100644
index 0000000..0867564
--- /dev/null
+++ b/docs/201/examples/201-4-5.attribute-filter.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 
+    This file is an EXAMPLE policy file.  While the policy presented in this 
+    example file is illustrative of some simple cases, it relies on the names of
+    non-existent example services and the example attributes demonstrated in the
+    default attribute-resolver.xml file.
+    
+    Deployers should refer to the documentation for a complete list of components
+    and their options.
+-->
+<AttributeFilterPolicyGroup id="ShibbolethFilterPolicy"
+        xmlns="urn:mace:shibboleth:2.0:afp"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="urn:mace:shibboleth:2.0:afp http://shibboleth.net/schema/idp/shibboleth-afp.xsd">
+
+    <!-- Release some attributes to an SP. -->
+    <AttributeFilterPolicy id="example1">
+        <PolicyRequirementRule xsi:type="Requester" value="https://grouperdemo/shibboleth" />
+
+        <AttributeRule attributeID="cn">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="eduPersonAffiliation">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="eduPersonPrimaryAffiliation">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="eduPersonEntitlement">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="eduPersonPrincipalName">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="eduPersonScopedAffiliation">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="employeeNumber">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="givenName">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="mail">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="surname">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+        <AttributeRule attributeID="uid">
+            <PermitValueRule xsi:type="ANY" />
+        </AttributeRule>
+
+    </AttributeFilterPolicy>
+</AttributeFilterPolicyGroup>
+
diff --git a/docs/201/index.rst b/docs/201/index.rst
new file mode 100644
index 0000000..dab143a
--- /dev/null
+++ b/docs/201/index.rst
@@ -0,0 +1,18 @@
+Grouper Access Governance (201)
+===============================
+
+This course explores access governance approach described in the `Grouper
+Deployment Guide`_. After completing this course, the student will understand
+how to use Grouper primitives to achieve access governance capabilities, and be
+able to translate natural language policy into digital policy.
+
+.. toctree::
+   :maxdepth: 2
+
+   201.1
+   201.2
+   201.3
+   201.4
+   201.5
+
+.. _Grouper Deployment Guide: https://spaces.at.internet2.edu/display/Grouper/Grouper+Deployment+Guide+Work+-TIER+Program
\ No newline at end of file
diff --git a/docs/401/401.1.rst b/docs/401/401.1.rst
new file mode 100644
index 0000000..9e78cfd
--- /dev/null
+++ b/docs/401/401.1.rst
@@ -0,0 +1,249 @@
+========================
+401.1 VPN Access Control
+========================
+
+-------------------
+Learning Objectives
+-------------------
+
+* Use group math and reference groups to analyze legacy authorization groups
+* Translate natural language policy into Grouper digital policy
+* Implement distributed access management
+* Use Grouper to answer access management questions such as "who" and "why"
+
+--------------
+Lab Components
+--------------
+
+* Grouper
+* PSPNG
+* OpenLDAP
+* `Grouper Deployment Guide <https://spaces.at.internet2.edu/display/Grouper/Grouper+Deployment+Guide+Work+-TIER+Program>`_
+
+--------
+Overview
+--------
+
+VPN access is currently controlled by an LDAP group. You are not exactly sure
+who is in the group or what the policy is, but have a general notion of a
+natural language policy as all active faculty and staff, plus exceptions.
+However, people have been added to the VPN ldap group mostly by hand over many
+years with little to no lifecycle management in place. There is no easy way to
+determine who should or should not be in the group. We just had a major breach
+which was facilitated by access to the VPN. The compromised account used in the
+breach was given to a former consultant and was never deprovisioned. CISO is
+coming down hard on us to clean up our act!
+
+----------------
+Exercise 401.1.1
+----------------
+
+*Gain insight into who exactly has access to the VPN based on the cohorts found
+in the legacy VPN authorization group.*
+
+""""""""""""""""""""""
+Import Legacy VPN Data
+""""""""""""""""""""""
+
+* Create a loader job from the existing ldap vpn authorization group.
+* Make sure grouper group counts matches ldap group counts.
+* First thing to notice is you can eyeball the types of subjects in Grouper UI.
+
+.. note::
+    For small enough groups this might be sufficient, but our VPN group has
+    hundreds of subjects.
+
+"""""""""""""""""""""""""""""""
+Use Set Operations for Analysis
+"""""""""""""""""""""""""""""""
+
+Use intersect composite groups to gain insight into types of cohorts
+
+* `test:vpn:vpn_faculty`: Intersect `ref:faculty` with `test:vpn:vpn_legacy`.
+  This yields faculty count (almost) - aha! This explains help desk calls!
+
+* `test:vpn:vpn_employees`: Intersect `ref:staff` with `test:vpn:vpn_legacy`.
+  This yields staff count (again almost!)
+
+* `test:vpn:vpn_students`: Intersect `ref:students` with `test:vpn:vpn_legacy`.
+  This yields a small count - aha!
+
+* Totals don’t add up...so we have other cohorts too. Who are they?
+
+* Set up composite group to filter out "other cohorts".
+
+* `test:vpn:other_cohorts` = `...:vpn_legacy` - (`...:vpn_faculty` + 
+  `...:vpn_employees` + `...:vpn_students`)
+
+  * create `...:vpn_facstaffstudent` (include `...:vpn_faculty`,
+       `...:vpn_employees`, `...:vpn_students`)
+  * `...:other_cohorts` = `...:vpn_legacy` - `...:vpn_facstaffstudent`
+
+* "Other cohorts" is a relatively small number ... can now eyeball those.
+
+    * fac/staff that are now longer active
+    * Contractors, sponsored accounts, etc
+    * Others
+
+----------------
+Exercise 401.1.2
+----------------
+
+*State the natural language policy and create VPN application group and digital
+policy.*
+
+#. Natural language policy: "Faculty, staff and exceptions (some students,
+   contractors, etc.)"
+#. Construct `app:vpn:vpn_authorized|allow|deny` policy groups from appropriate
+   reference groups.
+
+   * `ref:faculty`
+   * `ref:employees`
+   * `app:vpn:ref:vpn_adhoc`
+
+#. Compare counts between ldap vpn group and `app:vpn:vpn_authorized`.
+   `vpn_authorized` should be different from the legacy group in the following
+   ways:
+
+   * only active accounts
+   * only current exceptions (none!)
+
+----------------
+Exercise 401.1.3
+----------------
+
+*Export `vpn_authorized` to LDAP for use with new VPN config.*
+
+#. Mark/config `vpn_authorized` to export to LDAP.  The PSPNG needs to be
+   configured to provision group members. 
+
+   .. literalinclude:: examples/401.1.3-pspng-config.properties
+        :language: properties
+        :lines: 72-
+        :caption: grouper-loader.properties
+        :name: 401.1.3-pspng-groupofnames
+        :linenos:
+
+#. Open a ticket to switch VPN config to use vpn_authorized.
+#. Bask in the glow of TIER IAM goodness...
+
+   * Automatic provisioning/deprovisioning for faculty and staff.
+   * Natural language policy - clear and visible.
+   * Exceptions management:
+
+        * Still dealing with tickets to add and remove subjects (well at least to add!).
+        * No way to distinguish different exceptions.
+        * Who is responsible for lifecycle, attestation, etc.?
+
+----------------
+Exercise 401.1.4
+----------------
+
+*Implement distributed exception management.*
+
+We initially added exceptions to single application reference group. This a
+good step, but we still lack an easy way to know the "who and why" of
+exceptions. IAM still also getting tickets to add people. In some case, the
+expiration is known and added, but most are a one way street-- back to old
+practices. How can we do better?
+
+"""""""""""""""""""""""""""""
+Organize Exceptions to Policy
+"""""""""""""""""""""""""""""
+
+Each policy exception is represented by an application specific reference group.
+
+#. Create `app:vpn:ref:vpn_consultants`.  This ACL will be managed by the IAM
+   team.
+#. Create `app:vpn:ref:vpn_ajohnson409`.  Management of this ACL will be
+   delegated to a professor.
+ 
++++++++++++++++++++++++++++++++++++
+Professor Johnson's Special Project
++++++++++++++++++++++++++++++++++++
+
+Professor Johnson (**ajohnson409**) runs a special project that includes various online
+resources that can only be accessed from the VPN.  The professor should be able to
+control who is allowed to have VPN access for the purpose of accessing his
+project's resources.
+
+ACL `app:vpn:ref:vpn_ajohnson409` represents subjects that will access resources
+related to Professor Johnson's special project.  In order to delegate management
+of this ACL to the professor, we must create a security group and grant it
+appropriate permissions:
+
+#. Create `app:vpn:etc:vpn_ajohnson409_mgr`.
+#. Add subject `ajohnson409` to this security group. 
+#. Grant *UPDATE* and *READ* access on the `...:ajohnson409` access control
+   list to this security group.
+#. In a private browser window, log into the GTE was account `ajohnson409`, 
+   password "password".  You should be able to add and remove members from the
+   `vpn_ajohnson409` ACL.
+
+""""""""""""""""""""""
+Put Limits on Policies
+""""""""""""""""""""""
+
+It is the IAM team's responsibility to make sure that VPN access is granted to the
+correct subjects.  Putting some limits in place can help make sure improper
+access is not granted.  Attestation makes sure that access which was granted
+in the past is still appropriate.
+
+#. Create `ref:iam:global_deny`.  This reference group represents a broad cohort
+   of subjects that should not be granted access to most policies.  Subjects
+   that fall into this category may be:
+
+   * Termed "with cause"
+   * Deceased
+   * Other reasons
+
+#. Add `ref:iam:global_deny` to the `app:vpn:vpn_deny` policy.
+#. Add attestation requirements to the `app:vpn:ref:vpn_ajohnson409` ACL.
+
+    * Create attestation requirements (30 days).
+    * Review notification settings.
+    * View :guilabel:`home` -> :guilabel:`misc` -> :guilabel:`attestation settings`.
+    * Log in as `ajohnson409` and attest! 
+    * View audit log to see who attested group.
+
+#. Add automatic age-off / lifecycle - exceptions only good for 180 days.
+   There are 2 techniques:
+
+   * Add member, edit membership, add membership end date.
+   * Better approach, use grouper rule to automatically add end date to
+     members.  See :ref:`the appendix <apdx-401.1.4-auto-end-date>` for
+     details.
+
+#. Use Grouper 2.4 affiliation-based deprovisioning.
+
+All access to VPN is now traceable to natural language policy and known
+exceptions! Policy is enforced automatically and kept in sync with changing
+subject attributes. Exceptions are known and managed with a defined
+attestation lifecycle. VPN policy participates in the global deny policy.
+
+----------------
+Exercise 401.1.5
+----------------
+
+*CISO is working on a investigation and wants to know if this particular NetID
+"blee172" has access to the VPN now or in the past 90 days?*
+
+#. Navigate to `apps:vpn:vpn_authorized`.
+#. Search for so-and-so.
+#. Open up phpMyAdmin (https://localhost:8443/phpmyadmin/)
+#. Open Views, Go to SQL tab, paste in
+   :ref:`PIT query <apdx-401.1.5-pit-query>`, Go!
+
+.. _apdx-401.1.5-pit-query:
+
+----------------
+Exercise 401.1.6
+----------------
+
+*CISO wants to know if anyone on this list of NetIDs has access to the VPN? And
+why?*
+
+#. Import list to a test group.
+#. Intersect with `vpn_authorized`.
+#. Trace membership to determine what level of access and why.
+
diff --git a/docs/401/401.2.rst b/docs/401/401.2.rst
new file mode 100644
index 0000000..10db353
--- /dev/null
+++ b/docs/401/401.2.rst
@@ -0,0 +1,236 @@
+===========================
+401.2 MFA Policy Governance
+===========================
+
+-------------------
+Learning Objectives
+-------------------
+
+* Use Grouper policy to control Shibboleth MFA behavior
+* Create `eduPersonEntitlement` value to represent desired MFA behavior
+* Evolve digital policy to match changing natural language policy
+
+--------------
+Lab Components
+--------------
+
+* Shibboleth
+* Grouper
+* PSPNG
+* OpenLDAP
+* eduPerson schema - `eduPersonEntitlement`
+* REFEDS MFA profile
+* `Grouper Deployment Guide <https://spaces.at.internet2.edu/display/Grouper/Grouper+Deployment+Guide+Work+-TIER+Program>`_
+
+--------
+Overview
+--------
+
+Your institution is deploying multi-factor authentication (MFA). The first
+target application is Web SSO. Any account enabled for MFA will experience
+common MFA behaviors sufficient to assert the REFEDS MFA profile during 
+WebSSO authentication. The project plan calls for an initial pilot phase,
+followed by a number phases where different cohorts will be required or
+may opt-in. During the initial pilot phase, select cohorts will be asked to
+volunteer. Your mission, should you choose to accept, is to create and evolve
+the digital policy necessary to achieve the project goals.
+
+----------------
+Exercise 401.2.1
+----------------
+
+*Create initial MFA application folder set and policy in Grouper*
+
+#. Create `app:mfa:mfa_enabled|allow|deny`.
+#. Create `app:mfa:ref:pilot`.  This reference group is an access control
+   list (ACL) as opposed to ABAC policy.
+#. Add `app:mfa:ref:pilot` to `app:mfa:mfa_enabled_allow`.
+
+
+----------------
+Exercise 401.2.2
+----------------
+
+*Establish an eduPersonEntitlement value to signal "MFA enabled"*
+
+We will assign a unique `eduPersonEntitilement` (ePE) value to LDAP accounts
+that are MFA enabled.  We choose the value 
+**http://tier.internet2.edu/mfa/enabled**.
+
+There are 2 steps to accomplish this:
+
+#. Assign PSPNG `provision_to` attribute (attribute def `provision_to_def`) 
+   to `app:mfa:mfa_enabled` with a value of `pspng_entitlements`.
+#. Configure PSPNG to provision this attribute.
+
+   .. literalinclude:: examples/401.2.2-pspng-config.properties
+        :language: properties
+        :lines: 92-100
+        :caption: grouper-loader.properties
+        :name: 401.2.2-pspng-groupofnames
+        :linenos:
+
+----------------
+Exercise 401.2.3
+----------------
+
+*Configure Shibboleth IdP to honor MFA enabled ePE value and assert REFEDS
+MFA profile*
+
+.. literalinclude:: examples/401.2.3-general-authn.xml
+   :language: xml
+   :emphasize-lines: 14, 16
+   :lines: 112-130
+   :caption: mfa-authn-config.xml
+   :linenos:
+
+.. literalinclude:: examples/401.2.3-mfa-authn-config.xml
+   :language: xml
+   :emphasize-lines: 25
+   :lines: 53-86
+   :caption: mfa-authn-config.xml
+   :linenos:
+
+Now have a working MFA policy. Adding new volunteers to the pilot is as easy as
+adding members to the pilot group. The next rollout phase calls for onboarding
+select departments, but allow for exceptions.
+
+----------------
+Exercise 401.2.4
+----------------
+
+*Onboard select departments, but allow for exceptions*
+
+#. Add `ref:dept:its` to `app:mfa:mfa_enabled_allow`.
+#. Add `app:mfa:ref:mfa_bypass` to `app:mfa:mfa_enabled_deny`.  Add [NetID] to
+   `mfa_bypass` to exclude from `mfa_enabled`.
+#. Athletics departement
+
+   * You don't have a reference group, but you were provided a list of subject IDs:
+
+     .. literalinclude:: examples/401.2.4-athletics-dept.txt
+        :language: text 
+        :caption: Athletics Department
+        :linenos:
+
+   * Import the list as a temporary app-specific reference group.
+   * Add this reference group to `mfa_enabled_allow`.
+
+The MFA pilot is going well when the institution is hit with some direct deposit
+fraud. Mandate comes from leadership to add some required cohorts. The new
+policy is "any non-faculty who has access to sensitive data (i.e. Banner
+INB) must have mfa enabled". The new policy should be active within two days.
+
+
+----------------
+Exercise 401.2.5
+----------------
+
+*Update policy to include all non-faculty employees who have access to sensitive data in Banner*
+
+The Banner support team provides a list of NetIDs to satisfy the "non-faculty
+who have access to sensitive data in Banner" part of the policy.
+
+#. Create `app:mfa:ref:NonFacultyBannerINB` and import list of NetIDs.
+
+    .. literalinclude:: examples/401.2.5-banner-netids.txt
+
+#. Add `NonFacultyBannerINB` to `app:mfa:mfa_enabled_allow`.  Edit the start
+   date for this group to be in the future.
+#. Use :ref:`SQL <apdx-401.2.5-future-memberships-query>` to view memberships
+   enabled in the future.
+
+That’s was easy! Except-- the list is not quite right. Some faculty were
+included for some reason. Help desk calling!  Need to remove faculty members.
+
+
+----------------
+Exercise 401.2.6
+----------------
+
+*Update policy to include all Banner users except faculty*
+
+#. Create `app:mfa:ref:BannerUsersMinusFaculty`.
+#. Edit this reference group to make it composite of `NonFacultyBannerINB`
+   minus `ref:faculty`.
+
+The new policy is in place and the pilot continues to expand. The next phase
+calls for any faculty, staff, or student who are not already required to be
+able to opt-in or out of MFA at their discretion. 
+
+
+----------------
+Exercise 401.2.7
+----------------
+
+*Allow any faculty, staff, or student to opt-in/out if they are not already
+required by other policy*
+
+#. Create `app:mfa:ref:mfa_opt_in`, an opt-in group for individuals who want to
+   join or leave the service.
+#. Add `mfa_opt_in` to `mfa_enabled_allow`.
+#. Create a new grouper security group, `app:mfa:etc:mfa_opt_in_access|allow|deny`.
+#. Add faculty, staff, and student reference groups to
+   `app:mfa:etc:mfa_opt_in_access_allow`.
+#. Create `app:mfa:ref:mfa_required` and copy your required members from the
+   `mfa_enabled_allow` policy to `mfa_required`.  
+#. Add `mfa_required` to `mfa_enabled_allow` and remove the redundant members.
+#. Add `app:mfa:ref:mfa_required` to `mfa_opt_in_access_deny`.
+#. Configure `mfa_opt_in` privileges to grant `mfa_opt_in_access` *OptIn* and
+   *OptOut* rights.
+
+Now, subject **awhite318** (Amber White) can log in and see the
+`mfa_opt_in` group.  This subject is able to join or leave at will.
+
+"""""""""""""""""""""""""""""
+Improving the User Experience
+"""""""""""""""""""""""""""""
+
+The Grouper UI is sufficient for simple user interaction, but not a great user
+experience. Another approach is to build a small, web-based application to
+manage membership directly or via database and grouper loader.
+
+* Web application maintains a database of NetIDs that have opted in.
+* Grouper loader job imports opt-in members into a reerence group.
+* The web app needs to know what NetIDs are required to use MFA and are
+  therefore ineligible to use the web app.  Grouper can provision a 2nd
+  ePE, `http://tier.internet2.edu/mfa/required`.
+
+Working great!  But, accounts that were put in early bypass for some reason
+now can't opt-in.  It looks like they enabled it, but they get filtered out of
+`mfa_enabled` because of the bypass membership.  Removing those accounts from
+bypass puts them in `mfa_enabled`.
+
+----------------
+Exercise 401.2.8
+----------------
+
+*Refactor `...:ref:bypass` to accommodate users who opt-in to MFA*
+
+#. Refactor `...:ref:bypass` to  `...:basis:mfa_bypass`.
+#. Create new `...:ref:mfa_bypass_not_opt_in` composite 
+   (`...:basis:mfa_bypass` - `...:ref:mfa_opt_in`).
+#. Add `...:ref:mfa_bypass_not_opt_in` to `app:mfa:mfa_enabled_deny`.
+
+Another way might be to use Grouper rules.
+
+Pilot has been a success. Leadership wants all remaining faculty, staff,
+and students to be enabled by policy.
+
+----------------
+Exercise 401.2.9
+----------------
+
+*Add all remaining faculty, staff, and students to policy*
+
+#. Add appropriate reference groups to allow policy.
+#. Clean up intermediate policy and application reference groups.
+
+   * Add `faculty`, `staff`, and `student` reference groups to policy.
+   * Remove app specific reference and basis groups.
+
+We should now have a fairly clean app policy folder.  We were able to update
+digital policy without affecting service.
+
+Kick back and have a margarita!
+
diff --git a/docs/401/401.3.rst b/docs/401/401.3.rst
new file mode 100644
index 0000000..240a440
--- /dev/null
+++ b/docs/401/401.3.rst
@@ -0,0 +1,224 @@
+===============================
+401.3 Board Effect Provisioning
+===============================
+
+-------------------
+Learning Objectives
+-------------------
+
+
+--------------
+Lab Components
+--------------
+
+* Shibboleth IdP
+* InCommon Federation
+* Grouper
+* RabbitMQ
+* Grouper ESBChangeLog Consumer
+* `Grouper Deployment Guide`_
+
+--------
+Overview
+--------
+
+We have been asked to deploy a SaaS application called Board Effect. The
+service is already an InCommon member and honors an `eduPersonEntitlement`
+for "front door" access. Permission management within the application is
+centered around "work rooms".  Each work room provide access to specific
+documents, chat, mailing lists, etc.  The system will be used by trustees,
+executives, and various committee members.
+
+Thankfully the service is an InCommon member and using `eduPersonEntitlement`
+values.  However, it turns out users still need to have accounts provisioned
+in order to get access. We will need two different kinds of policy groups.
+The first, the account policy group, will be mapped to an `eduPersonEntitlement`
+value and also be used for provisioning accounts.  The second type,
+authorization groups, will provide subject to role mapping, and are mapped
+to work rooms created in Board Effect. This is an example of access control
+model 3 described in the `Grouper Deployment Guide`_.
+
+----------------
+Exercise 401.3.1
+----------------
+
+*Create a application policy folder and groups*
+
+Rather than create the basic structure manually, use a
+:ref:`GSH script <apdx-401.3.1-app-skeleton>`.
+
+
+----------------
+Exercise 401.3.2
+----------------
+
+*Workrooms (i.e. authorization groups) can be updated via the Board Effect
+REST API.  Create Grouper authorization groups to manage those.*
+
+A new workroom call Committee on Finance has been created in Board Effect.
+Need to create authorization group in grouper and configure provisioning.
+
+#. Create `app:boardeffect:wr_cmt_fin_authorized|allow|deny`.
+#. Configure grouperESB to send membership changes to rabbitMQ exchange.
+
+   .. literalinclude:: examples/401.3.2-grouper-loader.properties
+        :language: properties
+        :lines: 102-118
+        :caption: grouper-loader.properties
+        :linenos:
+
+   .. literalinclude:: examples/401.3.2-grouper.client.properties
+        :language: properties
+        :lines: 61-112
+        :caption: grouper.client.properties
+        :linenos:
+
+#. Write provisioner component to read rabbitMQ and update BoardEffect via REST API.
+
+   .. note::
+
+        This step is what logically should happen next to process the messages.
+        You aren't expected to actually accomplish this step during the lab.
+
+----------------
+Exercise 401.3.3
+----------------
+
+*Board Effect account provisioning*
+
+#. Create `app:boardeffect:boardeffect_authorized`.
+#. Add `...:wr_cmt_fin_authorized` to `boardeffect_authorized_allow`.
+#. Configure PSPNG to write `eduPersonEntitlement` value
+   **https://college.boardeffect.com/** to LDAP and release via Shibboleth only
+   for Boardeffect.
+
+   .. literalinclude:: examples/401.3.2-grouper-loader.properties
+        :language: properties
+        :lines: 92-100
+        :emphasize-lines: 6
+        :caption: grouper-loader.properties
+        :linenos:
+
+Subject to role mapping in place and provisioners working, but how do we get
+reference groups for committees? Ann in President’s Office knows.
+
+----------------
+Exercise 401.3.4
+----------------
+
+*Distributed Reference Group Management*
+
+Amy maintains list of committee members. Use these to build application specific
+reference groups.
+
+#. Create `app:boardeffect:ref:cmt_fin`.
+#. Add `...:ref:cmt_fin` to `...:wr_cmt_fin_allow`.
+#. Add `ref:global_deny` to `...:wr_cmt_fin_deny`.
+#. Give Ann admin access to `app:boardeffect:ref` by adding account 
+   **amartinez410** to `app:boardeffect:etc:boardeffect_admins`.
+
+Log in as Ann Martinez (**amartinez410**).  Under *My Groups* you should see
+the reference groups and policies Ann can manage.
+
+----------------
+Exercise 401.3.5
+----------------
+
+*Committee member helpers*
+
+Joe Trustee is on committee, but Joe’s assistant also needs access to
+committee work group.
+
+#. Create app specific ref group `app:boardeffect:ref:cmt_fin_helpers`.
+#. Add `...:cmt_fin_helpers. to `...:wr_cmt_fin_allow`.
+
+.. note::
+
+    By *not* adding the helper subject to `app:boardeffect:ref:cmt_fin`,
+    we preserve the truth of the subject attributes.  Members of `cmt_fin`
+    *are* members of the Finance Committee.  The helpers are *not* members
+    of the committee, but they *are* granted access to the workroom by
+    the policy.
+
+This works great for specific assistants, but there are also general helpers
+who need access to all workrooms *temporarily* during board meetings.
+
+#. Create app specific ref group `app:boardeffect:ref:workroom_helpers`.
+#. Run :ref:`GSH script <apdx-401.3.5-temp-access>` to add age off rule
+   to `workroom_helpers`.
+#. Add `workroom_helpers` to all workroom allow groups.
+
+Workrooms created in Boardeffect.  Grouper policy groups map to workroom, and
+are kept up to date via Grouper provisioners.  We could create workrooms
+automatically based on policy group creation-- exercise left to student at home.
+
+----------------
+Exercise 401.3.6
+----------------
+
+*Anna's Grouper Privileges*
+
+Anna was added as a direct member of `app:boardeffect:etc:boardeffect_admins`,
+but we can do better!  Responsibility for committee member management goes to
+the president's executive assistant, whoever that might be.
+
+#. Create a new reference group (role), `ref:roles:president_assistant`
+   for president executive assistant.
+#. Add Anna's account to `president_assistant`.
+
+This is better, but does Anna really need full admin privileges to
+`app:boardeffect`?  Probably only needs update / read.
+
+#. Add `ref:roles:president_assistant` to `app:boardeffect:etc:boardeffect_managers`.
+#. Remove Anna from `app:boardeffect:etc:boardeffect_admins`.
+
+
+----------------
+Exercise 401.3.7
+----------------
+
+*Global Committee reference groups*
+
+All working great-- new system request comes in with policy based on board
+committees.  Need to elevate app-specific ref groups to global ref groups.
+
+#. Create `ref:board` folder for board committee ref groups.
+#. Move `app:boardeffect:ref:cmt_fin` to `ref:board:cmt_fin`.
+
+    .. note::
+
+        The Board Effect policies are not impacted by moving the location of
+        the reference groups!
+
+#. Create `ref:board:etc` security folder.
+#. Create `ref:board:etc:board_managers` security group.
+#. Assign *UPDATE* and *READ* rights on reference groups to `board_managers`.
+#. Revoke *UPDATE* and *READ* rights of reference groups from `app:board_effect:etc:boardeffect_managers`.
+
+    .. warning::
+
+        Moving our reference groups did *not* remove the access we had granted
+        on them from application-specific security groups.  After moving a
+        reference group, it is good practive to review its permissions.
+
+#. Add `president_assistant` to `ref:board:etc:board_managers`.
+
+
+--------
+Epilogue
+--------
+
+New request comes in for four advisory councils. Each will have their own
+workroom in Board Effect.  Initially you are handed a spreadsheet with the
+council members and you import them into app-specific reference groups 
+(e.g. `app:boardeffect:ref:advisory_council_northeast`).  Later you find
+out that council membership is available in Banner, so you create loader
+jobs for those.  As it turns out, the spreadsheets were old and had the wrong
+members.  Thank goodness for loader jobs!  Alas, not all advisory council
+members have NetIDs.  To get them access we add them as sponsored accounts
+in COmanage.
+
+The End
+
+
+.. _Grouper Deployment Guide: https://spaces.at.internet2.edu/display/Grouper/Grouper+Deployment+Guide+Work+-TIER+Program
diff --git a/docs/401/401.4-example-solution.rst b/docs/401/401.4-example-solution.rst
new file mode 100644
index 0000000..4aba227
--- /dev/null
+++ b/docs/401/401.4-example-solution.rst
@@ -0,0 +1,26 @@
+
+==========================================================
+401.4 Untangling Legacy Access Policies - Example Solution
+==========================================================
+
+The follwing solution uses techniques demonstrated in the other 401 series
+labs in order to create an independent policy for the LMS service.
+
+#. Use Grouper Loader to import existing LDAP cohort group into a "community
+   members" reference group-- `ref:legacy:community_members`
+#. Add loader job to populate `communtiy_members` from
+   `cn=community_members,ou=groups,dc=example,dc=edu`.
+#. Run loader job to import members into reference group.
+#. Create a Grouper service folder for the LMS with a policy for LMS
+   authorization: `app:lms:lms_authorize|allow|deny`
+#. Add the "institutional people" reference group, `ref:community_members`,
+    to the allow policy for the LMS, `app:lms:lms_allow`.
+#. Create `app:lms:ref:visiting_scholars`.  Import the NetIDs for the visiting
+   scholors into this reference group.
+#. Add `visiting_scholars` to `lms_allow`.
+#. Provision this policy to a new group in the LDAP DIT that the LMS group can
+   use to allow access to the service.
+
+Congrats!  You are now a certified Grouper Guru associate level 1!  
+And remember nothing gets'em going like chum!
+
diff --git a/docs/401/401.4.rst b/docs/401/401.4.rst
new file mode 100644
index 0000000..712f57d
--- /dev/null
+++ b/docs/401/401.4.rst
@@ -0,0 +1,67 @@
+======================================
+401.4 Untangling Legacy Access Polcies
+======================================
+
+-------------------
+Learning Objectives
+-------------------
+
+* Learn to recognize tangled access control policies.
+* Use techniques to untangle co-mingled policies and cohorts.
+
+--------------
+Lab Components
+--------------
+
+* Grouper
+
+--------
+Overview
+--------
+
+A baseline of core services services are enabled by default for a broad range of
+community cohorts.  The current approach uses a hodge-podge of scripts and
+manual intervention to establish a group of "institutional people" that are
+granted access to a wide range of services.  The system can best be described as
+fragile, brittle, and difficult, if not impossible, to evolve and maintain.  In
+other words-- state of the industry.
+
+Last year your CIO came back from Internet2 Summit and declared that your
+institution is going to deploy TIER.  You've just managed to get the Grouper
+software up and running, when the head of your LMS group, Vicky, bursts into your
+office space and tells you that there are 50 visiting scholars showing up on
+campus tomorrow, and they all need access to the LMS for a campus-wide lecture
+series.
+
+Your co-worker had mentioned this to you before she left for her month long
+vacation.  She had told you she had taken care of creating the guest accounts,
+and not to worry.  You just need to grant access to the LMS when the time comes.
+No problem.
+
+But suddenly, you realize that access is controlled via the "institutional
+people" group in your Enterprise Directory Information Tree!  If you add the
+scholars to that group, they'll have access to everything on campus!
+
+Before panic sets in, you remember your Grouper training.  You'll need a little
+help from Vicky, but with Grouper, you've got this covered.  "OK, Vicky," you say
+in a calm, steady voice.  "Here's what I'm going to need your team to do ..."
+
+----------------
+Exercise 401.4.1
+----------------
+
+*Untangling Policies from Cohorts*
+
+The goal of this exercise is to grant access to the LMS for the 50 visiting
+scholar guest accounts *without* granting additional access to those accounts.
+Since access control does not happen in a vacuum, you'll need some minimal
+assistance from the LMS team.  Vicky's team can configure the LMS to point to a
+new group in the LDAP DIT, but that's all the help you'll get.
+
+The basic issue is that the legacy access control mechanisms are based on a 
+cohort of loosely defined "institutional people".  All your institution's services
+are using this cohort directly to determine who is supposed to have access.
+
+You'll need to use your new Grouper skills to resolve this issue.
+
+
diff --git a/docs/401/appendix.rst b/docs/401/appendix.rst
new file mode 100644
index 0000000..81a25d4
--- /dev/null
+++ b/docs/401/appendix.rst
@@ -0,0 +1,217 @@
+========
+Appendix
+========
+
+.. _apdx-401.1.4-auto-end-date:
+
+-------------------------------
+401.1.4 Automatic End Date Rule
+-------------------------------
+
+To configure the automatic rule end date on the access control list,
+`app:vpn:ref:vpn_adhoc` you must use the Grouper Shell (GSH) to run
+a short script.  To run GSH, you must connect to the GTE container
+that has the Grouper API installed:
+
+.. code-block:: bash
+
+   root# docker exec -it CONTAINER_NAME /bin/bash 
+   bash# cd bin
+   bash# gsh
+
+At this point you can paste in the following script:
+
+.. code-block:: groovy
+   :emphasize-lines: 1,3
+   :linenos:
+
+   numDays = 180;
+   actAs = SubjectFinder.findRootSubject();
+   vpn_adhoc = getGroups("app:vpn:ref:vpn_adhoc")[0];
+   attribAssign = vpn_adhoc.getAttributeDelegate().addAttribute(RuleUtils.ruleAttributeDefName()).getAttributeAssign();
+   attribValueDelegate = attribAssign.getAttributeValueDelegate();
+   attribValueDelegate.assignValue(RuleUtils.ruleActAsSubjectSourceIdName(), actAs.getSourceId());
+   attribValueDelegate.assignValue(RuleUtils.ruleRunDaemonName(), "F");
+   attribValueDelegate.assignValue(RuleUtils.ruleActAsSubjectIdName(), actAs.getId());
+   attribValueDelegate.assignValue(RuleUtils.ruleCheckTypeName(), RuleCheckType.membershipAdd.name());
+   attribValueDelegate.assignValue(RuleUtils.ruleIfConditionEnumName(), RuleIfConditionEnum.thisGroupHasImmediateEnabledNoEndDateMembership.name());
+   attribValueDelegate.assignValue(RuleUtils.ruleThenEnumName(), RuleThenEnum.assignMembershipDisabledDaysForOwnerGroupId.name());
+   attribValueDelegate.assignValue(RuleUtils.ruleThenEnumArg0Name(), numDays.toString());
+   attribValueDelegate.assignValue(RuleUtils.ruleThenEnumArg1Name(), "T");
+
+.. _apdx-401.1.5-pit-query:
+
+--------------------------------------
+401.1.5 Point-in-Time Membership Query
+--------------------------------------
+
+.. code-block:: sql
+   :linenos:
+
+   SELECT 
+       gpm.SUBJECT_ID, 
+       gpg.NAME, 
+       FROM_UNIXTIME(gpmav.MEMBERSHIP_START_TIME / 1000000) start_time, 
+       FROM_UNIXTIME(gpmav.MEMBERSHIP_END_TIME / 1000000) end_time 
+   FROM grouper_pit_memberships_all_v gpmav 
+       INNER JOIN grouper_pit_groups gpg 
+           ON gpmav.owner_group_id = gpg.id 
+       INNER JOIN grouper_pit_members gpm 
+           ON gpmav.MEMBER_ID = gpm.id 
+       INNER JOIN grouper_pit_fields gpf 
+           ON gpmav.field_id = gpf.id
+   WHERE gpg.name = 'app:vpn:vpn_authorized' 
+   AND gpm.subject_type = 'person'
+   AND gpf.name = 'members'
+   ORDER BY gpmav.MEMBERSHIP_START_TIME DESC 
+   ;
+
+
+.. _apdx-401.2.5-future-memberships-query:
+
+--------------------------------
+401.2.5 Future Memberships Query
+--------------------------------
+
+.. code-block:: sql
+   :linenos:
+
+   SELECT 
+       ggv.name,
+       FROM_UNIXTIME(gmav.IMMEDIATE_MSHIP_ENABLED_TIME / 1000) enabled_time,
+       CASE
+           WHEN gm.subject_type = 'group' THEN gm.subject_identifier0
+           ELSE gm.subject_id
+       END member
+   FROM `grouper_memberships_all_v` gmav
+       INNER JOIN grouper_groups_v ggv
+           ON gmav.OWNER_GROUP_ID = ggv.GROUP_ID
+       INNER JOIN grouper_members gm
+           ON gmav.member_id = gm.id
+   WHERE gmav.IMMEDIATE_MSHIP_ENABLED_TIME IS NOT NULL
+   ;
+
+.. _apdx-401.3.1-app-skeleton:
+
+-----------------------------------
+401.3.1 Application Skeleton Script
+-----------------------------------
+
+This script automatically creates an application folder along with
+security groups and permission rules.
+You must use the Grouper Shell (GSH) to run
+a short script.  To run GSH, you must connect to the GTE container
+that has the Grouper API installed:
+
+.. code-block:: bash
+
+   root# docker exec -it CONTAINER_NAME /bin/bash 
+   bash# cd bin
+   bash# gsh
+
+At this point you can paste in the following script:
+
+.. code-block:: groovy
+   :emphasize-lines: 3,4
+   :linenos:
+
+   // SET THESE
+   parent_stem_path = "app";
+   app_extension = "boardeffect";
+   app_name = "Board Effect";
+    
+    
+   if (!app_name?.trim())
+   {
+       app_name = app_extension;
+   }
+    
+   def makeStemInheritable(obj, stemName, groupName, priv="admin") {
+       baseStem = obj.getStems(stemName)[0];
+       aGroup = obj.getGroups(groupName)[0];
+       RuleApi.inheritGroupPrivileges(
+           SubjectFinder.findRootSubject(),
+           baseStem,
+           Stem.Scope.SUB,
+           aGroup.toSubject(),
+           Privilege.getInstances(priv)
+       );
+       RuleApi.runRulesForOwner(baseStem);
+       if(priv == 'admin')
+       {
+           RuleApi.inheritFolderPrivileges(
+               SubjectFinder.findRootSubject(),
+               baseStem,
+               Stem.Scope.SUB,
+               aGroup.toSubject(),
+               Privilege.getInstances("stem, create"));
+       }
+       RuleApi.runRulesForOwner(baseStem);
+   }
+    
+   stem = addStem(parent_stem_path, app_extension, app_name);
+   etc_stem = addStem(stem.name, "etc", "etc");
+   admin_group_name = "${app_extension}_admins";
+   admin_group = addGroup(etc_stem.name, admin_group_name, admin_group_name);
+   admin_group.grantPriv(admin_group.toMember().getSubject(), AccessPrivilege.ADMIN);
+   mgr_group_name = "${app_extension}_mgr";
+   mgr_group = addGroup(etc_stem.name, mgr_group_name, mgr_group_name);
+   mgr_group.grantPriv(admin_group.toMember().getSubject(), AccessPrivilege.ADMIN);
+   mgr_group.grantPriv(mgr_group.toMember().getSubject(), AccessPrivilege.UPDATE);
+   mgr_group.grantPriv(mgr_group.toMember().getSubject(), AccessPrivilege.READ);
+   view_group_name = "${app_extension}_viewers";
+   view_group = addGroup(etc_stem.name, view_group_name, view_group_name);
+   view_group.grantPriv(view_group.toMember().getSubject(), AccessPrivilege.READ);
+   view_group.grantPriv(admin_group.toMember().getSubject(), AccessPrivilege.ADMIN);
+   view_group.grantPriv(mgr_group.toMember().getSubject(), AccessPrivilege.UPDATE);
+   view_group.grantPriv(mgr_group.toMember().getSubject(), AccessPrivilege.READ);
+   admin_group.grantPriv(view_group.toMember().getSubject(), AccessPrivilege.READ);
+   mgr_group.grantPriv(view_group.toMember().getSubject(), AccessPrivilege.READ);
+   // Child objects should also grant perms to these groups.
+   makeStemInheritable(this, stem.name, admin_group.name, 'admin');
+   makeStemInheritable(this, stem.name, mgr_group.name, 'update');
+   makeStemInheritable(this, stem.name, mgr_group.name, 'read');
+   makeStemInheritable(this, stem.name, view_group.name, 'read');
+   admin_group.revokePriv(mgr_group.toMember().getSubject(), AccessPrivilege.UPDATE);
+
+.. _apdx-401.3.5-temp-access:
+
+-------------------------------
+401.3.1 Temporary Access Script 
+-------------------------------
+
+This script automatically creates an application folder along with
+security groups and permission rules.
+You must use the Grouper Shell (GSH) to run
+a short script.  To run GSH, you must connect to the GTE container
+that has the Grouper API installed:
+
+.. code-block:: bash
+
+   root# docker exec -it CONTAINER_NAME /bin/bash 
+   bash# cd bin
+   bash# gsh
+
+At this point you can paste in the following script:
+
+.. code-block:: groovy
+   :emphasize-lines: 2,3
+   :linenos:
+
+   // Script parameters
+   group_name = "app:boardeffect:ref:workroom_helpers";
+   numDays = 3;
+    
+   actAs = SubjectFinder.findRootSubject();
+   vpn_adhoc = getGroups(group_name)[0];
+   attribAssign = vpn_adhoc.getAttributeDelegate().addAttribute(RuleUtils.ruleAttributeDefName()).getAttributeAssign();
+   attribValueDelegate = attribAssign.getAttributeValueDelegate();
+   attribValueDelegate.assignValue(RuleUtils.ruleActAsSubjectSourceIdName(), actAs.getSourceId());
+   attribValueDelegate.assignValue(RuleUtils.ruleRunDaemonName(), "F");
+   attribValueDelegate.assignValue(RuleUtils.ruleActAsSubjectIdName(), actAs.getId());
+   attribValueDelegate.assignValue(RuleUtils.ruleCheckTypeName(), RuleCheckType.membershipAdd.name());
+   attribValueDelegate.assignValue(RuleUtils.ruleIfConditionEnumName(), RuleIfConditionEnum.thisGroupHasImmediateEnabledNoEndDateMembership.name());
+   attribValueDelegate.assignValue(RuleUtils.ruleThenEnumName(), RuleThenEnum.assignMembershipDisabledDaysForOwnerGroupId.name());
+   attribValueDelegate.assignValue(RuleUtils.ruleThenEnumArg0Name(), numDays.toString());
+   attribValueDelegate.assignValue(RuleUtils.ruleThenEnumArg1Name(), "T");
+
diff --git a/docs/401/examples/401.1.3-pspng-config.properties b/docs/401/examples/401.1.3-pspng-config.properties
new file mode 100644
index 0000000..9f356a1
--- /dev/null
+++ b/docs/401/examples/401.1.3-pspng-config.properties
@@ -0,0 +1,90 @@
+#specify the consumers here.  specify the consumer name after the changeLog.consumer. part.  This example is "psp"
+#but it could be changeLog.consumer.myConsumerName.class
+#the class must extend edu.internet2.middleware.grouper.changeLog.ChangeLogConsumerBase
+#changeLog.consumer.psp.class = edu.internet2.middleware.psp.grouper.PspChangeLogConsumer
+
+#the quartz cron is a cron-like string.  it defaults to every minute on the minute (since the temp to change log job runs
+#at 10 seconds to each minute).  it defaults to this: 0 * * * * ?                                          
+#though it will stagger each one by 2 seconds                                                              
+# http://www.quartz-scheduler.org/documentation/quartz-1.x/tutorials/crontrigger                           
+#changeLog.consumer.psp.quartzCron = 0 * * * * ?                                                          
+                                                                                                           
+# To retry processing a change log entry if an error occurs, set retryOnError to true. Defaults to false.  
+#changeLog.consumer.psp.retryOnError = false                                                              
+                                                                                                           
+# To run full provisioning synchronizations periodically, provide the class name which provides a 'public void fullSync()' method.
+#changeLog.psp.fullSync.class = edu.internet2.middleware.psp.grouper.PspChangeLogConsumer                 
+                                                                                                           
+# Schedule full synchronizations. Defaults to 5 am : 0 0 5 * * ?.                                          
+#changeLog.psp.fullSync.quartzCron = 0 0 5 * * ?
+                                                                                                           
+# Run a full synchronization job at startup. Defaults to false.                                            
+#changeLog.psp.fullSync.runAtStartup = false                                                              
+                                                                                                           
+# Omit diff responses from bulk response to conserve memory.                                               
+#changeLog.psp.fullSync.omitDiffResponses = true                                                          
+                                                                                                           
+# Omit sync responses from bulk response to conserve memory.                                               
+#changeLog.psp.fullSync.omitSyncResponses = true 
+
+#################################
+## LDAP connections
+#################################
+# specify the ldap connection with user, pass, url
+# the string after "ldap." is the ID of the connection, and it should not have
+# spaces or other special chars in it.  In this case is it "personLdap"
+ 
+#note the URL should start with ldap: or ldaps: if it is SSL.  
+#It should contain the server and port (optional if not default), and baseDn,
+#e.g. ldaps://ldapserver.school.edu:636/dc=school,dc=edu
+ldap.demo.url = ldap://localhost:389/
+ 
+#optional, if authenticated
+ldap.demo.user = cn=root,dc=internet2,dc=edu
+ 
+#optional, if authenticated note the password can be stored encrypted in an external file
+ldap.demo.pass = password
+ 
+#optional, if you are using tls, set this to true.  Generally you will not be using an SSL URL to use TLS...
+ldap.demo.tls = false
+ 
+#optional, if using sasl
+#ldap.personLdap.saslAuthorizationId =
+#ldap.personLdap.saslRealm =
+ 
+#optional (note, time limit is for search operations, timeout is for connection timeouts),
+#most of these default to vt-ldap defaults.  times are in millis
+#validateOnCheckout defaults to true if all other validate methods are false
+#ldap.personLdap.batchSize =
+#ldap.personLdap.countLimit =
+#ldap.personLdap.timeLimit =
+#ldap.personLdap.timeout =
+#ldap.personLdap.minPoolSize =
+#ldap.personLdap.maxPoolSize =
+#ldap.personLdap.validateOnCheckIn =
+#ldap.personLdap.validateOnCheckOut =
+#ldap.personLdap.validatePeriodically =
+#ldap.personLdap.validateTimerPeriod =
+#ldap.personLdap.pruneTimerPeriod =
+#if connections expire after a certain amount of time, this is it, in millis, defaults to 300000 (5 minutes)
+#ldap.personLdap.expirationTime =
+
+#make the paths fully qualified and not relative to the loader group.
+loader.ldap.requireTopStemAsStemFromConfigGroup=false
+
+changeLog.consumer.pspng_groupOfNames.class = edu.internet2.middleware.grouper.pspng.PspChangelogConsumerShim
+changeLog.consumer.pspng_groupOfNames.type = edu.internet2.middleware.grouper.pspng.LdapGroupProvisioner
+changeLog.consumer.pspng_groupOfNames.quartzCron = 0 * * * * ?
+changeLog.consumer.pspng_groupOfNames.ldapPoolName = demo
+changeLog.consumer.pspng_groupOfNames.supportsEmptyGroups = false
+changeLog.consumer.pspng_groupOfNames.memberAttributeName = member
+changeLog.consumer.pspng_groupOfNames.memberAttributeValueFormat = ${ldapUser.getDn()}
+changeLog.consumer.pspng_groupOfNames.groupSearchBaseDn = ou=groups,dc=internet2,dc=edu
+changeLog.consumer.pspng_groupOfNames.allGroupsSearchFilter = objectclass=groupOfNames
+changeLog.consumer.pspng_groupOfNames.singleGroupSearchFilter = (&(objectclass=groupOfNames)(cn=${group.name}))
+changeLog.consumer.pspng_groupOfNames.groupSearchAttributes = cn,objectclass
+changeLog.consumer.pspng_groupOfNames.groupCreationLdifTemplate = dn: cn=${group.name}||cn: ${group.name}||objectclass: groupOfNames
+changeLog.consumer.pspng_groupOfNames.userSearchBaseDn = ou=people,dc=internet2,dc=edu
+changeLog.consumer.pspng_groupOfNames.userSearchFilter = uid=${subject.id}
+changeLog.consumer.pspng_groupOfNames.grouperIsAuthoritative = false
+
diff --git a/docs/401/examples/401.2.2-pspng-config.properties b/docs/401/examples/401.2.2-pspng-config.properties
new file mode 100644
index 0000000..1050e7f
--- /dev/null
+++ b/docs/401/examples/401.2.2-pspng-config.properties
@@ -0,0 +1,100 @@
+#specify the consumers here.  specify the consumer name after the changeLog.consumer. part.  This example is "psp"
+#but it could be changeLog.consumer.myConsumerName.class
+#the class must extend edu.internet2.middleware.grouper.changeLog.ChangeLogConsumerBase
+#changeLog.consumer.psp.class = edu.internet2.middleware.psp.grouper.PspChangeLogConsumer
+
+#the quartz cron is a cron-like string.  it defaults to every minute on the minute (since the temp to change log job runs
+#at 10 seconds to each minute).  it defaults to this: 0 * * * * ?                                          
+#though it will stagger each one by 2 seconds                                                              
+# http://www.quartz-scheduler.org/documentation/quartz-1.x/tutorials/crontrigger                           
+#changeLog.consumer.psp.quartzCron = 0 * * * * ?                                                          
+                                                                                                           
+# To retry processing a change log entry if an error occurs, set retryOnError to true. Defaults to false.  
+#changeLog.consumer.psp.retryOnError = false                                                              
+                                                                                                           
+# To run full provisioning synchronizations periodically, provide the class name which provides a 'public void fullSync()' method.
+#changeLog.psp.fullSync.class = edu.internet2.middleware.psp.grouper.PspChangeLogConsumer                 
+                                                                                                           
+# Schedule full synchronizations. Defaults to 5 am : 0 0 5 * * ?.                                          
+#changeLog.psp.fullSync.quartzCron = 0 0 5 * * ?
+                                                                                                           
+# Run a full synchronization job at startup. Defaults to false.                                            
+#changeLog.psp.fullSync.runAtStartup = false                                                              
+                                                                                                           
+# Omit diff responses from bulk response to conserve memory.                                               
+#changeLog.psp.fullSync.omitDiffResponses = true                                                          
+                                                                                                           
+# Omit sync responses from bulk response to conserve memory.                                               
+#changeLog.psp.fullSync.omitSyncResponses = true 
+
+#################################
+## LDAP connections
+#################################
+# specify the ldap connection with user, pass, url
+# the string after "ldap." is the ID of the connection, and it should not have
+# spaces or other special chars in it.  In this case is it "personLdap"
+ 
+#note the URL should start with ldap: or ldaps: if it is SSL.  
+#It should contain the server and port (optional if not default), and baseDn,
+#e.g. ldaps://ldapserver.school.edu:636/dc=school,dc=edu
+ldap.demo.url = ldap://localhost:389/
+ 
+#optional, if authenticated
+ldap.demo.user = cn=root,dc=internet2,dc=edu
+ 
+#optional, if authenticated note the password can be stored encrypted in an external file
+ldap.demo.pass = password
+ 
+#optional, if you are using tls, set this to true.  Generally you will not be using an SSL URL to use TLS...
+ldap.demo.tls = false
+ 
+#optional, if using sasl
+#ldap.personLdap.saslAuthorizationId =
+#ldap.personLdap.saslRealm =
+ 
+#optional (note, time limit is for search operations, timeout is for connection timeouts),
+#most of these default to vt-ldap defaults.  times are in millis
+#validateOnCheckout defaults to true if all other validate methods are false
+#ldap.personLdap.batchSize =
+#ldap.personLdap.countLimit =
+#ldap.personLdap.timeLimit =
+#ldap.personLdap.timeout =
+#ldap.personLdap.minPoolSize =
+#ldap.personLdap.maxPoolSize =
+#ldap.personLdap.validateOnCheckIn =
+#ldap.personLdap.validateOnCheckOut =
+#ldap.personLdap.validatePeriodically =
+#ldap.personLdap.validateTimerPeriod =
+#ldap.personLdap.pruneTimerPeriod =
+#if connections expire after a certain amount of time, this is it, in millis, defaults to 300000 (5 minutes)
+#ldap.personLdap.expirationTime =
+
+#make the paths fully qualified and not relative to the loader group.
+loader.ldap.requireTopStemAsStemFromConfigGroup=false
+
+changeLog.consumer.pspng_groupOfNames.class = edu.internet2.middleware.grouper.pspng.PspChangelogConsumerShim
+changeLog.consumer.pspng_groupOfNames.type = edu.internet2.middleware.grouper.pspng.LdapGroupProvisioner
+changeLog.consumer.pspng_groupOfNames.quartzCron = 0 * * * * ?
+changeLog.consumer.pspng_groupOfNames.ldapPoolName = demo
+changeLog.consumer.pspng_groupOfNames.supportsEmptyGroups = false
+changeLog.consumer.pspng_groupOfNames.memberAttributeName = member
+changeLog.consumer.pspng_groupOfNames.memberAttributeValueFormat = ${ldapUser.getDn()}
+changeLog.consumer.pspng_groupOfNames.groupSearchBaseDn = ou=groups,dc=internet2,dc=edu
+changeLog.consumer.pspng_groupOfNames.allGroupsSearchFilter = objectclass=groupOfNames
+changeLog.consumer.pspng_groupOfNames.singleGroupSearchFilter = (&(objectclass=groupOfNames)(cn=${group.name}))
+changeLog.consumer.pspng_groupOfNames.groupSearchAttributes = cn,objectclass
+changeLog.consumer.pspng_groupOfNames.groupCreationLdifTemplate = dn: cn=${group.name}||cn: ${group.name}||objectclass: groupOfNames
+changeLog.consumer.pspng_groupOfNames.userSearchBaseDn = ou=people,dc=internet2,dc=edu
+changeLog.consumer.pspng_groupOfNames.userSearchFilter = uid=${subject.id}
+changeLog.consumer.pspng_groupOfNames.grouperIsAuthoritative = false
+
+
+changeLog.consumer.pspng_entitlements.class = edu.internet2.middleware.grouper.pspng.PspChangelogConsumerShim
+changeLog.consumer.pspng_entitlements.type = edu.internet2.middleware.grouper.pspng.LdapAttributeProvisioner
+changeLog.consumer.pspng_entitlements.quartzCron = 0 * * * * ?
+changeLog.consumer.pspng_entitlements.ldapPoolName = demo
+changeLog.consumer.pspng_entitlements.provisionedAttributeName = eduPersonEntitlement
+changeLog.consumer.pspng_entitlements.provisionedAttributeValueFormat = ${group.name.equalsIgnoreCase('app:mfa:mfa_enabled') ? 'http://tier.internet2.edu/mfa/enabled' : 'urn:mace:example.edu:' + group.extension}
+changeLog.consumer.pspng_entitlements.userSearchBaseDn = ou=people,dc=internet2,dc=edu
+changeLog.consumer.pspng_entitlements.userSearchFilter = uid=${subject.id}
+changeLog.consumer.pspng_entitlements.allProvisionedValuesPrefix=*
diff --git a/docs/401/examples/401.2.3-general-authn.xml b/docs/401/examples/401.2.3-general-authn.xml
new file mode 100644
index 0000000..152d8e2
--- /dev/null
+++ b/docs/401/examples/401.2.3-general-authn.xml
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:context="http://www.springframework.org/schema/context"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xmlns:p="http://www.springframework.org/schema/p"
+       xmlns:c="http://www.springframework.org/schema/c"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+                           
+       default-init-method="initialize"
+       default-destroy-method="destroy">
+
+    <!--
+    This file provisions the IdP with information about the configured login mechanisms available for use.
+    The actual beans and subflows that make up those mechanisms are in their own files, but this pulls them
+    together with deployer-supplied metadata to describe them to the system.
+    
+    You can turn on and off individual mechanisms by adding and remove them here. Nothing left out will
+    be used, regardless any other files loaded by the Spring container.
+    
+    Flow defaults include: no support for IsPassive/ForceAuthn, support for non-browser clients enabled,
+    and default timeout and lifetime values set via properties. We also default to supporting the SAML 1/2
+    expressions for password-based authentication over a secure channel, so anything more exotic requires
+    customization, as the examples below for IP address and SPNEGO authentication illustrate.
+    -->
+
+    <util:list id="shibboleth.AvailableAuthenticationFlows">
+        
+        <bean id="authn/IPAddress" parent="shibboleth.AuthenticationFlow"
+                p:passiveAuthenticationSupported="true"
+                p:lifetime="PT60S" p:inactivityTimeout="PT60S">
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol" />
+                </list>
+            </property>
+        </bean>
+
+        <bean id="authn/SPNEGO" parent="shibboleth.AuthenticationFlow"
+                p:nonBrowserSupported="false">
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:Kerberos" />
+                    <bean parent="shibboleth.SAML1AuthenticationMethod"
+                        c:method="urn:ietf:rfc:1510" />
+                </list>
+            </property>
+        </bean>
+        
+        <bean id="authn/External" parent="shibboleth.AuthenticationFlow"
+            p:nonBrowserSupported="false" />
+
+        <bean id="authn/RemoteUser" parent="shibboleth.AuthenticationFlow"
+            p:nonBrowserSupported="false" />
+
+        <bean id="authn/RemoteUserInternal" parent="shibboleth.AuthenticationFlow" />
+
+        <bean id="authn/X509" parent="shibboleth.AuthenticationFlow"
+                p:nonBrowserSupported="false">
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:X509" />
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient" />
+                    <bean parent="shibboleth.SAML1AuthenticationMethod"
+                        c:method="urn:ietf:rfc:2246" />
+                </list>
+            </property>
+        </bean>
+
+        <bean id="authn/X509Internal" parent="shibboleth.AuthenticationFlow">
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:X509" />
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:TLSClient" />
+                    <bean parent="shibboleth.SAML1AuthenticationMethod"
+                        c:method="urn:ietf:rfc:2246" />
+                </list>
+            </property>
+        </bean>
+
+        <bean id="authn/Password" parent="shibboleth.AuthenticationFlow"
+                p:passiveAuthenticationSupported="true"
+                p:forcedAuthenticationSupported="true" />
+
+        <bean id="authn/Duo" parent="shibboleth.AuthenticationFlow"
+                p:forcedAuthenticationSupported="true"
+                p:nonBrowserSupported="false">
+            <!--
+            The list below should be changed to reflect whatever locally- or
+            community-defined values are appropriate to represent MFA. It is
+            strongly advised that the value not be specific to Duo or any
+            particular technology.
+            -->
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="http://example.org/ac/classes/mfa" />
+                    <bean parent="shibboleth.SAML1AuthenticationMethod"
+                        c:method="http://example.org/ac/classes/mfa" />
+                </list>
+            </property>
+        </bean>
+
+        <!-- A Mock MFA provider for this tutorial -->
+        <bean id="authn/Gaspo" parent="shibboleth.AuthenticationFlow"
+                p:forcedAuthenticationSupported="true"
+                p:nonBrowserSupported="false">
+            <!--
+            The list below should be changed to reflect whatever locally- or
+            community-defined values are appropriate to represent MFA. It is
+            strongly advised that the value not be specific to Duo or any
+            particular technology.
+            -->
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="https://refeds.org/profile/mfa" />
+                    <bean parent="shibboleth.SAML1AuthenticationMethod"
+                        c:method="https://refeds.org/profile/mfa" />
+                </list>
+            </property>
+        </bean>
+
+        <bean id="authn/MFA" parent="shibboleth.AuthenticationFlow"
+                p:passiveAuthenticationSupported="true"
+                p:forcedAuthenticationSupported="true">
+            <!--
+            The list below almost certainly requires changes, and should generally be the
+            union of any of the separate factors you combine in your particular MFA flow
+            rules. The example corresponds to the example in mfa-authn-config.xml that
+            combines GaspoMFA with Password.
+            -->
+            <property name="supportedPrincipals">
+                <list>
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:InternetProtocol" />
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" />
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:Password" />
+                    <bean parent="shibboleth.SAML1AuthenticationMethod"
+                        c:method="urn:oasis:names:tc:SAML:1.0:am:password" />
+                    <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                        c:classRef="https://refeds.org/profile/mfa" />
+                    <bean parent="shibboleth.SAML1AuthenticationMethod"
+                        c:method="https://refeds.org/profile/mfa" />
+
+                </list>
+            </property>
+        </bean>
+
+    </util:list>
+
+    <!--
+    This is a map used to "weight" particular methods above others if the IdP has to randomly select one
+    to insert into a SAML authentication statement. The typical use shown below is to bias the IdP in favor
+    of expressing the SAML 2 PasswordProtectedTransport class over the more vanilla Password class on the
+    assumption that the IdP doesn't accept passwords via an insecure channel. This map never causes the IdP
+    to violate its matching rules if an RP requests a particular value; it only matters when nothing specific
+    is chosen. Anything not in the map has a weight of zero.
+    -->
+    
+    <util:map id="shibboleth.AuthenticationPrincipalWeightMap">
+        <entry>
+            <key>
+                <bean parent="shibboleth.SAML2AuthnContextClassRef"
+                    c:classRef="urn:oasis:names:tc:SAML:2.0:ac:classes:PasswordProtectedTransport" />
+            </key>
+            <value>1</value>
+        </entry>
+    </util:map>
+
+</beans>
diff --git a/docs/401/examples/401.2.3-mfa-authn-config.xml b/docs/401/examples/401.2.3-mfa-authn-config.xml
new file mode 100644
index 0000000..f40a3de
--- /dev/null
+++ b/docs/401/examples/401.2.3-mfa-authn-config.xml
@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<beans xmlns="http://www.springframework.org/schema/beans"
+       xmlns:context="http://www.springframework.org/schema/context"
+       xmlns:util="http://www.springframework.org/schema/util"
+       xmlns:p="http://www.springframework.org/schema/p"
+       xmlns:c="http://www.springframework.org/schema/c"
+       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
+                           http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
+                           http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"
+
+       default-init-method="initialize"
+       default-destroy-method="destroy">
+
+    <!--
+    This is a map of transition rules that guide the behavior of the MFA flow
+    and controls how factors are sequenced, skipped, etc. The key of each entry
+    is the name of the step/flow out of which control is passing. The starting
+    rule has an empty key.
+    Each entry is a bean inherited from "shibboleth.authn.MFA.Transition". Per
+    the Javadoc for net.shibboleth.idp.authn.MultiFactorAuthenticationTransition:
+        p:nextFlow (String)
+            - A flow to run if the previous step signaled a "proceed" event, for simple
+                transitions.
+        p:nextFlowStrategy (Function<ProfileRequestContext,String>)
+            - A function to run if the previous step signaled a "proceed" event, for dynamic
+                transitions. Returning null ends the MFA process.
+        p:nextFlowStrategyMap (Map<String,Object> where Object is String or Function<ProfileRequestContext,String>)
+            - Fully dynamic way of expressing control paths. Map is keyed by a previously
+                signaled event and the value is a flow to run or a function to
+                return the flow to run. Returning null ends the MFA process.
+    When no rule is provided, there's an implicit "null" that ends the MFA flow
+    with whatever event was last signaled. If the "proceed" event from a step is
+    the final event, then the MFA process attempts to complete itself successfully.
+    -->
+    <util:map id="shibboleth.authn.MFA.TransitionMap">
+        <!-- First rule runs the IPAddress login flow. -->
+        <entry key="">
+            <bean parent="shibboleth.authn.MFA.Transition" p:nextFlow="authn/Password" />
+        </entry>
+
+        <!--
+        Second rule runs a function if IPAddress succeeds, to determine whether an additional
+        factor is required.
+        -->
+        <entry key="authn/Password">
+            <bean parent="shibboleth.authn.MFA.Transition" p:nextFlowStrategy-ref="checkSecondFactor" />
+        </entry>
+
+        <!-- An implicit final rule will return whatever the final flow returns. -->
+    </util:map>
+
+    <!-- Example script to see if second factor is required. -->
+    <bean id="checkSecondFactor" parent="shibboleth.ContextFunctions.Scripted" factory-method="inlineScript"
+        p:customObject-ref="shibboleth.AttributeResolverService">
+        <constructor-arg>
+            <value>
+            <![CDATA[
+                nextFlow = null;
+                // Go straight to second factor if we have to, or set up for an attribute lookup first.
+                authCtx = input.getSubcontext("net.shibboleth.idp.authn.context.AuthenticationContext");
+                mfaCtx = authCtx.getSubcontext("net.shibboleth.idp.authn.context.MultiFactorAuthenticationContext");
+                if (mfaCtx.isAcceptable()) {
+                    // Attribute check is required to decide if first factor alone is enough.
+                    resCtx = input.getSubcontext(
+                        "net.shibboleth.idp.attribute.resolver.context.AttributeResolutionContext", true);
+                    // Look up the username using a standard function.
+                    usernameLookupStrategyClass
+                        = Java.type("net.shibboleth.idp.session.context.navigate.CanonicalUsernameLookupStrategy");
+                    usernameLookupStrategy = new usernameLookupStrategyClass();
+                    resCtx.setPrincipal(usernameLookupStrategy.apply(input));
+                    resCtx.getRequestedIdPAttributeNames().add("eduPersonEntitlement");
+                    resCtx.resolveAttributes(custom);
+                    // Check for an attribute that authorizes use of first factor.
+                    attribute = resCtx.getResolvedIdPAttributes().get("eduPersonEntitlement");
+                    valueType =  Java.type("net.shibboleth.idp.attribute.StringAttributeValue");
+                    if (attribute != null && attribute.getValues().contains(new valueType("http://tier.internet2.edu/mfa/enabled"))) {
+                        nextFlow = "authn/Gaspo";
+                    }
+                    input.removeSubcontext(resCtx);   // cleanup
+                }
+                nextFlow;   // pass control to second factor or end with the first
+            ]]>
+            </value>
+        </constructor-arg>
+    </bean>
+
+</beans>
diff --git a/docs/401/examples/401.2.4-athletics-dept.txt b/docs/401/examples/401.2.4-athletics-dept.txt
new file mode 100644
index 0000000..27e4405
--- /dev/null
+++ b/docs/401/examples/401.2.4-athletics-dept.txt
@@ -0,0 +1,15 @@
+ahenderson36
+amorrison42
+bsmith65
+cthompson28
+janderson13
+jdavis4
+jlangenberg100
+jprice108
+jvales117
+ldavis5
+mgrady137
+mmartinez133
+nscott103
+pthompson61
+rdavis16
diff --git a/docs/401/examples/401.2.5-banner-netids.txt b/docs/401/examples/401.2.5-banner-netids.txt
new file mode 100644
index 0000000..2362df0
--- /dev/null
+++ b/docs/401/examples/401.2.5-banner-netids.txt
@@ -0,0 +1,5 @@
+agasper508
+agasper678
+alopez899
+aprice362
+agrady791
diff --git a/docs/401/examples/401.3.2-grouper-loader.properties b/docs/401/examples/401.3.2-grouper-loader.properties
new file mode 100644
index 0000000..45f2b18
--- /dev/null
+++ b/docs/401/examples/401.3.2-grouper-loader.properties
@@ -0,0 +1,118 @@
+#specify the consumers here.  specify the consumer name after the changeLog.consumer. part.  This example is "psp"
+#but it could be changeLog.consumer.myConsumerName.class
+#the class must extend edu.internet2.middleware.grouper.changeLog.ChangeLogConsumerBase
+#changeLog.consumer.psp.class = edu.internet2.middleware.psp.grouper.PspChangeLogConsumer
+
+#the quartz cron is a cron-like string.  it defaults to every minute on the minute (since the temp to change log job runs
+#at 10 seconds to each minute).  it defaults to this: 0 * * * * ?                                          
+#though it will stagger each one by 2 seconds                                                              
+# http://www.quartz-scheduler.org/documentation/quartz-1.x/tutorials/crontrigger                           
+#changeLog.consumer.psp.quartzCron = 0 * * * * ?                                                          
+                                                                                                           
+# To retry processing a change log entry if an error occurs, set retryOnError to true. Defaults to false.  
+#changeLog.consumer.psp.retryOnError = false                                                              
+                                                                                                           
+# To run full provisioning synchronizations periodically, provide the class name which provides a 'public void fullSync()' method.
+#changeLog.psp.fullSync.class = edu.internet2.middleware.psp.grouper.PspChangeLogConsumer                 
+                                                                                                           
+# Schedule full synchronizations. Defaults to 5 am : 0 0 5 * * ?.                                          
+#changeLog.psp.fullSync.quartzCron = 0 0 5 * * ?
+                                                                                                           
+# Run a full synchronization job at startup. Defaults to false.                                            
+#changeLog.psp.fullSync.runAtStartup = false                                                              
+                                                                                                           
+# Omit diff responses from bulk response to conserve memory.                                               
+#changeLog.psp.fullSync.omitDiffResponses = true                                                          
+                                                                                                           
+# Omit sync responses from bulk response to conserve memory.                                               
+#changeLog.psp.fullSync.omitSyncResponses = true 
+
+#################################
+## LDAP connections
+#################################
+# specify the ldap connection with user, pass, url
+# the string after "ldap." is the ID of the connection, and it should not have
+# spaces or other special chars in it.  In this case is it "personLdap"
+ 
+#note the URL should start with ldap: or ldaps: if it is SSL.  
+#It should contain the server and port (optional if not default), and baseDn,
+#e.g. ldaps://ldapserver.school.edu:636/dc=school,dc=edu
+ldap.demo.url = ldap://localhost:389/
+ 
+#optional, if authenticated
+ldap.demo.user = cn=root,dc=internet2,dc=edu
+ 
+#optional, if authenticated note the password can be stored encrypted in an external file
+ldap.demo.pass = password
+ 
+#optional, if you are using tls, set this to true.  Generally you will not be using an SSL URL to use TLS...
+ldap.demo.tls = false
+ 
+#optional, if using sasl
+#ldap.personLdap.saslAuthorizationId =
+#ldap.personLdap.saslRealm =
+ 
+#optional (note, time limit is for search operations, timeout is for connection timeouts),
+#most of these default to vt-ldap defaults.  times are in millis
+#validateOnCheckout defaults to true if all other validate methods are false
+#ldap.personLdap.batchSize =
+#ldap.personLdap.countLimit =
+#ldap.personLdap.timeLimit =
+#ldap.personLdap.timeout =
+#ldap.personLdap.minPoolSize =
+#ldap.personLdap.maxPoolSize =
+#ldap.personLdap.validateOnCheckIn =
+#ldap.personLdap.validateOnCheckOut =
+#ldap.personLdap.validatePeriodically =
+#ldap.personLdap.validateTimerPeriod =
+#ldap.personLdap.pruneTimerPeriod =
+#if connections expire after a certain amount of time, this is it, in millis, defaults to 300000 (5 minutes)
+#ldap.personLdap.expirationTime =
+
+#make the paths fully qualified and not relative to the loader group.
+loader.ldap.requireTopStemAsStemFromConfigGroup=false
+
+changeLog.consumer.pspng_groupOfNames.class = edu.internet2.middleware.grouper.pspng.PspChangelogConsumerShim
+changeLog.consumer.pspng_groupOfNames.type = edu.internet2.middleware.grouper.pspng.LdapGroupProvisioner
+changeLog.consumer.pspng_groupOfNames.quartzCron = 0 * * * * ?
+changeLog.consumer.pspng_groupOfNames.ldapPoolName = demo
+changeLog.consumer.pspng_groupOfNames.supportsEmptyGroups = false
+changeLog.consumer.pspng_groupOfNames.memberAttributeName = member
+changeLog.consumer.pspng_groupOfNames.memberAttributeValueFormat = ${ldapUser.getDn()}
+changeLog.consumer.pspng_groupOfNames.groupSearchBaseDn = ou=groups,dc=internet2,dc=edu
+changeLog.consumer.pspng_groupOfNames.allGroupsSearchFilter = objectclass=groupOfNames
+changeLog.consumer.pspng_groupOfNames.singleGroupSearchFilter = (&(objectclass=groupOfNames)(cn=${group.name}))
+changeLog.consumer.pspng_groupOfNames.groupSearchAttributes = cn,objectclass
+changeLog.consumer.pspng_groupOfNames.groupCreationLdifTemplate = dn: cn=${group.name}||cn: ${group.name}||objectclass: groupOfNames
+changeLog.consumer.pspng_groupOfNames.userSearchBaseDn = ou=people,dc=internet2,dc=edu
+changeLog.consumer.pspng_groupOfNames.userSearchFilter = uid=${subject.id}
+changeLog.consumer.pspng_groupOfNames.grouperIsAuthoritative = false
+
+
+changeLog.consumer.pspng_entitlements.class = edu.internet2.middleware.grouper.pspng.PspChangelogConsumerShim
+changeLog.consumer.pspng_entitlements.type = edu.internet2.middleware.grouper.pspng.LdapAttributeProvisioner
+changeLog.consumer.pspng_entitlements.quartzCron = 0 * * * * ?
+changeLog.consumer.pspng_entitlements.ldapPoolName = demo
+changeLog.consumer.pspng_entitlements.provisionedAttributeName = eduPersonEntitlement
+changeLog.consumer.pspng_entitlements.provisionedAttributeValueFormat = ${group.name.equalsIgnoreCase('app:mfa:mfa_enabled') ? 'http://tier.internet2.edu/mfa/enabled' : (group.name.equalsIgnoreCase('app:boardeffect:boardeffect_authorized') ? 'https://college.boardeffect.com/' : 'urn:mace:example.edu:' + group.extension) }
+changeLog.consumer.pspng_entitlements.userSearchBaseDn = ou=people,dc=internet2,dc=edu
+changeLog.consumer.pspng_entitlements.userSearchFilter = uid=${subject.id}
+changeLog.consumer.pspng_entitlements.allProvisionedValuesPrefix=*
+
+#####################################
+## Messaging integration with change log
+#####################################
+changeLog.consumer.rabbitMqMessagingSample.quartzCron = 0 * * * * ?                                                          
+
+# note, change "messagingSample" in key to be the name of the consumer.  e.g. changeLog.consumer.someNameAnyName.class
+changeLog.consumer.rabbitMqMessagingSample.class = edu.internet2.middleware.grouper.changeLog.esb.consumer.EsbConsumer
+
+changeLog.consumer.rabbitMqMessagingSample.publisher.class = edu.internet2.middleware.grouper.changeLog.esb.consumer.EsbMessagingPublisher
+changeLog.consumer.rabbitMqMessagingSample.publisher.messagingSystemName = rabbitmq
+# note, routingKey property is valid only for rabbitmq. For other messaging systems, it is ignored.
+changeLog.consumer.rabbitMqMessagingSample.publisher.routingKey = 
+## queue or topic
+changeLog.consumer.rabbitMqMessagingSample.publisher.messageQueueType = queue
+changeLog.consumer.rabbitMqMessagingSample.publisher.queueOrTopicName = grouper
+## this is optional if not using "id" for subjectId, need to be a subject attribute in the sources.xml
+#changeLog.consumer.rabbitMqMessagingSample.publisher.addSubjectAttributes = email
diff --git a/docs/401/examples/401.3.2-grouper.client.properties b/docs/401/examples/401.3.2-grouper.client.properties
new file mode 100644
index 0000000..8edc9a9
--- /dev/null
+++ b/docs/401/examples/401.3.2-grouper.client.properties
@@ -0,0 +1,112 @@
+#
+# Copyright 2014 Internet2
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+#
+# Grouper client configuration
+# $Id: grouper.client.example.properties,v 1.24 2009-12-30 04:23:02 mchyzer Exp $
+#
+
+# The grouper client uses Grouper Configuration Overlays (documented on wiki)
+# By default the configuration is read from grouper.client.base.properties
+# (which should not be edited), and the grouper.client.properties overlays
+# the base settings.  See the grouper.client.base.properties for the possible
+# settings that can be applied to the grouper.client.properties
+
+########################################
+## LDAP connection settings
+########################################
+
+# url of directory, including the base DN (distinguished name)
+# e.g. ldap://server.school.edu/dc=school,dc=edu
+# e.g. ldaps://server.school.edu/dc=school,dc=edu
+grouperClient.ldap.url =
+
+# kerberos principal used to connect to ldap
+grouperClient.ldap.login =
+
+# password for shared secret authentication to ldap
+# or you can put a filename with an encrypted password
+grouperClient.ldap.password =
+
+########################################
+## Web service Connection settings
+########################################
+
+# url of web service, should include everything up to the first resource to access
+# e.g. http://groups.school.edu:8090/grouper-ws/servicesRest
+# e.g. https://groups.school.edu/grouper-ws/servicesRest
+grouperClient.webService.url = https://localhost/grouper-ws/servicesRest
+
+# kerberos principal used to connect to web service
+grouperClient.webService.login = banderson
+
+# password for shared secret authentication to web service
+# or you can put a filename with an encrypted password
+grouperClient.webService.password.elConfig = password
+
+
+################################
+## Grouper Messaging System
+################################
+ 
+# name of messaging system which is the default
+grouper.messaging.default.name.of.messaging.system = rabbitmq
+ 
+# name of a messaging system.  note, "grouperBuiltinMessaging" can be arbitrary
+# grouper.messaging.system.grouperBuiltinMessaging.name = grouperBuiltinMessaging
+ 
+# class that implements edu.internet2.middleware.grouperClient.messaging.GrouperMessagingSystem
+# grouper.messaging.system.grouperBuiltinMessaging.class = edu.internet2.middleware.grouper.messaging.GrouperBuiltinMessagingSystem
+ 
+# name of a messaging system.  note, "grouperBuiltinMessaging" can be arbitrary
+grouper.messaging.system.rabbitmqSystem.name = rabbitmqSystem
+ 
+# class that implements edu.internet2.middleware.grouperClient.messaging.GrouperMessagingSystem
+grouper.messaging.system.rabbitmqSystem.class = edu.internet2.middleware.grouperMessagingRabbitmq.GrouperMessagingRabbitmqSystem
+ 
+# host address of rabbitmq queue
+grouper.messaging.system.rabbitmqSystem.host = rabbitmq
+ 
+# virtual host of rabbitmq queue
+grouper.messaging.system.rabbitmqSystem.virtualhost =
+ 
+# port of rabbitmq queue
+grouper.messaging.system.rabbitmqSystem.port =
+ 
+grouper.messaging.system.rabbitmqSystem.defaultPageSize = 10
+ 
+grouper.messaging.system.rabbitmqSystem.maxPageSize = 50
+ 
+ 
+# name of a messaging system, required
+grouper.messaging.system.rabbitmq.name = rabbitmq
+ 
+# default system settings to this messaging system, note, there is only one level of inheritance
+grouper.messaging.system.rabbitmq.defaultSystemName = rabbitmqSystem
+
+grouper.messaging.system.rabbitmq.user = guest
+
+#pass
+grouper.messaging.system.rabbitmq.password.elConfig = guest
+# set the following three properties if you want to use TLS connection to rabbitmq. All three need to be populated.
+# TLS Version
+#grouper.messaging.system.rabbitmqSystem.tlsVersion = TLSv1.1
+ 
+# path to trust store file
+#grouper.messaging.system.rabbitmqSystem.pathToTrustStore =
+ 
+# trust passphrase
+#grouper.messaging.system.rabbitmqSystem.trustPassphrase =
diff --git a/docs/401/index.rst b/docs/401/index.rst
new file mode 100644
index 0000000..59cd18e
--- /dev/null
+++ b/docs/401/index.rst
@@ -0,0 +1,22 @@
+Access Goverance Practicum (401)
+================================
+
+The practicum consists of four labs where students can apply the knowledge they
+have learned from previous Grouper courses. Students can demostrate how to
+implement access goverance using Grouper and associated `InCommon Trusted
+Access Platform`_ components by translating natural language policy into the
+appropriate digital policy. The access governance practicum incorporates much
+of the InCommon Trusted Access Platform and provides a “full IAM stack”
+experience.
+
+.. toctree::
+   :maxdepth: 2
+
+   401.1
+   401.2
+   401.3
+   401.4
+   401.4-example-solution
+   appendix
+
+.. _InCommon Trusted Access Platform: https://www.incommon.org/tap/
diff --git a/docs/401/intro.rst b/docs/401/intro.rst
new file mode 100644
index 0000000..aeeef28
--- /dev/null
+++ b/docs/401/intro.rst
@@ -0,0 +1,18 @@
+============
+Introduction
+============
+
+The TIER Access Governance Practicum (401) describes a series of real world
+access governance use cases that students can use to demonstrate knowledge
+of how to translate natural language policy into the appropriate digital
+policy in Grouper. The access governance practicum incorporates much of the
+TIER IAM suite of components and provides a “full TIER stack” experience.
+
+The Practicum consists of 4 labs that will allow students to apply knowledge
+they have learned from other Grouper courses.  They will present real-world
+access control scenarios.  Instructors will present techniques and possible
+solutions to these problems.
+
+
+
+
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000..298ea9e
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,19 @@
+# Minimal makefile for Sphinx documentation
+#
+
+# You can set these variables from the command line.
+SPHINXOPTS    =
+SPHINXBUILD   = sphinx-build
+SOURCEDIR     = .
+BUILDDIR      = _build
+
+# Put it first so that "make" without argument is like "make help".
+help:
+	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
+
+.PHONY: help Makefile
+
+# Catch-all target: route all unknown targets to Sphinx using the new
+# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
+%: Makefile
+	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
new file mode 100644
index 0000000..a52be6a
--- /dev/null
+++ b/docs/conf.py
@@ -0,0 +1,182 @@
+# -*- coding: utf-8 -*-
+#
+# Configuration file for the Sphinx documentation builder.
+#
+# This file does only contain a selection of the most common options. For a
+# full list see the documentation:
+# http://www.sphinx-doc.org/en/master/config
+
+# -- Path setup --------------------------------------------------------------
+
+# If extensions (or modules to document with autodoc) are in another directory,
+# add these directories to sys.path here. If the directory is relative to the
+# documentation root, use os.path.abspath to make it absolute, like shown here.
+#
+# import os
+# import sys
+# sys.path.insert(0, os.path.abspath('.'))
+
+
+# -- Project information -----------------------------------------------------
+
+project = 'Grouper Training Environment'
+copyright = '2019, Internet2'
+html_show_copyright = True
+author = 'Carl Waldbieser'
+
+# The short X.Y version
+version = ''
+# The full version, including alpha/beta/rc tags
+release = '062019'
+
+
+# -- General configuration ---------------------------------------------------
+
+# If your documentation needs a minimal Sphinx version, state it here.
+#
+# needs_sphinx = '1.0'
+
+# Add any Sphinx extension module names here, as strings. They can be
+# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
+# ones.
+extensions = [
+]
+
+# Add any paths that contain templates here, relative to this directory.
+templates_path = ['_templates']
+
+# The suffix(es) of source filenames.
+# You can specify multiple suffix as a list of string:
+#
+# source_suffix = ['.rst', '.md']
+source_suffix = '.rst'
+
+# The master toctree document.
+master_doc = 'index'
+
+# The language for content autogenerated by Sphinx. Refer to documentation
+# for a list of supported languages.
+#
+# This is also used if you do content translation via gettext catalogs.
+# Usually you set "language" from the command line for these cases.
+language = None
+
+# List of patterns, relative to source directory, that match files and
+# directories to ignore when looking for source files.
+# This pattern also affects html_static_path and html_extra_path.
+exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
+
+# The name of the Pygments (syntax highlighting) style to use.
+pygments_style = None
+
+
+# -- Options for HTML output -------------------------------------------------
+
+# The theme to use for HTML and HTML Help pages.  See the documentation for
+# a list of builtin themes.
+#
+#html_theme = 'agogo'
+#html_theme = 'alabaster'
+#html_theme = 'bizstyle'
+#html_theme = 'classic'
+html_theme = 'haiku'
+#html_theme = 'nature'
+#html_theme = 'pyramid'
+#html_theme = 'scrolls'
+#html_theme = 'traditional'
+
+# Theme options are theme-specific and customize the look and feel of a theme
+# further.  For a list of options available for each theme, see the
+# documentation.
+#
+# html_theme_options = {}
+
+# Add any paths that contain custom static files (such as style sheets) here,
+# relative to this directory. They are copied after the builtin static files,
+# so a file named "default.css" will overwrite the builtin "default.css".
+# html_static_path = ['_static']
+
+# Custom sidebar templates, must be a dictionary that maps document names
+# to template names.
+#
+# The default sidebars (for documents that don't match any pattern) are
+# defined by theme itself.  Builtin themes are using these templates by
+# default: ``['localtoc.html', 'relations.html', 'sourcelink.html',
+# 'searchbox.html']``.
+#
+# html_sidebars = {}
+
+
+# -- Options for HTMLHelp output ---------------------------------------------
+
+# Output file base name for HTML help builder.
+htmlhelp_basename = 'gte-doc'
+
+
+# -- Options for LaTeX output ------------------------------------------------
+
+latex_elements = {
+    # The paper size ('letterpaper' or 'a4paper').
+    #
+    # 'papersize': 'letterpaper',
+
+    # The font size ('10pt', '11pt' or '12pt').
+    #
+    # 'pointsize': '10pt',
+
+    # Additional stuff for the LaTeX preamble.
+    #
+    # 'preamble': '',
+
+    # Latex figure (float) alignment
+    #
+    # 'figure_align': 'htbp',
+}
+
+# Grouping the document tree into LaTeX files. List of tuples
+# (source start file, target name, title,
+#  author, documentclass [howto, manual, or own class]).
+latex_documents = [
+    (master_doc, 'gte-doc.tex', 'Grouper Training Environment Documentation',
+     'Carl Waldbieser', 'manual'),
+]
+
+
+# -- Options for manual page output ------------------------------------------
+
+# One entry per manual page. List of tuples
+# (source start file, name, description, authors, manual section).
+man_pages = [
+    (master_doc, 'gte-doc', 'Grouper Training Environment Documentation',
+     [author], 1)
+]
+
+
+# -- Options for Texinfo output ----------------------------------------------
+
+# Grouping the document tree into Texinfo files. List of tuples
+# (source start file, target name, title, author,
+#  dir menu entry, description, category)
+texinfo_documents = [
+    (master_doc, 'gte-doc', 'Grouper Training Environment Documentation',
+     author, 'gte-doc', 'One line description of project.',
+     'Miscellaneous'),
+]
+
+
+# -- Options for Epub output -------------------------------------------------
+
+# Bibliographic Dublin Core info.
+epub_title = project
+
+# The unique identifier of the text. This can be a ISBN number
+# or the project homepage.
+#
+# epub_identifier = ''
+
+# A unique identification for the text.
+#
+# epub_uid = ''
+
+# A list of files that should not be packed into the epub file.
+epub_exclude_files = ['search.html']
diff --git a/docs/index.rst b/docs/index.rst
new file mode 100644
index 0000000..bae9535
--- /dev/null
+++ b/docs/index.rst
@@ -0,0 +1,25 @@
+Grouper Training Environment Documentation
+==========================================
+
+.. toctree::
+   :maxdepth: 2
+
+   201/index
+   401/index
+
+Conventions
+===========
+
+In the documentation, many references will be made to Grouper groups and
+folders. These references will sometimes take the form of a complete path to
+the referenced object, such as `:app:vpn:service:ref:ad_hoc`. Other times, the
+references will be relative to the current context under discussion. For
+example, when referring to the same application-specific reference group for
+the VPN service, the notation `...:ref:ad_hoc` may be used to indicate that a
+portion of the full path has been omitted.
+
+Indices and tables
+==================
+
+* :ref:`genindex`
+* :ref:`search`
diff --git a/docs/make.bat b/docs/make.bat
new file mode 100644
index 0000000..27f573b
--- /dev/null
+++ b/docs/make.bat
@@ -0,0 +1,35 @@
+@ECHO OFF
+
+pushd %~dp0
+
+REM Command file for Sphinx documentation
+
+if "%SPHINXBUILD%" == "" (
+	set SPHINXBUILD=sphinx-build
+)
+set SOURCEDIR=.
+set BUILDDIR=_build
+
+if "%1" == "" goto help
+
+%SPHINXBUILD% >NUL 2>NUL
+if errorlevel 9009 (
+	echo.
+	echo.The 'sphinx-build' command was not found. Make sure you have Sphinx
+	echo.installed, then set the SPHINXBUILD environment variable to point
+	echo.to the full path of the 'sphinx-build' executable. Alternatively you
+	echo.may add the Sphinx directory to PATH.
+	echo.
+	echo.If you don't have Sphinx installed, grab it from
+	echo.http://sphinx-doc.org/
+	exit /b 1
+)
+
+%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+goto end
+
+:help
+%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
+
+:end
+popd