Create
cancel
Showing results for 
Search instead for 
Did you mean: 
Sign up Log in

Built-in script "Rename a user id" missing

Jim Winsor January 8, 2015

We are planning an upgrade of JIRA from 6.0.7 to 6.3.7, and in our testing have discovered that the built-in script to Rename a User ID is missing.  

Our version of Scriptrunner is 3.0.7.

We rely heavily on this script to avoid orphaned objects in JIRA when our users leave the company.  Any assistance here will be appreciated.

5 answers

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

1 vote
Ubisoft April 22, 2015

I figured that we should share, I did a port of the old script in the previous versions, and so far it's been working for me.  There are still some bugs with it but it should get the job done, the bugs that I've seen so far are:

-Sometimes the loading spinner just keeps going, and going, and going....  doesn't give you any report at the end.  It's still going to do it's work, just wait a few minutes and it should still complete

-It may spam your logs with errors while it's cycling through all of your instance's filters.  It's nothing critical, it might not be liking some of the characters in the filters?  I'm still not sure what is causing this.

That being said it's worked for all of the times that I used it, I even corrected a small bug that preventing the groups from transferring properties when the user wasn't in the internal directory.

If there are any bugs that you're able to find, i'm all ears.  Just place the file in your script home folder and it should load in the admin panel.

Last thing of note, I tested the rename, but not as much on the merge.  That being said, from how the merge works it shouldn't be a problem.

//Renames Users, as a built-in script in the admin panel.  Configured for Groovy 3.0

package com.onresolve.scriptrunner.canned.jira.admin

import com.atlassian.core.ofbiz.util.CoreTransactionUtil
import com.atlassian.event.api.EventPublisher
import com.atlassian.jira.ComponentManager
import com.atlassian.jira.ManagerFactory
import com.atlassian.jira.bc.JiraServiceContextImpl
import com.atlassian.jira.component.ComponentAccessor
import com.atlassian.jira.event.ClearCacheEvent
import com.atlassian.jira.imports.project.parser.UserAssociationParser
import com.atlassian.jira.issue.CustomFieldManager
import com.atlassian.jira.issue.IssueManager
import com.atlassian.jira.issue.customfields.CustomFieldType
import com.atlassian.jira.issue.fields.UserField
import com.atlassian.jira.issue.index.IssueIndexManager
import com.atlassian.jira.issue.search.CachingSearchRequestStore
import com.atlassian.jira.issue.search.SearchRequest
import com.atlassian.jira.issue.search.SearchRequestManager
import com.atlassian.jira.issue.search.managers.SearchHandlerManager
import com.atlassian.jira.issue.security.IssueSecuritySchemeManagerImpl
import com.atlassian.jira.jql.ClauseHandler
import com.atlassian.jira.notification.DefaultNotificationSchemeManager
import com.atlassian.jira.notification.type.SingleUser
import com.atlassian.jira.permission.DefaultPermissionSchemeManager
import com.atlassian.jira.security.roles.CachingProjectRoleAndActorStore
import com.atlassian.jira.security.roles.actor.UserRoleActorFactory
import com.atlassian.jira.user.util.UserUtil
import com.atlassian.jira.util.BuildUtils
import com.atlassian.jira.util.Consumer
import com.atlassian.jira.util.ImportUtils
import com.atlassian.jira.util.collect.EnclosedIterable
import com.atlassian.query.Query
import com.atlassian.query.QueryImpl
import com.atlassian.query.clause.*
import com.atlassian.query.operand.*
import com.onresolve.scriptrunner.canned.CannedScript
import com.onresolve.scriptrunner.canned.util.BuiltinScriptErrors
import com.onresolve.scriptrunner.canned.util.SimpleBuiltinScriptErrors
import org.apache.log4j.Category
import org.ofbiz.core.entity.*

public class RenameUser implements CannedScript {

    Category log = Category.getInstance(RenameUser.class)
    ComponentManager componentManager = ComponentManager.getInstance()
    UserUtil userUtil = componentManager.getUserUtil()
    SearchRequestManager searchRequestManager = componentManager.getSearchRequestManager()
    def searchRequestService = componentManager.getSearchRequestService()

    public static final String FIELD_FROM_USER_ID = 'FIELD_FROM_USER_ID'
    public static final String FIELD_TO_USER_ID = 'FIELD_TO_USER_ID'
    public static final String FIELD_MERGE = 'FIELD_MERGE'

    List msgs = []
    String msg
    String msg2
    Set<Long> reindexIssueIds = new HashSet()
    boolean preview = false
    Long nSearchReqsUpdated = 0
    boolean doMerge

    public String getName() {
        "Renames a user ID"
    }

    public String getDescription() {
        "Changes a user's ID without reindexing or restarting"
    }

    List getCategories() {
        ["ADMIN"]
    }

    public Boolean isFinalParamsPage(Map params) {
        true
    }

    public List getParameters(Map params) {
        [
                [
                        Name       : FIELD_FROM_USER_ID,
                        Label      : 'From user ID',
                        Description: "Rename or merge from who?",
                ],
                [
                        Name       : FIELD_TO_USER_ID,
                        Label      : 'To user ID',
                        Description: "Rename or merge to who?",
                ],
                [
                        Name       : FIELD_MERGE,
                        Label      : 'Merge',
                        Type       : 'radio',
                        Description: "Merge the first user to the second user, rather than rename",
                        Values     : ["false": "Rename", "true": "Merge"],
                ],
        ]
    }

    public BuiltinScriptErrors doValidate(Map params, boolean forPreview) {
        def errorCollection = new SimpleBuiltinScriptErrors()
        String sourceUserId = params[FIELD_FROM_USER_ID] as String
        String targetUserId = params[FIELD_TO_USER_ID] as String

        if (!params[FIELD_MERGE]) {
            errorCollection.addError(FIELD_MERGE, "Please select an option")
        }
        doMerge = Boolean.parseBoolean(params[FIELD_MERGE] as String)

        [FIELD_FROM_USER_ID, FIELD_TO_USER_ID].each { String p ->
            if (!params[p]) {
                errorCollection.addError(p, "Please provide the user ID")
            }
        }
        if (errorCollection.hasAnyErrors()) {
            return errorCollection
        }

        if (!userUtil.getUser(sourceUserId as String)) {
            errorCollection.addError(FIELD_FROM_USER_ID, "Cannot find user ID")
        }

        if (sourceUserId == targetUserId) {
            errorCollection.addErrorMessage("From and to user can't be the same")
        }

        boolean targetExists = userUtil.getUser(targetUserId as String) as Boolean
        if (doMerge && !targetExists) {
            errorCollection.addError(FIELD_TO_USER_ID, "Target user ID must exist already for a merge")
        }

        if (!doMerge && targetExists) {
            errorCollection.addError(FIELD_TO_USER_ID, "Target user ID must not exist already")
        }

        if (sourceUserId == ComponentAccessor.getJiraAuthenticationContext().getLoggedInUser()?.name) {
            errorCollection.addError(FIELD_FROM_USER_ID, "You can't rename the logged in user")
        }

        errorCollection
    }

    Query transformSearchRequest(Query query, String sourceUser, String targetUser) {
        Clause whereClause = query.getWhereClause()
        Clause newWhereClause = whereClause.accept(new RenamingClauseVisitor(sourceUser, targetUser)) as Clause

//        log.debug("original whereClause: " + query.whereClause.toString())
//        log.debug "new where clause: " + newWhereClause.toString()

        Query newQuery = new QueryImpl(newWhereClause)
        newQuery = new QueryImpl(newWhereClause, query.orderByClause, newQuery.queryString)
        newQuery
    }

    Map doScript(Map params) {
        String sourceUser = params[FIELD_FROM_USER_ID] as String
        String targetUser = params[FIELD_TO_USER_ID] as String
        doMerge = Boolean.parseBoolean(params[FIELD_MERGE] as String)

        // make sure they're not logged on??

        if (!preview) {
            msgs << "<b>Completed</b><br>"
        }
        updateGenericValues(sourceUser, targetUser)
        updateAllSearchRequests(sourceUser, targetUser)

        if (!preview) {
            msg = "Completed ${doMerge ? "merge" : "rename"} of $sourceUser to $targetUser"
            log.warn(msg)
            msgs << msg
        }



        params["output"] = "<pre>" + msgs.join("\n") + "<pre>"
        params
    }

    private def reindexIssue(IssueManager issueManager, Long issueId, Category log, IssueIndexManager indexManager) {
        GenericValue issue = issueManager.getIssue(issueId)
        boolean wasIndexing = ImportUtils.isIndexIssues();
        ImportUtils.setIndexIssues(true);
        if ((BuildUtils.getCurrentBuildNumber() as Long) < 614) {
            ManagerFactory.getCacheManager().flush(com.atlassian.jira.issue.cache.CacheManager.ISSUE_CACHE, issue)
        }
        log.debug("Reindex issue ${issue.key}")
        indexManager.reIndex(issue);
        ImportUtils.setIndexIssues(wasIndexing)
    }

    def updateGenericValues(String sourceUser, String targetUser) {
        boolean began = false

        DelegatorInterface gd = (DelegatorInterface) componentManager.getComponentInstanceOfType(DelegatorInterface.class)
        IssueIndexManager indexManager = ComponentManager.getInstance().getIndexManager()
        CachingProjectRoleAndActorStore cachingProjectRoleAndActorStore = ComponentManager.getComponentInstanceOfType(CachingProjectRoleAndActorStore.class) as CachingProjectRoleAndActorStore
        DefaultNotificationSchemeManager notificationSchemeManager = ComponentManager.getComponentInstanceOfType(DefaultNotificationSchemeManager.class) as DefaultNotificationSchemeManager
        DefaultPermissionSchemeManager defaultPermissionSchemeManager = ComponentManager.getComponentInstanceOfType(DefaultPermissionSchemeManager.class) as DefaultPermissionSchemeManager
        IssueSecuritySchemeManagerImpl securitySchemeManager = ComponentManager.getComponentInstanceOfType(IssueSecuritySchemeManagerImpl.class) as IssueSecuritySchemeManagerImpl
        CachingSearchRequestStore cachingSearchRequestStore = ComponentManager.getComponentInstanceOfType(CachingSearchRequestStore.class) as CachingSearchRequestStore
        IssueManager issueManager = componentManager.getIssueManager()

        if (!preview) {
            log.debug("Begin transaction")
            began = CoreTransactionUtil.begin();
        }

        try {
            msg = "Beginning ${doMerge ? "merge" : "rename"} of $sourceUser to $targetUser"
            log.warn(msg)
            msgs << msg

            // Rename the user record itself
            if ((BuildUtils.getCurrentBuildNumber() as Long) < 614) {
                List gvs = gd.findByAnd("OSUser", [name: sourceUser])
                log.debug(gvs)
                assert gvs.size() == 1
                if (!preview) {
                    GenericValue usergv = gvs.first() as GenericValue
                    if (doMerge) {
                        usergv.remove()
                    } else {
                        usergv.set("name", targetUser)
                        usergv.store()
                    }
                }

                // group memberships
                gvs = gd.findByAnd("OSMembership", [userName: sourceUser])
                msg = "Update ${gvs.size()} OSMembership records"
                log.debug(gvs)
                log.debug(msg)
                msgs << msg
                if (!preview) {

                    List<GenericValue> targetGvs = gd.findByAnd("OSMembership", [userName: targetUser])
                    List<Object> targetGroups = targetGvs*.get("groupName")

                    gvs.each { GenericValue gv ->
                        if (doMerge && targetGroups.contains(gv.get("groupName"))) {
                            gv.remove()
                        } else {
                            gv.set("userName", targetUser)
                            gv.store()
                        }
                    }
                }
            } else {
                List gvs = gd.findByAnd("User", [userName: sourceUser])
                assert gvs.size() == 1
                log.debug(gvs)
                if (!preview) {
                    GenericValue usergv = gvs.first() as GenericValue
                    if (doMerge) {
                        usergv.remove()
                    } else {
                        usergv.set("userName", targetUser)
                        usergv.set("lowerUserName", targetUser.toLowerCase())
                        usergv.store()
                    }
                }

                // group memberships
                gvs = gd.findByAnd("Membership", [childName: sourceUser, membershipType: "GROUP_USER"])
                msg = "Update ${gvs.size()} Membership records"
                log.debug(gvs)
                log.debug(msg)
                msgs << msg
                if (!preview) {

                    List<GenericValue> targetGvs = gd.findByAnd("Membership", [childName: targetUser, membershipType: "GROUP_USER"])
                    List<Object> targetGroups = targetGvs*.get("parentName")

                    gvs.each { GenericValue gv ->
                        if (doMerge && targetGroups.contains(gv.get("parentName"))) {
                            gv.remove()
                        } else {
                            gv.set("childName", targetUser)
                            gv.set("lowerChildName", targetUser)
                            gv.store()
                        }
                    }
                }
            }
            if ((BuildUtils.getCurrentBuildNumber() as Long) > 6093) {
                List gvs = gd.findByAnd("ApplicationUser", [userKey: sourceUser, lowerUserName: sourceUser])
                List gvstoremove = gd.findByAnd("ApplicationUser", [userKey: targetUser])
                log.debug(gvs)
                log.debug(gvstoremove)
                msg = "Update ${gvs.size()} ApplicationUser records"
                GenericValue gvremove
                if (gvstoremove) {
                    msg2 = "Remove ${gvstoremove.size()} outdated ApplicationUser records"
                    msgs << msg2
                    gvremove = gvstoremove.first() as GenericValue
                }
                log.debug(msg)
                msgs << msg
                assert gvs.size() == 1
                if (!preview) {
                    GenericValue usergv = gvs.first() as GenericValue

                    if (doMerge) {
                        // todo: delete their associated property records
                        usergv.remove()
                    } else {
                        if (gvremove) {
                            gvremove.remove()
                        }
                        usergv.set("userKey", targetUser)
                        usergv.set("lowerUserName", targetUser)
                        usergv.store()
                    }
                }
            }

            // trustedapp
            List gvs = gd.findByAnd("TrustedApplication", [createdBy: sourceUser])
            msg = "Update ${gvs.size()} TrustedApplication records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                gvs.each { GenericValue gv ->
                    gv.set("createdBy", targetUser)
                    gv.store()
                }
            }

            // SearchRequest ownership
            // if this is a merge, we may get dupe filter names. Not ideal but handled OK by the UI
            gvs = gd.findByOr("SearchRequest", [user: sourceUser, author: sourceUser])
            msg = "Update ${gvs.size()} SearchRequest records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                gvs.each { GenericValue gv ->
                    if (gv.get("user")) {
                        gv.set("user", targetUser)
                    }
                    if (gv.get("author")) {
                        gv.set("author", targetUser)
                    }
                    removeFromCache(cachingSearchRequestStore)
                    gv.store()
                }
            }

            // ColumnLayout - choose one or tother.
            gvs = gd.findByAnd("ColumnLayout", [username: sourceUser])
            msg = "Update ${gvs.size()} ColumnLayout records"
            List<GenericValue> targetGvs = gd.findByAnd("ColumnLayout", [username: targetUser])
            log.debug(msg)
            msgs << msg
            if (!preview) {
                gvs.each { GenericValue gv ->
                    if (doMerge && targetGvs.find { it.getLong("searchrequest") == gv.getLong("searchrequest") }) {
                        gd.removeRelated("ChildColumnLayoutItem", gv)
                        gv.remove()
                    } else {
                        gv.set("username", targetUser)
                        gv.store()
                    }
                }
            }

            if (!doMerge) {
                gvs = gd.findByAnd("UserHistoryItem", [username: sourceUser])
                msg = "Update ${gvs.size()} UserHistoryItem records"
                log.debug(msg)
                msgs << msg
                if (!preview) {
                    gvs.each { GenericValue gv ->
                        gv.set("username", targetUser)
                        gv.store()
                    }
                }
            }

            gvs = gd.findByAnd("FavouriteAssociations", [username: sourceUser])
            targetGvs = gd.findByAnd("FavouriteAssociations", [username: targetUser])
            msg = "Update ${gvs.size()} FavouriteAssociations records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                List matchFields = ["entityType", "entityId"]
                gvs.each { GenericValue gv ->
                    // favourite assocs, only fave if not already faved
                    if (doMerge && targetGvs.find {
                        it.getFields(matchFields) == gv.getFields(matchFields)
                    }) {
                        // favcount must be adjusted downwards when both are favouriting
                        log.debug("Remove fav association record and decrement favourite count")
                        GenericValue sharedEntityGv = gd.findByPrimaryKey(gv.get("entityType") as String, [id: gv.get("entityId")])
                        sharedEntityGv.set("favCount", (sharedEntityGv.get("favCount") as Long) - 1)
                        sharedEntityGv.store()

                        gv.remove()
                    } else {
                        gv.set("username", targetUser)
                        gv.store()
                    }
                }
            }


            ["PortalPage", "FilterSubscription"].each { String entityName ->
                gvs = gd.findByAnd(entityName, [username: sourceUser])
                msg = "Update ${gvs.size()} $entityName records"
                log.debug(msg)
                msgs << msg
                if (!preview) {
                    gvs.each { GenericValue gv ->
                        gv.set("username", targetUser)
                        gv.store()
                    }
                }
            }
            // note: for versions prior to 4.1 the favouritesStoreCache needs to be flushed

            // Core issue fields
            gvs = gd.findByOr("Issue", [reporter: sourceUser, assignee: sourceUser])
            msg = "Update ${gvs.size()} Issue records"
            log.debug(msg)
            msgs << msg
            gvs.each { GenericValue gv ->
                if (!preview) {
                    if (gv.get("reporter") == sourceUser) {
                        gv.set("reporter", targetUser)
                    }
                    if (gv.get("assignee") == sourceUser) {
                        gv.set("assignee", targetUser)
                    }
                    gv.store()
                }
                reindexIssueIds.add(gv.getLong("id"))
            }

            // issue transition history
            gvs = gd.findByOr("Action", [author: sourceUser, updateauthor: sourceUser])
            msg = "Update ${gvs.size()} Action records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                gvs.each { GenericValue gv ->
                    if (gv.get("author") == sourceUser) {
                        gv.set("author", targetUser)
                    }
                    if (gv.get("updateauthor") == sourceUser) {
                        gv.set("updateauthor", targetUser)
                    }
                    gv.store()
                }
            }

            // change groups
            gvs = gd.findByAnd("ChangeGroup", [author: sourceUser])
            msg = "Update ${gvs.size()} ChangeGroup records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                gvs.each { GenericValue gv ->
                    gv.set("author", targetUser)
                    gv.store()
                }
            }

            // Change items
            // todo: custom field history items - problem is they are referenced by name which is not unique
            EntityConditionList conditionList = new EntityConditionList(
                    [new EntityConditionList(
                            [
                                    // Stoopid oracle can't do equals on a clob, LIKE shd be OK
                                    // alternative is to do the filtering in the code
                                    new EntityExpr("oldvalue", EntityOperator.LIKE, sourceUser),
                                    new EntityExpr("newvalue", EntityOperator.LIKE, sourceUser),
                            ], EntityOperator.OR),
                     new EntityConditionList([new EntityExpr("field", EntityOperator.IN, ["reporter", "assignee"])], EntityOperator.AND)
                    ], EntityOperator.AND
            )
            gvs = gd.findByCondition("ChangeItem", conditionList, ["id", "field", "oldvalue", "newvalue"], Collections.<String> emptyList())
            msg = "Update ${gvs.size()} ChangeItem records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                gvs.each { GenericValue gv ->
                    if (gv.get("oldvalue") == sourceUser) {
                        gv.set("oldvalue", targetUser)
                    }
                    if (gv.get("newvalue") == sourceUser) {
                        gv.set("newvalue", targetUser)
                    }
                    gv.store()
                }
            }

            // Watchers and voters
            gvs = gd.findByAnd("UserAssociation", getUserAssociationConditionList(sourceUser))
            targetGvs = gd.findByAnd("UserAssociation", getUserAssociationConditionList(targetUser))
            msg = "Update ${gvs.size()} UserAssociation records"
            log.debug(msg)
            msgs << msg
            gvs.each { GenericValue gv ->
                if (gv.get("sinkNodeEntity") == "Issue") {
                    reindexIssueIds.add(gv.getLong("sinkNodeId"))
                }
                if (!preview) {
                    gd.removeValue(gv)

                    if (doMerge && targetGvs.find {
                        it.get("sinkNodeId") == gv.get("sinkNodeId") &&
                                it.get("sinkNodeEntity") == gv.get("sinkNodeEntity") &&
                                it.get("associationType") == gv.get("associationType")
                    }) {
                        // do nothing as always remove
                        log.debug("Removed userassoc record")
                    } else {
                        // gv.store() doesn't work because we're changing the primary key
                        gv.set("sourceName", targetUser)
                        gd.create(gv)
                    }
                }
            }

            // roles
            gvs = gd.findByAnd("ProjectRoleActor", [roletype: UserRoleActorFactory.TYPE, roletypeparameter: sourceUser])
            targetGvs = gd.findByAnd("ProjectRoleActor", [roletype: UserRoleActorFactory.TYPE, roletypeparameter: targetUser])
            msg = "Update ${gvs.size()} ProjectRoleActor records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                gvs.each { GenericValue gv ->
                    List matchFields = ["pid", "projectroleid"]
                    if (doMerge && targetGvs.find {
                        gv.getFields(matchFields) == it.getFields(matchFields)
                    }) {
                        log.debug("Remove role record")
                        gv.remove()
                    } else {
                        gv.set("roletypeparameter", targetUser)
                        gv.store()
                    }
                }
                cachingProjectRoleAndActorStore.clearCaches()
            }

            // SharePermissions not required as cannot share with a user

            // worklogs
            gvs = gd.findByOr("Worklog", [author: sourceUser, updateauthor: sourceUser])
            msg = "Update ${gvs.size()} Worklog records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                gvs.each { GenericValue gv ->
                    if (gv.get("author") == sourceUser) {
                        gv.set("author", targetUser)
                    }
                    if (gv.get("updateauthor") == sourceUser) {
                        gv.set("updateauthor", targetUser)
                    }
                    gv.store()
                }
            }

            // project and comp leads
            gvs = gd.findByAnd("Project", [lead: sourceUser])
            msg = "Update ${gvs.size()} Project records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                gvs.each { GenericValue gv ->
                    gv.set("lead", targetUser)
                    gv.store()
                }
            }
            gvs = gd.findByAnd("Component", [lead: sourceUser])
            msg = "Update ${gvs.size()} Component records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                gvs.each { GenericValue gv ->
                    gv.set("lead", targetUser)
                    gv.store()
                }
            }

            // notification schemes
            gvs = gd.findByAnd("Notification", [type: SingleUser.DESC, parameter: sourceUser])
            targetGvs = gd.findByAnd("Notification", [type: SingleUser.DESC, parameter: targetUser])
            msg = "Update ${gvs.size()} Notification records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                gvs.each { GenericValue gv ->
                    if (doMerge && targetGvs.find {
                        gv.getFields(["scheme", "eventTypeId", "type"]) == it.getFields(["scheme", "eventTypeId", "type"])
                    }) {
                        log.debug("Remove Notification record")
                        gv.remove()
                    } else {
                        gv.set("parameter", targetUser)
                        gv.store()
                    }
                }
                notificationSchemeManager.flushProjectSchemes()
            }

            // permission schemes
            gvs = gd.findByAnd("SchemePermissions", [type: "user", parameter: sourceUser])
            targetGvs = gd.findByAnd("SchemePermissions", [type: "user", parameter: targetUser])
            msg = "Update ${gvs.size()} SchemePermissions records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                List matchFields = ['scheme', 'permission', 'type', 'parameter']
                gvs.each { GenericValue gv ->
                    if (doMerge && targetGvs.find {
                        gv.getFields(matchFields) == it.getFields(matchFields)
                    }) {
                        log.debug("Remove Notification record")
                        gv.remove()
                    } else {
                        gv.set("parameter", targetUser)
                        gv.store()
                    }
                }
                defaultPermissionSchemeManager.flushProjectSchemes()
                defaultPermissionSchemeManager.flushSchemeEntities()
            }

            // issue security schemes
            gvs = gd.findByAnd("SchemeIssueSecurities", [type: "user", parameter: sourceUser])
            targetGvs = gd.findByAnd("SchemePermissions", [type: "user", parameter: targetUser])
            msg = "Update ${gvs.size()} SchemeIssueSecurities records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                List matchFields = ['scheme', 'security', 'type', 'parameter']
                gvs.each { GenericValue gv ->
                    if (doMerge && targetGvs.find {
                        gv.getFields(matchFields) == it.getFields(matchFields)
                    }) {
                        log.debug("Remove Notification record")
                        gv.remove()
                    } else {
                        gv.set("parameter", targetUser)
                        gv.store()
                    }
                }
                securitySchemeManager.flushProjectSchemes()
            }

            // user and multi-user custom fields
            gvs = gd.findByCondition("CustomField",
                    new EntityExpr("customfieldtypekey", EntityOperator.IN,
                            [
                                    "com.atlassian.jira.plugin.system.customfieldtypes:userpicker",
                                    "com.atlassian.jira.plugin.system.customfieldtypes:multiuserpicker"
                            ])
                    ,
                    ["id"], Collections.<String> emptyList())
            Long cfValueKount = 0
            gvs.each { GenericValue gv ->
                gv.getRelatedByAnd("ChildCustomFieldValue", [stringvalue: sourceUser]).each { GenericValue cfGv ->
                    cfValueKount++
                    if (!preview) {
                        cfGv.set("stringvalue", targetUser)
                        cfGv.store()
                    }
                    reindexIssueIds.add(cfGv.getLong("issue"))
                }
            }
            msg = "Update $cfValueKount CustomFieldValue records"
            log.debug(msg)
            msgs << msg

            gvs = gd.findByAnd("OSHistoryStep", [caller: sourceUser])
            msg = "Update ${gvs.size()} OSHistoryStep records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                gvs.each { GenericValue gv ->
                    gv.set("caller", targetUser)
                    gv.store()
                }
            }

            // fileattachments
            gvs = gd.findByAnd("FileAttachment", [author: sourceUser])
            msg = "Update ${gvs.size()} FileAttachment records"
            log.debug(msg)
            msgs << msg
            if (!preview) {
                gvs.each { GenericValue gv ->
                    gv.set("author", targetUser)
                    gv.store()
                }
            }

            // -------------------------------------- Finalise --------------------------------------
            if (!preview) {
                log.debug("Commit transaction")
                CoreTransactionUtil.commit(began);

                // Only for 4.1 and above - clearing the other caches are not necessary above but kept there if
                // I decide to backport this
                log.debug("Publish clear cache event")
                ComponentManager.getComponentInstanceOfType(EventPublisher.class).publish(ClearCacheEvent.INSTANCE);

                // reindex issues
                reindexIssueIds.each { Long issueId ->
                    reindexIssue(issueManager, issueId, log, indexManager);
                }
            }

            msg = "Reindex ${reindexIssueIds.size()} issues"
            log.debug(msg)
            msgs << msg
        }
        catch (Exception e) {
            log.error("Problem renaming user, rolling back")
            CoreTransactionUtil.rollback(began)
            throw e
        }
        finally {
            // not sure if we need anything here
        }

    }

    private void removeFromCache(CachingSearchRequestStore cachingSearchRequestStore) {
        def cachemap = cachingSearchRequestStore.cacheByUser
        cachemap.removeAll()
    }

    private List<EntityExpr> getUserAssociationConditionList(String userId) {
        List userAssocCList = [
                new EntityExpr("sourceName", EntityOperator.EQUALS, userId),
                new EntityExpr("associationType", EntityOperator.IN,
                        [UserAssociationParser.ASSOCIATION_TYPE_WATCH_ISSUE, UserAssociationParser.ASSOCIATION_TYPE_VOTE_ISSUE]),
        ]
        return userAssocCList
    }

    def updateSingleRequest(SearchRequest sr, String sourceUser, String targetUser) {
        Query query = sr.getQuery()

        Query newQuery
        // A query may have no where clause and select everything... if so .accept dies
        if (query.getWhereClause()) {
            newQuery = transformSearchRequest(query, sourceUser, targetUser)
        } else {
            newQuery = query
        }

        if (query.whereClause.toString() != newQuery.whereClause.toString()) {
            // this is stupid but setting a queryString seems to blow away the whereClause, so we do this twice
            nSearchReqsUpdated++
            log.debug("Update sr name: " + sr.name)
            // log.debug("queryString: " + newQuery.queryString)
            // log.debug("newQuery: $newQuery")

            sr.setQuery(newQuery)
            JiraServiceContextImpl ctx = new JiraServiceContextImpl(userUtil.getUser(sr.ownerUserName))
            searchRequestService.validateFilterForUpdate(ctx, sr)
            if (ctx.errorCollection.hasAnyErrors()) {
                log.error("errors: " + ctx.getErrorCollection())
            } else {
                if (!preview) {
                    searchRequestService.updateFilter(ctx, sr)
                }
            }
        } else {
            log.debug("No need to update name: " + sr.name)
        }
    }

    private def updateAllSearchRequests(String sourceUser, String targetUser) {
        EnclosedIterable<SearchRequest> srs = searchRequestManager.getAll()
        srs.foreach(new Consumer<SearchRequest>() {
            void consume(SearchRequest sr) {

                try {
                    updateSingleRequest(sr, sourceUser, targetUser)
                } catch (Exception e) {
                    log.error("Error checking or updating search request: $sr - this is not fatal but you may wish to check this manually", e)
                }
            }
        })
        msg = "Update parameters for $nSearchReqsUpdated search requests"
        log.debug(msg)
        msgs << msg
    }

    String getDescription(Map params, boolean forPreview) {
        preview = true
        Map out = doScript(params)
        out["output"]
    }

    public String getHelpUrl() {
        "https://jamieechlin.atlassian.net/wiki/display/GRV/Built-In+Scripts#Built-InScripts-RenameaUserID"
    }
}

class RenamingClauseVisitor implements ClauseVisitor {

    Category log = Category.getInstance(RenamingClauseVisitor.class)
    SearchHandlerManager searchHandlerManager = ComponentManager.getComponentInstanceOfType(SearchHandlerManager.class) as SearchHandlerManager
    ComponentManager componentManager = ComponentManager.getInstance()
    CustomFieldManager customFieldManager = componentManager.getCustomFieldManager()

    String sourceUser
    String targetUser

    RenamingClauseVisitor(String sourceUser, String targetUser) {
        this.sourceUser = sourceUser
        this.targetUser = targetUser
    }

    Object visit(AndClause andClause) {
        Collection<Clause> newClauses = transformSubClauses(andClause)
        new AndClause(newClauses)
    }

    Object visit(NotClause notClause) {
        new NotClause(notClause.getSubClause().accept(new RenamingClauseVisitor(sourceUser, targetUser)) as Clause)
    }

    Object visit(OrClause orClause) {
        Collection<Clause> newClauses = transformSubClauses(orClause)
        new OrClause(newClauses)
    }

    private Collection<Clause> transformSubClauses(Clause mainClause) {
        Collection<Clause> newClauses = []
        mainClause.getClauses().each { Clause clause ->
            newClauses.add(clause.accept(new RenamingClauseVisitor(sourceUser, targetUser)) as Clause)
        }
        return newClauses
    }

    Object visit(TerminalClause clause) {
        final Collection<ClauseHandler> handlers = searchHandlerManager.getClauseHandler(clause.getName());
        Collection<String> fieldIds = searchHandlerManager.getFieldIds(clause.getName())

        // If there are multiple handers, eg field has multiple duplicate names, don't deal with it
        // ie multiple fields with the same name visible to this user
        if (fieldIds?.size() != 1) {
            return clause
        }

        String fieldId = fieldIds.toList().first()
        boolean isUserField = false
        if (fieldId.startsWith("customfield_")) {
            CustomFieldType cfType = customFieldManager.getCustomFieldObject(fieldId).getCustomFieldType()
            if (cfType instanceof UserField) {
                isUserField = true
            }
        }

        if (["reporter", "assignee"].contains(fieldId) || isUserField) {
            Operand operand = clause.getOperand()
            Operand newOp = operand.accept(new RenamingOperandVisitor(sourceUser, targetUser)) as Operand
            TerminalClauseImpl newTermClause = new TerminalClauseImpl(clause.name, clause.getOperator(), newOp)
            return newTermClause
        }

        clause
    }

    Object visit(WasClause wasClause) {
        // wasClause in 4.3 not handled at the moment
        wasClause
    }

    Object visit(ChangedClause changedClause) {
        // changed not handled
        return null
    }
}

class RenamingOperandVisitor implements OperandVisitor {

    Category log = Category.getInstance(RenamingOperandVisitor.class)
    String sourceUser
    String targetUser

    RenamingOperandVisitor(String sourceUser, String targetUser) {
        this.sourceUser = sourceUser
        this.targetUser = targetUser
    }

    Object visit(EmptyOperand empty) {
        empty
    }

    Object visit(FunctionOperand function) {
        function
    }

    Object visit(MultiValueOperand multiValue) {
        List<Operand> newOps = multiValue.getValues().collectAll { Operand operand ->
            if (operand instanceof SingleValueOperand && operand.getStringValue() == sourceUser) {
                return new SingleValueOperand(targetUser)
            }
            operand
        }
        new MultiValueOperand(newOps)
    }

    Object visit(SingleValueOperand singleValueOperand) {
        if (singleValueOperand.getStringValue() == sourceUser) {
            return new SingleValueOperand(targetUser)
        }
        singleValueOperand
    }

}

Cheers, 

Eric

Andrew DeFaria October 26, 2015

How do you use this thing? Is it a groovy script or java? How do you do "Just place the file in your script home folder and it should load in the admin panel." Where is my script home folder? /var/atlassian/application-data/jira/scripts? I tried putting this there but I didn't see it in my admin panel.

0 votes
skorzinetzki April 7, 2015

did you find a solution/workaround?

0 votes
skorzinetzki April 7, 2015

Same problem here, we may reuse certain user names for new employees after people leaving the company. therefore we used merge user for moving all "old stuff" to one "left employee" account.

0 votes
MattS
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
January 8, 2015

I would ask your ad admin to leave the user but deactivate them. Then the user will be deactivated in JIRA and not count to your license. Otherwise you are losing history in JIRA for no gain

0 votes
Michel Tremblay January 8, 2015

In JIRA 6 you should be able to rename a user ID directly from the User Admin Panel

 

Nic Brough -Adaptavist-
Community Leader
Community Leader
Community Leaders are connectors, ambassadors, and mentors. On the online community, they serve as thought leaders, product experts, and moderators.
January 8, 2015

I think that's why Script Runner no longer has "rename user" - you can do it natively now.

Jim Winsor January 8, 2015

Thanks for the reply. I'm not sure I see where we can accomplish our use case. In order to preserve the name associated with objects in Jira (issues, filters, dashboards, etc.) we have created an internal user and used the built-in command to "move" everything to the new user using the merge option. If this is a capability through the user management panel, I fail to see it. Can you help? Thanks!

Michel Tremblay January 8, 2015

So it is the merge user that you are looking after ?

Jim Winsor January 8, 2015

Correct. I found where users in the internal directory can be edited, but we use the AD sync to define and authenticate our users, and their entries in Jira are read-only. When a user leaves the company, we are given a 30 day window to define an internal user, and "move" all the AD user's objects to the internal user. Thanks!

Michel Tremblay January 8, 2015

you could try to put back the edit issue script into the right path in Jira and test it in your jira 6.3. I am not sure this script will work as it is. (I doubt it) If it's not, maybe debugging this script to make it compatible to jira 6.3 would be the way to go for you. you can find the file in the .jar of script runner plugin (I suggest downloading the last version that still has this builtin) After extracting the plugin files, you will find your desired script here : com\onresolve\jira\groovy\canned\admin\RenameUser.groovy you then have to place this file in : jiraHome/WEB-INF/classes/com/onresolve/jira/groovy/canned/admin You should not need to reboot to get the script to load back into your panel. Give it a try, and see if it works. Michel

Adam Downer January 22, 2015

I am now in a similar position. The ability to rename a user natively is great. but really what I want to do is remove the incorrectly defined local user by merging all their activity with the properly defined user from AD. Did the script get tried out on 6.3?

Raju Adluru February 2, 2015

Hi Michel we have latest JIRA 6.3.12, i dont see that path(jiraHome/WEB-INF/classes/com/onresolve/jira/groovy/canned/admin) in our JIRA!, i think they moved all plugins to some other folder structure. Do you know where to keep RenameUser.groovy in latest JIRA version, i found this groovy from old version of script runner. Let us know if you have any workaround for this, thanks alot. Raju

skorzinetzki April 7, 2015

Any new suggestions?

Michel Tremblay April 17, 2015

Usually the folder WEB-INF/classes should be there but youwill have to create the com/onresolve/jira/groovy/canned/admin If you test it, only try it on a test instance first. As there might have some difference between 6.2 and 6.3 that will make this script break things.

Comments for this post are closed

Community moderators have prevented the ability to post new answers.

Post a new question

TAGS
AUG Leaders

Atlassian Community Events