We recently moved from a Server environment to a Data Center environment for Jira. We kept our Server licenses for some proof of concept work. With Scriptrunner's new LDAP resource I was able to write a script to pull user profile pics from Active Directory and update their profiles in Jira and Confluence. I did this in our Data Center environment and works great. I'm currently running a proof of concept in our server environment with Jira Service Desk. So I thought I would put the script in there as well. However, when running the script in the server environment, the avatars are getting set to the silhouette with a question mark in it instead of the profile picture. When I go to /atlassian/application-data/jira/data/avatars directory, all the images are there and contain the user's profile picture from AD. Checking the users avatar ID after assigning it, points to the correct avatars. I can't find any differences between my Data Center and Server installs other than the fact that they are Data Center and Server. I even tested in my Development Data Center environment by installing a trial of service desk there. Anyone aware of any difference between the two or if there is a permission issue I'm missing? Oh, and if I set to one of the system avatars it works fine.
This is the avatar that is displayed after running the script:
Jira Version: 8.7.1
Script Runner Version: 6.1.0-p5
For reference the code for the script is here:
import com.onresolve.scriptrunner.ldap.LdapUtil
import org.springframework.ldap.core.AttributesMapper
import javax.naming.directory.SearchControls
import javax.naming.directory.Attributes
import com.atlassian.jira.avatar.Avatar
import com.atlassian.jira.avatar.AvatarManager
import com.atlassian.jira.avatar.AvatarService
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.icon.IconType
import com.atlassian.jira.icon.IconOwningObjectId
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.bc.JiraServiceContext
import com.atlassian.jira.bc.JiraServiceContextImpl
import com.atlassian.crowd.manager.directory.DirectoryManager
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.user.util.UserManager
import com.atlassian.jira.icon.IconType
import com.atlassian.jira.avatar.AvatarImpl
import com.atlassian.jira.avatar.Avatar.Size
def userManager = ComponentAccessor.getUserManager()
AvatarService avatarService = ComponentAccessor.getAvatarService()
userManager.getAllApplicationUsers().each { user ->
def avatarManager = ComponentAccessor.getAvatarManager()
def avatars = avatarManager.getCustomAvatarsForOwner(IconType.USER_ICON_TYPE, user.name)
avatars.each {avatar ->
if (!avatar.isSystemAvatar() && avatar != null)
{
avatarManager.delete(avatar.id)
}
}
if (user.active)
{
def noPhoto = false
def cnList
if (user.directoryId == 10300)
{
cnList = LdapUtil.withTemplate("GMCC") { template ->
template.search("", "(sAMAccountName=" + user.name + ")", SearchControls.SUBTREE_SCOPE, { attributes ->
if(attributes.get('thumbnailPhoto') != null)
{
noPhoto = true
attributes.get('thumbnailPhoto').get()
}
} as AttributesMapper<byte[]>)
}
}
else if (user.directoryId == 10400)
{
cnList = LdapUtil.withTemplate("IMICO") { template ->
template.search("", "(sAMAccountName=" + user.name + ")", SearchControls.SUBTREE_SCOPE, { attributes ->
if(attributes.get('thumbnailPhoto') != null)
{
noPhoto = true
attributes.get('thumbnailPhoto').get()
}
} as AttributesMapper<byte[]>)
}
}
if (noPhoto)
{
ByteArrayInputStream bis = new ByteArrayInputStream(cnList[0] as byte[])
BufferedImage bImage = ImageIO.read(bis)
ImageIO.write(bImage, "jpg", new File("/data/atlassian/application-data/jira/scripts/data/test.jpg"))
def fileName = "test.jpg"
File file = new File("/data/atlassian/application-data/jira/scripts/data/test.jpg")
def contentType = AvatarManager.PNG_CONTENT_TYPE
IconType iconType = IconType.USER_ICON_TYPE
IconOwningObjectId owner = new IconOwningObjectId(user.name)
BufferedInputStream iStream = new BufferedInputStream(new FileInputStream(file))
Avatar avatar = avatarManager.create(fileName, contentType, iconType, owner, iStream, null)
avatarService.setCustomUserAvatar(user, user, avatar.getId())
}
}
}
I figured this out. I'm not sure I understand why, but setting the IconOwningObjectId with the Jira Administrator account and the remote user on the setCustomUserAvatar call to the Jira Administrator it worked.
In this case all avatars will be stored under Administrator profile and user avatars will be 'linked' to them.
It can be a bit confusing for users.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
We have now set this up in exactly the same way with a scheduled job. Thanks for the article, it was very helpful :)
Here is an updated version of the script, the main differences are:
import org.springframework.ldap.core.LdapTemplate import com.onresolve.scriptrunner.ldap.LdapUtil import org.springframework.ldap.core.AttributesMapper import javax.naming.directory.SearchControls import com.atlassian.jira.avatar.AvatarManager import com.atlassian.jira.component.ComponentAccessor import com.atlassian.jira.icon.IconType import com.atlassian.jira.icon.IconOwningObjectId import com.google.common.io.ByteSource final USERDIRECTORY_ID = 00000 final AD_RESOURCE_NAME = "YourAdResouceName" def userManager = ComponentAccessor.getUserManager() def avatarService = ComponentAccessor.getAvatarService() def avatarManager = ComponentAccessor.getAvatarManager() userManager.getAllApplicationUsers().each { user -> // ignore inactive users if (!user.active) { return } // ignore other user directories if (user.directoryId != USERDIRECTORY_ID) { return } // get the profile picture from the AD def hasPhoto = false def cnList = LdapUtil.withTemplate(AD_RESOURCE_NAME) { template -> (template as LdapTemplate).setIgnorePartialResultException(true) template.search("", "(sAMAccountName=" + user.name + ")", SearchControls.SUBTREE_SCOPE, { attributes -> final thumbnailPhotoAttribue = 'thumbnailPhoto' if (attributes.get(thumbnailPhotoAttribue) != null) { hasPhoto = true attributes.get(thumbnailPhotoAttribue).get() } } as AttributesMapper <byte[]> ) } // return if no profile picture was found if (!hasPhoto) { return } // delete the old avatars of the user def avatars = avatarManager.getCustomAvatarsForOwner(IconType.USER_ICON_TYPE, user.key) avatars.each { avatar -> if (!avatar.isSystemAvatar() && avatar != null) { avatarManager.delete(avatar.id) } } // upload avatar to jira def owningObjectId = new IconOwningObjectId(user.key) def fileName = user.key + ".jpg" def avatar = avatarManager.create(fileName, AvatarManager.PNG_CONTENT_TYPE, IconType.USER_ICON_TYPE, owningObjectId, ByteSource.wrap(cnList[0] as byte[]).openStream(), null) avatarService.setCustomUserAvatar(user, user, avatar.getId()) }
For all those who want to set it up in the same way, here are a few more steps on how to set up the LDAP resource and the scheduled job. Even if it is almost self-explanatory:
Dont forget to replace the AD Resource Name und Userdirectory ID with your values ;)
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
The original code works for me with one small change:
IconOwningObjectId owner = new IconOwningObjectId(user.name.toLowerCase())
It looks like internally search algorithm for user avatar by user name is case-sensitive but Jira uses User Name in lower case to search. So avatar.owner field should be stored in lower case in database to work correctly.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.