Forums

Articles
Create
cancel
Showing results for 
Search instead for 
Did you mean: 

Using Scriptrunner to Delete Users in Specific Directory (Jira/JSM Datacenter)

Megan Moulton
Community Champion
December 30, 2024

After playing around through some community threads and testing, I created a script that allows you to

  1. Find users in a specified directory
  2. Determine if they have data assigned, reported, or commented (and if not, are safe to delete)
  3. Removes user

 

This is great for removing unneeded JSM customers in datacenter that appear in your user list under Internal Directory. 

Recommended you limit results to 20k or less and run iteratively 

(Script broken into two parts due to formatting) 

import com.atlassian.crowd.manager.directory.DirectoryManager
import com.atlassian.jira.bc.user.search.UserSearchParams
import com.atlassian.jira.bc.user.search.UserSearchService
import com.atlassian.jira.component.ComponentAccessor
import groovy.xml.MarkupBuilder
import java.io.StringWriter
import com.atlassian.jira.user.ApplicationUser
import com.atlassian.jira.bc.user.UserService
final directoryToCheck = 'Jira Internal Directory'

// Get necessary Jira components
def directoryManager = ComponentAccessor.getComponent(DirectoryManager)
def userSearchService = ComponentAccessor.getComponent(UserSearchService)
def userService = ComponentAccessor.getComponent(UserService)
def currentUser = ComponentAccessor.jiraAuthenticationContext.loggedInUser

// Get the internal directory ID based on directory name
def internalDirectoryId = directoryManager.findAllDirectories()?.find { it.name.toString().toLowerCase() == directoryToCheck.toLowerCase() }?.id

// Build search parameters for user search
UserSearchParams.Builder paramBuilder = UserSearchParams.builder()
        .allowEmptyQuery(true)
        .includeActive(true)  // Only active users
        .includeInactive(false) // Exclude inactive users
        .limitResults(200)  // Adjust this if you have many users

// Get all active users from the internal directory
def allInternalDirectoryUsers = userSearchService.findUsers('', paramBuilder.build()).findAll { it.directoryId == internalDirectoryId }

// Method to check if a user does **not** have any assigned, reported, or commented issues
def isUserInactive(user) {
    if (user instanceof ApplicationUser) {
        def username = user.username

        // Construct JQL queries for:
        // - Issues assigned to the user
        // - Issues reported by the user
        // - Comments authored by the user
        def assignedJQL = "assignee = \"${username}\""
        def reportedJQL = "reporter = \"${username}\""
        def commentedJQL = "issue in commentsBy(\"${username}\")"

        try {
            def assignedIssues = Issues.search(assignedJQL)
            def reportedIssues = Issues.search(reportedJQL)
            def commentedIssues = Issues.search(commentedJQL)

            // If none of the queries return issues, the user is effectively inactive (no assigned, reported, or commented issues)
            return (assignedIssues.size() == 0 && reportedIssues.size() == 0 && commentedIssues.size() == 0)
        } catch (Exception e) {
            println "Error executing JQL: ${e.message}"
            return false
        }
    }
    return false
}

// Collect inactive users for the table and prepare a list of deleted users
def inactiveUsers = []
def deletedUsers = []

allInternalDirectoryUsers.each { user ->
    // Safely access user properties and check if they are active but have no issues or comments
    if (user instanceof ApplicationUser && isUserInactive(user)) {
        // Add the inactive user to the list (active but no issues, no comments)
        inactiveUsers.add([username: user.username, email: user.emailAddress])  // Only add users with username and email

        // Validate the deletion of the user
        try {
            def result = userService.validateDeleteUser(currentUser, user.username)

            // Check if the validation is successful
            if (result.isValid()) {
                // If validation passes, delete the user
                userService.removeUser(currentUser, result)
                deletedUsers.add([username: user.username, email: user.emailAddress]) // Track deleted user
                println "User deleted: ${user.username} (Email: ${user.emailAddress})"
            } else {
                println "User deletion validation failed for ${user.username}: ${result.errorCollection}"
            }
        } catch (Exception e) {
            println "Error deleting user ${user.username}: ${e.message}"
        }
    }
}

// Create the StringWriter to generate HTML output
def stringWriter = new StringWriter()
def content = new MarkupBuilder(stringWriter)

// Create the header for the HTML output
content.html {
    p("Users in Jira Internal Directory: ${allInternalDirectoryUsers?.size()}")
    p("List of Deleted Users:")
    
    // Print a table of deleted users
    if (deletedUsers.size() > 0) {
        table {
            tr {
                th("Username")
                th("Email")
            }
            deletedUsers.each { user ->
                tr {
                    td(user.username)
                    td(user.email)
                }
            }

1 answer

0 votes
Radek Dostál
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.
December 31, 2024

Ok, but why is this raised as a question?

Sure you can use something like this but it's missing a few other checks that would be error-prone to suggest this so broadly. Sure it's a start, but it's by no means a generic solution for everyone. Leaving it as is will cause problems elsewhere that perhaps you might not be susceptible to with your use case, but I do recall finding some troubles when I wrote something similar.

Deleting users also drops some other data in parallel (dashboards come to mind, for instance), so that is already one possibility to cause "data loss" if the user was not JUST a jsm customer - and I can easily see someone copy pasting this on their instance and subsequently verifying their backup restoration procedure.

As you're using UserService, I recall coming across it leaving corrupt data related to favourite associations that had to be pre-wiped to avoid it, so that's another potential thing.

Additionally, I wouldn't trust indexes to decide whether to keep or delete anything. UserService validation should prevent the actual deletion anyway, but it's not optimal (you could as well not do any JQL check at all and just straight up pass every user for deletion, and now that doesn't feel very safe does it).

JQL with 'commentsBy' in it may run forever on sufficiently large instances (once you go over couple millions, you start noticing global functions in jql right away).

 

I'm not saying this won't work for you, but it shouldn't be relied on or be regarded as safe enough to suggest broadly.

 

 

Suggest an answer

Log in or Sign up to answer