Valid for Grouper v2.6.18+
This is the lite version of this implementation
This is an example of how an institution would make a provisioner that is not included in the Grouper Provisioning Framework. If you have something that is generic that others can leverage, maybe we should add it to the Grouper product. If your needs are specific to your institution, this is what you need to do.
This example is based on requirements posted to the slack channel from University of Minnesota.
All the code is included in Grouper, so you can see the source code. Though this provisioner is not enabled, it is intended as an example.
Table of contents
WS spec
The first step is to identify and document the WS spec. Proof of concepts of calling the WS could be done.
In this case there is one operation, a REPLACE of members for a group
It is assumed that authentication is basic auth.
HTTP method
PUT /path/endpoint/<SOURCE>/<ROLE>
HTTP body
<?xml version="1.0"?> <ExternalRoleRequest> <Users> <netID>USER1234</netID> <netID>USER5678</netID> <netID>USER9012</netID> <netID>USER3456</netID> </Users> </ExternalRoleRequest>
Design the target representation of objects
Plan out which operations of the WS will be used for Grouper and how.
There is one operation, so we will use that
The <SOURCE> will be configured for the provisioner instance. If you want to provision to multiple sources, make another provisioner. This is an assumption and could be metadata or based on a parent folder or whatever.
The <ROLE> will be the sole attribute of the target representation of the group. We will translate this from the extension of the group. Again this is an assumption. Figure out how you want to translate based on your requirements.
Target group representation | ||
Attribute | Translation | Notes |
---|---|---|
role | group extension | The provisioner will put this attribute in the role spot in the URL |
The provisioning type will be membershipObjects. We could have probably used groupAttributes, but this is what we did. The membershipObject will have two attributes.
Target membership representation | ||
Attribute | Translation | Notes |
---|---|---|
role | group extension | The provisioner needs this to differentiate memberships from one group to another. Needs a tuple |
netID | entity subjectIdentifier | The main subject identifier defaults to the subject source subjectIdentifier0. Will make the XML based on these |
We are not selecting or changing entities and there is a straight translation from grouper provisioning entities (subjectIdentifier0) so we don't need to define a target representation of entities.
Custom external system
Note, this is optional. You dont need an external system to make your provisioner. It is nice to have one to see it in the UI, test it, re-use it, etc. If you dont create an external system, just reference whatever properties you need from a grouper config file, and set those in the config file or configuration screen in the UI.
In this case we could probably re-use a built in Grouper basic auth WS external system. However, to show how to make your own, we will just implement one anyways
- Implement the external system - note, the test method in this case uses the DAO implementation below... this is very circular. You could implement something simple here instead if you like...
Register this external system in grouper.properties (config id doesnt matter)
grouperExtraExternalSystem.exampleWsExternalSystem.class = edu.internet2.middleware.grouper.app.provisioningExamples.exampleWsReplaceProvisioner.ExampleWsExternalSystem
- Register a file which has the spec for the external system wizard
- The path must be the same path as your External System implementation (identify some java package that is your own). So you might start with edu/upenn/penngroups/myProvisioner if you were at penn...
The filename is: grouper.extraMetadata.externalSystem.<externalSystemConfigIdAbove>.properties
edu/internet2/middleware/grouper/app/provisioningExamples/exampleWsReplaceProvisioner/grouper.extraMetadata.externalSystem.exampleWsExternalSystem.properties
- Then in that file, identify which properties you need. In this case we will implement testing of the external system which will just all a method on it. In this case we will just replace a group memberships. In yours you might have a less heavy method to call.
Look in the base properties files of Grouper for examples of the metadata (the JSON commented out above each property). Note this file has all commented out properties, not actual properties
You can externalize that text in grouper.text.en.us.properties
- Now you can see the external system in the UI and configure one
- Test the external system (note, this relies on the Mock and provisioner implementation below)
Mock service
In Grouper, when we maintain mock services for all our WS based provisioners. We do this so we can easily and quickly unit test locally without needing accounts or environments. This is optional. If you want to do that, it is the first step.
Note you need to setup your development environment to run tomcat and the grouper webapp or deploy this to one of your environments at your institution.
Implement the mock service class
The other steps in the source file:
- Implement the server side of basic auth
- Check to see if table there, and if not, create it
- Operation for replace memberships which parses XML and inserts/deletes in table
- Linking the path and method to that operation
To link this mock server to the Grouper mock system and run it:
Register the mock server in grouper.properties (myExampleWsMock is the config id, any config id is fine)
grouperExtraMockServer.myExampleWsMock.class = edu.internet2.middleware.grouper.app.provisioningExamples.exampleWsReplaceProvisioner.ExampleWsMockServiceHandler grouperExtraMockServer.myExampleWsMock.path = exampleWs
Enable mocks in your env in grouper.hibernate.properties (UI or WS, we generally use UI but should work in WS too)
grouper.is.mockServices = true
Tell the mock server which external system (credentials) it is in grouper.properties (see code for this mock service)
grouperTest.exampleWs.mockExternalSystem.configId = myExampleExternalSystem1
Now you can hit this server just like the spec!
- We can see this mock data in the database
Provisioner wizard
This enables you to have a custom wizard for your provisioner. This is optional
- The ProvisioningConfiguration class identifies the configs for this provisioner. It also specifies the startWiths (optional) explained below
Register the provisioner in the grouper.properties
grouperExtraProvisionerConfiguration.exampleWsProvisionerConfig.class = edu.internet2.middleware.grouper.app.provisioningExamples.exampleWsReplaceProvisioner.ExampleWsProvisionerConfiguration
These are the properties and json metadata needed for this provisioner. Pick keys that differentiate this provisioner from others (see myExampleWs in there?). The file must be in the same Java package as the provisioner, and must be named like this: grouper-loader.extraMetadata.provisioner.exampleWsProvisionerConfig.properties. where exampleWsProvisionerConfig is the config ID of the provisioner registered above in the grouper.properties. Note the drop down for attribute names for group and membership are just copied from the base config, the configID is changed, and the drop down is added with the valid attribute names
# provisioner class # {valueType: "class", required: true, readOnly: true} # provisioner.myExampleWsProvisioner.class = edu.internet2.middleware.grouper.app.provisioningExamples.exampleWsReplaceProvisioner.GrouperExampleWsProvisioner # Example WS external system endpoint # {valueType: "string", required: true, order: 19, formElement: "dropdown", optionValuesFromClass: "edu.internet2.middleware.grouper.app.provisioningExamples.exampleWsReplaceProvisioner.ExampleWsExternalSystem"} # provisioner.myExampleWsProvisioner.exampleWsExternalSystemConfigId = # This is the 'source' part of URL to the service: PUT /path/endpoint/<SOURCE>/<ROLE> # {valueType: "string", required: true, order: 20} # provisioner.myExampleWsProvisioner.exampleWsSource = # Name of the attribute # {valueType: "string", order: 21000, required: true, showEl: "${operateOnGrouperGroups && numberOfGroupAttributes > $i$}", formElement: "dropdown", optionValues: ["role"], repeatGroup: "targetGroupAttribute", repeatCount: 20} # provisioner.myExampleWsProvisioner.targetGroupAttribute.$i$.name = # Name of the attribute # {valueType: "string", order: 5710, required: true, showEl: "${operateOnGrouperMemberships && numberOfMembershipAttributes > $i$}", formElement: "dropdown", optionValues: ["role", "netID"], repeatGroup: "targetMembershipAttribute", repeatCount: 20} # provisioner.myExampleWsProvisioner.targetMembershipAttribute.$i$.name =
You can externalize that text in grouper.text.en.us.properties . Note the ExampleWsProvisionerConfiguration is the classname defined above
config.ExampleWsProvisionerConfiguration.title = Example replace WS config.ExampleWsProvisionerConfiguration.attribute.exampleWsSource.label = Example WS source config.ExampleWsProvisionerConfiguration.attribute.exampleWsSource.description = Source part of the URL for this service config.ExampleWsProvisionerConfiguration.attribute.exampleWsExternalSystemConfigId.label = WS external system config.ExampleWsProvisionerConfiguration.attribute.exampleWsExternalSystemConfigId.description = External system that this provisioner points to
- Now you will see the provisioner in the wizard screen with any extra configs
Configure the provisioner
This provisioner needs to:
- Not really do much with entities
- Not insert/update/delete groups
- Just replace memberships
- Map the role and netID
Unit test the provisioner
Based on the exported config, and the mock server (or real service if there is a test env), we can create a unit test
Make a "start with"
Based on the exported config, consider what needs to be asked in the start with.
Configure the wizard properties in the template grouper-loader file for this provisioner (same file that provisioner wizard properties are in): grouper-loader.extraMetadata.provisioner.exampleWsProvisionerConfig.properties
Register the "start with" in the provisioner configuration
- Implement the logic of the "start with"
Add externalized text to grouper.text.en.us.properties
See the "start with" in action
Implement the configuration subclass for provisioner properties
This is a javabean for the provisioner configuration. This is used in your DAO code
Implement the DAO
The DAO is the operations that communicate with the target and convert the native data (in this case JSON) with the provisioning object model in the target representation
Implement the provisioner class which identifies the other classes
This is the main provisioner class. It identifies which parts of the provisioner are customized
See the provisioner in action
In this case the provisioner calls a web service that uses a database to store the data. When the provisioner runs, this is what the data looks like...