Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

Include Page
spaceKeyGrouper
pageTitleNavigation

Panel
borderColor#ccc
bgColor#FcFEFF
titleColorwhite
titleBGColor#00a400

Image Removed  This topic is discussed in the "Grouper Connectors" training video.

Integrating Atlassian with Grouper

People have had success with this SAML connector: https://github.com/chauth/confluence_http_authenticator

...

To do a full reconcile on the new 2.2.1+ grouper atlassian conector, run this:

Code Block

java -cp conf:lib/* edu.internet2.middleware.grouperAtlassianConnector.db.GrouperAtlassianDataReconcile full notReadonly

...

To set this up, check it out and build it:

Code Block

[mchyzer@flash grouperAtlassian]$ svn co http://anonsvn.internet2.edu/svn/i2mi/branches/GROUPER_1_6_BRANCH/grouper-misc/grouperAtlassianConnector
[mchyzer@flash grouperAtlassian]$ cd grouperAtlassianConnector/
[mchyzer@flash grouperAtlassianConnector]$ ant

Take the grouper.client.example.properties in the grouperAtlassianConnector/conf/grouper.client.example.properties, and put the connector properties in your own grouper.client.properties:

Code Block

################################
## Atlassian connector settings
################################

# put a folder name that is the root for atlassian groups
atlassian.root =

# atlassian source to use (leave blank for all sources)
atlassian.subject.search.sourceId =

# atlassian search by id, identifier, or idOrIdentifer (idOrIdentifier is Grouper 2.0+)
atlassian.subject.search.subjectId = identifier

# number of minutes to cache reads
# defaults to 10.  Note, crank this up  to 25 hours if you are doing XMPP notifications
atlassian.cache.minutes = 10

# number of minutes to cache profile reads
# defaults to 10
atlassian.cache.profile.minutes = 20

# each cache has a failsafe cache, so that if grouper is down, and the data has been loaded,
# since atlassian has been started, the stale verison of the data can be retrieved
atlassian.cache.failsafe.hours = 48

# list all sources here, and how to get the atlassian id
atlassian.source.jdbc.sourceId = jdbc
# should be "id" or an attribute name to get the identifier for atlassian
atlassian.source.jdbc.idOrAttribute = loginid
# email attribute for this source (needed if using the ProfileProvider)
atlassian.source.jdbc.emailAttribute = EMAIL
# should be "name" or "description" or an attribute name to get the name for atlassian (needed if using the ProfileProvider)
atlassian.source.jdbc.nameAttribute = name

#atlassian name of group which has all users in it, e.g. jira-users
atlassian.usersGroup = jira-users

# grouper group name of all users that have ever been in atlassian (profile service has access to these).  Leave blank to
# just use the users group
atlassian.grouperAllUsersGroup =

# if you are doing XMPP for cache clearing, set to true, and set the XMPP sections of this config
atlassian.registerXmppListeners = false

# if incremental changes come through, then dont clear now, clear sometime in the future so that multiple changes
# cause fewer cache refreshes.  Note that changes come through the change log so that they are already buffered a little bit
# this should probably at least be 15 seconds...
atlassian.xmppIncrementalClearCacheSecondsInFuture = 75

# if all users must be in atlassian.grouperAllUsersGroup,
# or if lookups of old users can be done without having to be in this group
atlassian.requireGrouperAllUsersGroupForLookups = false

# groups which should be assigned to various privileges for new groups created in confluence
atlassian.updaters =
atlassian.admins =
atlassian.readers =

# pretend these memberships exist (e.g. to bootstrap or for users not in grouper)
atlassian.autoadd.administrators.groupname = jira-administrators
atlassian.autoadd.administrators.usernames = admin

atlassian.autoadd.users.groupname = jira-users
atlassian.autoadd.users.usernames = admin

# users not in idm, this is needed if using the profile provider
atlassian.autoadd.admin.user.id = admin
atlassian.autoadd.admin.user.name = Atlassian ADMIN
atlassian.autoadd.admin.user.email = you@yourschool.edu


#ignore calls on this user to the web service
atlassian.ws.users.to.ignore = admin

#put a valid subject id or identifier here for testing, and that user's email and name
atlassian.test.subjectIdOrIdentifier =
atlassian.test.email =
atlassian.test.name =

# if you are using the edu.internet2.middleware.grouperAtlassianConnector.GrouperLoggingAccessProviderWrapper
# to log an access provider, set the underlying class here
atlassian.logging.accessProvider.class = com.atlassian.jira.user.osuser.JiraOFBizAccessProvider

# if you are using the edu.internet2.middleware.grouperAtlassianConnector.GrouperLoggingAccessProviderWrapper
# to log an access provider, set the underlying class here
atlassian.logging.profileProvider.class = com.atlassian.jira.user.osuser.JiraOFBizProfileProvider

# if using the external authenticator, then this says if we should store the user token in session
# as opposed to getting it from the external authentication each time
atlassian.authentication.cacheUserToken = false

# if using the external authenticator, then this is the request attribute where the principal name is
# note, if it is REMOTE_USER, that will already be checked
atlassian.authentication.requestPrincipalAttributeName =

# if you are not in prod, and you want to backdoor as someone else, put a parameter name here (e.g. gibberish alphanumeric, or backdoorNetid)
# and if that param is in the URL or posted to the application, it will be used.  Note, if you use this then cacheUserToken will be true
# since this will not be in the URL for every request
atlassian.authentication.backdoorRequestParameterName =

# if you are not in prod, and you want to backdoor as someone else, put comma separated usernames here who
# are allowed to backdoor as someone else
atlassian.authentication.backdoorAllowedUsers =

Configure the WS url, and authentication in the grouper.client.properties, as you normally would, here is a kerberos (cleansed) example

Code Block

# 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://groups.school.edu/grouper-ws/servicesRest

 # kerberos principal used to connect to web service
 grouperClient.webService.login = atlassianGrouper/server.school.edu

 # password for shared secret authentication to web service
 # or you can put a filename with an encrypted password
grouperClient.webService.password = /home/user/pass/atlassianGrouper.pass

Take the grouperAtlassianConnector/dist/grouperAtlassianConnector.jar, the grouperClient.jar, and put them in the jira edit-webapp/WEB-INF/lib dir.  Take the grouper.client.properties and put it in the jira edit-webapps/WEB-INF/classes dir.  Take the osuser.xml file that was in the jira WEB-INF/classes dir.  Comment out the existing access and profile (optional) providers, and configure the grouper one(s)

Code Block

<!-- provider class="com.atlassian.jira.user.osuser.JiraOFBizCredentialsProvider">
    <property name="exclusive-access">true</property>
  </provider>

        <provider class="com.atlassian.jira.user.osuser.JiraOFBizProfileProvider">
    <property name="exclusive-access">true</property>
  </provider>

  <provider class="com.atlassian.jira.user.osuser.JiraOFBizAccessProvider">
    <property name="exclusive-access">true</property>
  </provider -->

        <provider class="edu.internet2.middleware.grouperAtlassianConnector.GrouperCredentialsProvider">
    <property name="exclusive-access">true</property>
  </provider>

        <provider class="edu.internet2.middleware.grouperAtlassianConnector.GrouperProfileProvider">
    <property name="exclusive-access">true</property>
  </provider>

  <provider class="edu.internet2.middleware.grouperAtlassianConnector.GrouperAccessProvider">
    <property name="exclusive-access">true</property>
  </provider>

For confluence, the file looks a little different, but same concept:

Code Block

<!--
    <provider class="bucket.user.providers.CachingCredentialsProvider">
        <property name="chain.classname">com.opensymphony.user.provider.hibernate.HibernateCredentialsProvider</property>
        <property name="chain.configuration.provider.class">bucket.user.BucketHibernateConfigProvider</property>
    </provider>

    <provider class="bucket.user.providers.CachingAccessProvider">
        <property name="chain.classname">com.opensymphony.user.provider.hibernate.HibernateAccessProvider</property>
        <property name="chain.configuration.provider.class">bucket.user.BucketHibernateConfigProvider</property>
    </provider>

    <provider class="bucket.user.providers.CachingProfileProvider">
        <property name="chain.classname">com.opensymphony.user.provider.hibernate.HibernateProfileProvider</property>
        <property name="chain.configuration.provider.class">bucket.user.BucketHibernateConfigProvider</property>
    </provider>
-->

        <provider class="edu.internet2.middleware.grouperAtlassianConnector.GrouperCredentialsProvider">
                <property name="exclusive-access">true</property>
        </provider>

        <provider class="edu.internet2.middleware.grouperAtlassianConnector.GrouperProfileProvider">
                <property name="exclusive-access">true</property>
        </provider>

        <provider class="edu.internet2.middleware.grouperAtlassianConnector.GrouperAccessProvider">
                <property name="exclusive-access">true</property>
        </provider>

Note, for confluence, you also need to edit the atlassian-user.xml file:

Code Block

<osuser key="osuserRepository" name="OSUser Repository"/>

        <!-- Default confluence user repository -->
        <hibernate name="Hibernate Repository" key="hibernateRepository"  description="Hibernate Repository" cache="true"/>

...

If you use shib or cosign or some web server plugin for authentication, there are other ways to integrate with confluence, but this jar has a way too. Just set this in the seraph-config.xml

Code Block

<!-- authenticator class="com.atlassian.confluence.user.ConfluenceAuthenticator"/ -->
    <authenticator class="edu.internet2.middleware.grouperAtlassianConnector.externalAuthentication.ExternalConfluenceAuthenticator"/>

Note that you can enable the backdoor with this in the grouper.client.properties to allow certain people to be able to login as other users:

Code Block

# if you are not in prod, and you want to backdoor as someone else, put a parameter name here (e.g. gibberish alphanumeric, or backdoorNetid)
 # and if that param is in the URL or posted to the application, it will be used.  Note, if you use this then cacheUserToken will be true
 # since this will not be in the URL for every request
 atlassian.authentication.backdoorRequestParameterName = backdoorNetId

 # if you are not in prod, and you want to backdoor as someone else, put comma separated usernames here who
 # are allowed to backdoor as someone else
 atlassian.authentication.backdoorAllowedUsers = jsmith, whoever

...

We are creating a script with a line for each group like this:

Code Block

addGroup(parent stem name, extension, displayExtension)

Run a query like this against the atlassian schema:

Code Block

SELECT CONCAT(CONCAT(CONCAT(CONCAT('addGroup("test:school:apps:atlassian:groups", "', groupname), '", "'), groupname), '");') AS gsh_script FROM groupbase;

Run the resulting GSH script against grouper.

Code Block

grouperSession = GrouperSession.startRootSession();
 grantPriv("test:school:apps:atlassian:groups:some-group", "test:school:apps:atlassian:admin:admins", AccessPrivilege.ADMIN); grantPriv("test:school:apps:atlassian:groups:another-group", "test:school:apps:atlassian:admin:admins", AccessPrivilege.ADMIN);

If you want to set privileges in grouper, then do a SQL script like this:

Code Block

SELECT CONCAT(CONCAT('grantPriv("test:school:apps:atlassian:groups:', groupname), '", "test:school:apps:atlassian:admin:admins", AccessPrivilege.ADMIN);') AS gsh_script FROM groupbase SELECT CONCAT(CONCAT('grantPriv("test:school:apps:atlassian:groups:', groupname), '", "test:school:apps:atlassian:admin:readers", AccessPrivilege.READ);') AS gsh_script FROM groupbase SELECT CONCAT(CONCAT('grantPriv("test:school:apps:atlassian:groups:', groupname), '", "test:school:apps:atlassian:admin:updaters", AccessPrivilege.UPDATE);') AS gsh_script FROM groupbase

Then do the memberships:

Code Block

addMember(group name, subject id)

Run thie sql script

Code Block

SELECT CONCAT(CONCAT(CONCAT(CONCAT('addMember("test:school:apps:atlassian:groups:', group_name), '", "'), user_name), '");') AS gsh_script FROM membershipbase WHERE user_name <> 'admin' AND user_name NOT LIKE '%.%' AND user_name NOT LIKE '%@%' AND user_name NOT LIKE '%\_%' ;

...

Generate a GSH script for groups

Code Block

SELECT CONCAT(CONCAT(CONCAT(CONCAT('addGroup("test:school:apps:atlassian:groupsConfluence", "', groupname), '", "'), groupname), '");') AS gsh_script FROM groups;

This script looks like this, add a grouper session and run with GSH (gsh.sh file.script):

Code Block

subject = findSubject("atlassianGrouper/school.edu");
 grouperSession = GrouperSession.start(subject);
 addGroup("test:school:apps:atlassian:groupsConfluence", "admin_systems_financials_users", "admin_systems_financials_users");
 addGroup("test:school:apps:atlassian:groupsConfluence", "admissions_admin", "admissions_admin");
 addGroup("test:school:apps:atlassian:groupsConfluence", "ait", "ait");
 addGroup("test:school:apps:atlassian:groupsConfluence", "ait_directors", "ait_directors"); ...

Generate a GSH script for privileges

Code Block

SELECT CONCAT(CONCAT('grantPriv("test:school:apps:atlassian:groupsConfluence:', groupname), '", "test:school:apps:atlassian:admin:admins", AccessPrivilege.ADMIN);') AS gsh_script FROM groups;

Run the script (gsh.sh file.script):

Code Block

subject = findSubject("atlassianGrouper/school.edu");
 grouperSession = GrouperSession.start(subject);
 grantPriv("test:school:apps:atlassian:groupsConfluence:admin_systems_financials_users", "test:school:apps:atlassian:admin:admins", AccessPrivilege.ADMIN);
 grantPriv("test:school:apps:atlassian:groupsConfluence:admissions_admin", "test:school:apps:atlassian:admin:admins", AccessPrivilege.ADMIN);
 grantPriv("test:school:apps:atlassian:groupsConfluence:ait", "test:school:apps:atlassian:admin:admins", AccessPrivilege.ADMIN); ...

Generate script:

Code Block

SELECT CONCAT(CONCAT('grantPriv("test:school:apps:atlassian:groupsConfluence:', groupname), '", "test:school:apps:atlassian:admin:readers", AccessPrivilege.READ);') AS gsh_script FROM groups;

Run the GSH

Code Block

subject = findSubject("atlassianGrouper/school.edu");
 grouperSession = GrouperSession.start(subject);
 grantPriv("test:school:apps:atlassian:groupsConfluence:admin_systems_financials_users", "test:school:apps:atlassian:admin:readers", AccessPrivilege.READ);
 grantPriv("test:school:apps:atlassian:groupsConfluence:admissions_admin", "test:school:apps:atlassian:admin:readers", AccessPrivilege.READ); ...

Generate script:

Code Block

SELECT CONCAT(CONCAT('grantPriv("test:school:apps:atlassian:groupsConfluence:', groupname), '", "test:school:apps:atlassian:admin:readers", AccessPrivilege.READ);') AS gsh_script FROM groups;

Run the GSH:

Code Block

subject = findSubject("atlassianGrouper/school.edu");
 grouperSession = GrouperSession.start(subject);
 grantPriv("test:school:apps:atlassian:groupsConfluence:admin_systems_financials_users", "test:school:apps:atlassian:admin:updaters", AccessPrivilege.UPDATE);
 grantPriv("test:school:apps:atlassian:groupsConfluence:admissions_admin", "test:school:apps:atlassian:admin:updaters", AccessPrivilege.UPDATE);

Generate membership script:

Code Block

SELECT CONCAT(CONCAT(CONCAT(CONCAT('addMember("test:school:apps:atlassian:groupsConfluence:', the_group.groupname), '", "'), the_user.name), '");') AS gsh_script FROM groups AS the_group, users AS the_user, local_members AS the_membership WHERE the_group.id = the_membership.groupid AND the_user.id = the_membership.userid AND the_user.name NOT LIKE '% %' AND the_user.name <> 'admin' AND the_user.name NOT LIKE '%.%' AND the_user.name NOT LIKE '%@%' AND the_user.name NOT LIKE '%\_%' ;

Run the GSH script

Code Block

subject = findSubject("atlassianPenngroups/medley.isc-seo.upenn.edu");
grouperSession = GrouperSession.start(subject);
addMember("test:school:ait:apps:atlassian:groupsConfluence:admin_systems_financials_users", "jsmith");
addMember("test:school:ait:apps:atlassian:groupsConfluence:admin_systems_financials_users", "asmith");
addMember("test:school:ait:apps:atlassian:groupsConfluence:admin_systems_financials_users", "bsmith");
addMember("test:school:ait:apps:atlassian:groupsConfluence:admin_systems_financials_users", "csmith");
addMember("test:school:ait:apps:atlassian:groupsConfluence:some_other_confluence_group", "asmith");

...

The connector has excellent logging, each method will log in DEBUG mode, including if it was cached, and how long the method call took.  Change this in the log4j.properties of jira/confluence

Code Block

#log4j.logger.edu.internet2.middleware.grouperAtlassianConnector.GrouperAccessProvider = DEBUG
#log4j.logger.edu.internet2.middleware.grouperAtlassianConnector.GrouperLoggingProfileProviderWrapper = DEBUG
log4j.logger.edu.internet2.middleware.grouperAtlassianConnector.GrouperProfileProvider = DEBUG
#log4j.logger.edu.internet2.middleware.grouperAtlassianConnector.GrouperCredentialsProvider = DEBUG
#log4j.logger.edu.internet2.middleware.grouperAtlassianConnector.externalAuthentication = DEBUG
#log4j.logger.edu.internet2.middleware.grouperAtlassianConnector.xmpp = DEBUG

#log4j.logger.org.jivesoftware.smack = DEBUG

The logs look like this:

Code Block

2010-12-18 15:53:22,516 TP-Processor8 DEBUG admin 953x51x1 x4b0lo 1.2.3.4 /plugins/servlet/streams [internet2.middleware.grouperAtlassianConnector.GrouperProfileProvider] operation: getPropertySet, username: myuser, retrievedFromPropertySetCache: true, fullName: Chris Hyzer ADMIN, email: mchyzer@school.edu, timeMillis: 0
2010-12-18 15:53:22,516 TP-Processor8 DEBUG admin 953x51x1 x4b0lo 1.2.3.4 /plugins/servlet/streams [internet2.middleware.grouperAtlassianConnector.GrouperProfileProvider] operation: list, retrievedFromUserCache: true, resultList: Size 262: asmith, bsmith, csmith, dsmith..., timeMillis: 0
2010-12-18 15:53:22,516 TP-Processor8 DEBUG admin 953x51x1 x4b0lo 1.2.3.4 /plugins/servlet/streams [internet2.middleware.grouperAtlassianConnector.GrouperProfileProvider] operation: handles, username: myuser, retrievedFromPropertySetCache: true, fullName: Chris Hyzer ADMIN, email: mchyzer@school.edu, timeMillis: 0

...

Atlassian calls methods frequently, so the connector does a lot of caching.  The default is to cache for 10 minutes.  If the write action is performed in Atlassian admin console, the caches are cleared.  Otherwise it could take 10 minutes for Grouper actions to propagate to Atlassian (or however you configure in grouper.client.properties). Note that if you have lots of groups, and lots of members, the cache refreshes can take some time (5-20 seconds?) For this reason you could have a long cache timeout, and use XMPP notifications for real time updates.

Code Block

# number of minutes to cache reads
 # defaults to 10.  Note, crank this up  to 25 hours if you are doing XMPP notifications
 atlassian.cache.minutes = 10

 # number of minutes to cache profile reads
 # defaults to 10
 atlassian.cache.profile.minutes = 20

 # each cache has a failsafe cache, so that if grouper is down, and the data has been loaded,
 # since atlassian has been started, the stale verison of the data can be retrieved
 atlassian.cache.failsafe.hours = 48

...

If you want to set the cache timeout to something long (1 day?), then you can enable XMPP notifications from the grouper-loader server to the jira or confluence system. Note, when a message comes from Grouper, the grouper.client.properties specifies a number of seconds to buffer the request. The cache clear will take place normally in the background so users will not notice. Also, there is a croned full refresh that happens in the background so users do not notice a delay. Here is an example of the grouper.client.properties config (note: you need grouper client 1.6.4+, if it is not released, you can build it from SVN or ask the Grouper team for a build of it):

Code Block

################################
## XMPP client settings
## Note: you need the smack.jar in your classpath, see the grouper xmpp wiki for usage
## https://spaces.at.internet2.edu/display/GrouperWG/Grouper+XMPP+notifications+v1.6.0
################################


## general xmpp configuration
grouperClient.xmpp.server.host = jabber.school.edu
grouperClient.xmpp.server.port = 5222
grouperClient.xmpp.user = atlassianjabber
# note, pass can be in an external file with morphstring
grouperClient.xmpp.pass = /home/dir/pass/grouper/grouperjabberClient.pass
grouperClient.xmpp.resource = jiraProd
# note, you need the exact id and resource here or it wont match
grouperClient.xmpp.trustedMessagesFromJabberIds = grouperjabber@school.edu/grouperServer

# if true, then each quartz trigger name will be unique
# do this for atlassian since it doesnt do quartz right, and wont delete or reuse old triggers
grouperClient.xmpp.uniqueQuartzTriggerNames = true

# just need a group here, any group, preferably not too large
grouperClient.xmpp.job.atlassian.groupNames = school:atlassian:groupsJira:jira-administrators
grouperClient.xmpp.job.atlassian.elfilter = (event.eventType eq 'MEMBERSHIP_DELETE' || event.eventType eq 'MEMBERSHIP_ADD') && event.membershipType eq 'flattened' && event.groupName.startsWith('school:apps:atlassian:groupsJira')
grouperClient.xmpp.job.atlassian.handlerClass = edu.internet2.middleware.grouperAtlassianConnector.xmpp.GrouperAtlassianXmppHandler
# set this to reload_group or incremental if not reload on each event
grouperClient.xmpp.job.atlassian.eventAction = incremental
# how often a full refresh should occur regardless of events
grouperClient.xmpp.job.atlassian.fullRefreshQuartzCronString =
grouperClient.xmpp.job.atlassian.subjectAttributeNames =
# subjects wont notify in not in these sources, comma separated, or blank for all
grouperClient.xmpp.job.atlassian.requireSources =
# subjects wont notify if they dont have a non blank value for these attributes, or blank for all
grouperClient.xmpp.job.atlassian.requireAttributes =

Here is an example of the grouper-loader.properties that will send the XMPP:

Code Block

###################################
## XMPP notifications
## (note, uncomment the consumer class and cron above)
## this will get grouper ws getMembers rest lite xmp:
## http://anonsvn.internet2.edu/cgi-bin/viewvc.cgi/i2mi/trunk/grouper-ws/grouper-ws/doc/samples/getMembers/WsSampleGetMembersRestLite_xml.txt?view=log
###################################

## general xmpp configuration
xmpp.server.host = jabber.school.edu
xmpp.server.port = 5222
xmpp.user = grouperjabber
# note, pass can be in an external file with morphstring
xmpp.pass = /home/dir/pass/grouper/grouperjabber.pass
xmpp.resource = grouperServer


changeLog.consumer.atlassianProd.class = edu.internet2.middleware.grouper.changeLog.esb.consumer.EsbConsumer
changeLog.consumer.atlassianProd.quartzCron = 0 * * * * ?
#match all events that are add or remove membership, only flattened, if the subject is from the jdbc source, with the loginid attribute and in the folder test:xmppGroups
changeLog.consumer.atlassianProd.elfilter = (event.eventType eq 'MEMBERSHIP_DELETE' || event.eventType eq 'MEMBERSHIP_ADD') && event.membershipType == 'flattened' && event.groupName =~ '^school\\:apps\\:atlassian\\:.*$'
changeLog.consumer.atlassianProd.publisher.class = edu.internet2.middleware.grouper.changeLog.esb.consumer.EsbXmppPublisher
#add in the recipients
changeLog.consumer.atlassianProd.publisher.recipient = atlassianjabber@school.edu/jiraProd, atlassianjabber@schools.edu/confluenceProd
#changeLog.consumer.atlassianProd.publisher.addSubjectAttributes =

...