If two grouper management systems need to share a group, Grouper will have a way to do this. There should be push, pull, and incremental. In the future we can add permissions. This is a design document for a potential enhancement to Grouper.
We would like this provisioning to occur with SPML similar to ldappcng, speak the common Groups API (up and coming). Therefore anything that speaks the (standard) API could be an endpoint. Perhaps the below design of web services and xmpp could use spml as the data format.
First there are connections to other groupers based on the grouper client. These connections are stored in the grouper.properties. The sources in the remote and local can be matched up.
Second, there are links of group to group based on these connections.
The subjects are synced based on external subjects generally. A key point is there if the external subject does not exist in the destination, it can be added dynamically.
A config might look like this in the grouper-loader.properties to define the connection to the school (similar to how we define various DB connections for the loader):
###################################### ## Grouper client connections ## if this grouper needs to talk to another grouper, this is the client connection information ###################################### # id of the source, should match the part in the property name grouperClient.someOtherSchool.id = someOtherSchool # url of web service, should include everything up to the first resource to access # e.g. https://groups.school.edu/grouperWs/servicesRest grouperClient.someOtherSchool.properties.grouperClient.webService.url = https://some.other.school.edu/grouperWs/servicesRest # login ID grouperClient.someOtherSchool.properties.grouperClient.webService.login = someRemoteLogin # password for shared secret authentication to web service # or you can put a filename with an encrypted password grouperClient.someOtherSchool.properties.grouperClient.webService.password = ******* # this is the subject to act as local, if blank, act as GrouperSystem, specify with SubjectFinder packed string, e.g. # subjectIdOrIdentifier or sourceId::::subjectId or ::::subjectId or sourceId::::::subjectIdentifier or ::::::subjectIdentifier # sourceId::::::::subjectIdOrIdentifier or ::::::::subjectIdOrIdentifier grouperClient.someOtherSchool.localActAsSubject = # the id of this source, generally the same as the name in the property name. This is mandatory grouperClient.someOtherSchool.source.jdbc.id = jdbc # the part between "grouperClient.someOtherSchool.source." and ".id" links up the configs, # in this case, "jdbc", make sure it has no special chars. sourceId can be blank if you dont want to specify grouperClient.someOtherSchool.source.jdbc.local.sourceId = jdbc # this is the identifier that goes between them, it is "id" or an attribute name. subjects without this attribute will not be processed grouperClient.someOtherSchool.source.jdbc.local.read.subjectId = identifier # this is the identifier to lookup to add a subject, should be "id" or "identifier" grouperClient.someOtherSchool.source.jdbc.local.write.subjectId = identifier # sourceId of the remote system, can be blank grouperClient.someOtherSchool.source.jdbc.remote.sourceId = jdbc # this is the identifier that goes between them, it is "id" or an attribute name. subjects without this attribute will not be processed grouperClient.someOtherSchool.source.jdbc.remote.read.subjectId = # this is the identifier to lookup to add a subject, should be "id" or "identifier" grouperClient.someOtherSchool.source.jdbc.remote.write.subjectId = # if subjects are external and should be created if not exist grouperClient.someOtherSchool.source.jdbc.addExternalSubjectIfNotFound = true ###################################### ## Sync to/from another grouper ###################################### # we need to know where our # connection name in grouper client connections above syncAnotherGrouper.testGroup0.connectionName = someOtherSchool # incremental or push or pull or incremental,push syncAnotherGrouper.testGroup0.syncType = incremental,push # quartz cron to schedule the pull or push (incremental is automatic as events happen) (e.g. 5am daily) syncAnotherGrouper.testGroup0.cron = 0 0 5 * * ? # local group which is being synced syncAnotherGrouper.testGroup0.local.groupName = test:testGroup # remote group at another grouper which is being synced syncAnotherGrouper.testGroup0.remote.groupName = test2:testGroup2
This is using the grouper client, so the authentication is pluggable.
We should look at real time proxying of the getMembers() call to the remote site.
Proof of concept setup
Add a group to grouper1
gsh 0% grouperSession = GrouperSession.startRootSession(); gsh 1% new GroupSave(grouperSession).assignName("aStem:anotherGroup").assignCreateParentStemsIfNotExist(true).save();
Add group to grouper 2
gsh 0% grouperSession = GrouperSession.startRootSession(); gsh 1% new GroupSave(grouperSession).assignName("aStem2:anotherGroup2").assignCreateParentStemsIfNotExist(true).save();
Test grouper1 with grouper client
C:\temp\grouperClient_v2_0>java -jar grouperClient.jar --operation=addMemberWs --groupName=aStem:anotherGroup --subjectIds=GrouperSystem C:\temp\grouperClient_v2_0>java -jar grouperClient.jar --operation=getMembersWs --groupNames=aStem:anotherGroup
Test grouper2 with grouper client
C:\temp\grouperClient_v2_0>java -jar grouperClient.jar --operation=groupSaveWs --name=aStem2:anotherGroup2 --createParentStemsIfNotExist=true C:\temp\grouperClient_v2_0>java -jar grouperClient.jar --operation=addMemberWs --groupName=aStem2:anotherGroup2 --subjectIds=GrouperSystem C:\temp\grouperClient_v2_0>java -jar grouperClient.jar --operation=getMembersWs --groupNames=aStem2:anotherGroup2
Save this file into subjects.sql and run it to load test subjects
Create a connection from grouper1 to grouper2 in the grouper.properties
###################################### ## Grouper client connections ## if this grouper needs to talk to another grouper, this is the client connection information ###################################### # id of the source, should match the part in the property name grouperClient.localhostGrouper2.id = localhostGrouper2 # url of web service, should include everything up to the first resource to access # e.g. https://groups.school.edu/grouperWs/servicesRest grouperClient.localhostGrouper2.properties.grouperClient.webService.url = http://localhost:8091/grouperWs2/servicesRest # login ID grouperClient.localhostGrouper2.properties.grouperClient.webService.login = GrouperSystem # password for shared secret authentication to web service # or you can put a filename with an encrypted password grouperClient.localhostGrouper2.properties.grouperClient.webService.password = ********** # client version should match or be related to the server on the other end... grouperClient.localhostGrouper2.properties.grouperClient.webService.client.version = v2_0_000 # the id of this source, generally the same as the name in the property name. This is mandatory grouperClient.localhostGrouper2.source.jdbc.id = jdbc # the part between "grouperClient.someOtherSchool.source." and ".id" links up the configs, # in this case, "jdbc", make sure it has no special chars. sourceId can be blank if you dont want to specify grouperClient.localhostGrouper2.source.jdbc.local.sourceId = jdbc # this is the identifier that goes between them, it is "id" or an attribute name. subjects without this attribute will not be processed grouperClient.localhostGrouper2.source.jdbc.local.read.subjectId = id # this is the identifier to lookup to add a subject, should be "id" or "identifier" or "idOrIdentifier" grouperClient.localhostGrouper2.source.jdbc.remote.write.subjectId = idOrIdentifier # if subjects are external and should be created if not exist #grouperClient.someOtherSchool.source.jdbc.addExternalSubjectIfNotFound = true ###################################### ## Sync to/from another grouper ###################################### # we need to know where our # connection name in grouper client connections above syncAnotherGrouper.anotherGroup.connectionName = localhostGrouper2 # incremental or push or pull or incremental_push. Note, incremental push is cron'ed and incremental (to make sure no discrepancies arise) syncAnotherGrouper.anotherGroup.syncType = incremental_push # quartz cron to schedule the pull or push (incremental is automatic as events happen) (e.g. 5am daily) #syncAnotherGrouper.testGroup0.cron = 0 0 5 * * ? # local group which is being synced syncAnotherGrouper.anotherGroup.local.groupName = aStem:anotherGroup # remote group at another grouper which is being synced syncAnotherGrouper.anotherGroup.remote.groupName = aStem2:anotherGroup2
Make sure the new default change log consumer is in the grouper-loader.properties
changeLog.consumer.syncGroups.class = edu.internet2.middleware.grouper.client.GroupSyncConsumer changeLog.consumer.syncGroups.quartzCron =
Add a member to grouper1 in the group configured:
gsh 12% addMember("aStem:anotherGroup", "bawi"); true
See the web service call go to grouper2:
<WsRestAddMemberRequest> <wsGroupLookup> <groupName>aStem2:anotherGroup2</groupName> </wsGroupLookup> <subjectLookups> <WsSubjectLookup> <subjectId>bawi</subjectId> <subjectIdentifier>bawi</subjectIdentifier> </WsSubjectLookup> </subjectLookups> </WsRestAddMemberRequest>
Check grouper2 for the member in the remote group name
gsh 2% hasMember("aStem2:anotherGroup2", "bawi") true
Delete it from the original
gsh 13% delMember("aStem:anotherGroup", "bawi");
See the XML
<WsRestDeleteMemberRequest> <wsGroupLookup> <groupName>aStem2:anotherGroup2</groupName> </wsGroupLookup> <subjectLookups> <WsSubjectLookup> <subjectId>bawi</subjectId> <subjectIdentifier>bawi</subjectIdentifier> </WsSubjectLookup> </subjectLookups> </WsRestDeleteMemberRequest>
Check grouper2 for the member in the remote group name
gsh 3% hasMember("aStem2:anotherGroup2", "bawi") false
sdf