Deleting JSM customer users via API

delete.jpeg

Adding users into you instance is one thing, however removing these users could be another challenge. If you're familiar with Jira's REST API it should be your saving grace to tweak and automate certain things. However making these calls could be challenging for people who are new to it and sometimes you can't get it to work the way you want due to various reasons. Below I'm going to show you how to delete users from your Jira cloud instance (JSM customer users or Jira users) using API and in less than 20 lines of code. Sound exciting, how about you continue reading below to get the insight.

To do this, we're focusing on python and using a python package called jiraone (get it via pip install jiraone).

Deleting a user

In order to delete a user on cloud, you will need to know the accountId. How about if there was a way to delete a user without knowing the accountId and simply by just knowing the display name of the user? Well this feature already exist within Jira's API, you just need to know how to call it. Let's start off with a basic call using the jiraone package to delete a user.

from jiraone import LOGIN, endpoint, USER, echo

user = "email"
password = "token"
link = "https://example.atlassian.net"

LOGIN(user=user, password=password, url=link)
# search for users
name_of_user = "Elf App 607"
search = USER.search_user(name_of_user, pull="active", user_type="customer")
for name in search:
# delete a user that's found
delete = LOGIN.delete(endpoint.jira_user(name.get("accountId")))
# returns 204 for successful deletion
if delete.status_code < 300:
echo("User deleted...")
else:
echo(delete.status_code)
echo("User not deleted...")

Let me explain what is happening on the above code. We need to delete a user with a name of "Elf App 607". Now we're calling our search_user method and our arguments needs to pull only active users of type "customer" - This way we want to only search for JSM customer with that name. If the user exist and is found, we delete and get a response of 204. This same principle applies to Jira users in such a scenario change the user_type argument to "atlassian".

Bulk deleting users

I think we got the hang of things, how about we take this further into bulk deleting multiple users. Remember we're keeping our code within 20 lines and not looking for any accountId but by means of the user's display name. In order to do this, we'll need a file source to wrap our user's display names. You can do this by using a csv file and placing each user's name on a single line and save as a .csv file format. Example below

Elf App 607

Elf App 61

Elf App 405

from jiraone import LOGIN, endpoint, USER, echo, file_reader

args = "email", "token", "https://example.atlassian.net"
LOGIN(*args)
read = file_reader(file_name="user_files.csv")
get_user_list = []
get_account_id = []
for users in read:
get_user_list.append(users[0])
for names in get_user_list:
search = USER.search_user(names, pull="active", user_type="customer", skip=True)
for name in search:
get_account_id.append(name.get("accountId"))
for aid in get_account_id:
delete = LOGIN.delete(endpoint.jira_user(aid))
if delete.status_code < 300:
print("User deleted")
else:
echo("User not deleted...")

Now we're going for a bulk delete of users, we start by using a tuple that way we can reduce our code line to just one and keep it within 20 lines of code. It has to be in a sequence to follow user, password and url keyword arguments from the LOGIN variable as shown in the first example above. We prepare two empty list to capture the names of the users after being read and the accountId after being found. In the latter situations, if the users is not found, the script will automatically terminate with an error of user not found. Once we get all the user's accountId we've listed out, then we run a delete of the users.

18 comments

Connor Barattini August 6, 2021

I'm getting a 404 status_code on trying to delete a user. Am I missing something?

Prince Nyeche
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.
August 6, 2021

404 generally means the resource was not found. Most probably, the user you're searching for was not found and returned 0 instead of a list containing key-value pairs of the user. Please can you print the result of the user search to see what's being returned?

Like Etienne likes this
Robert Rodrigues July 8, 2022

@Prince Nyeche I get a 400 error and given that the api docs state:

Returned if:

accountId, query or property is missing.
query and accountId are provided.
property parameter is not valid.
A schema has not been defined for this response code.

 

Im wondering if the returned values in the dict:

"accountId": <numbers>
"emailAddress": "<emailaddress>",
"displayName": <name>

 

arent whats causing the 400 due to  the emailaddress key being present before the display name?

Im simply guessing here, as Im not that experienced in python. 

 

Troubleshooting 101 :facepalm:

I ran the default API DELETE user command and got an error message:

"errorMessages":["Cannot delete user '1234xyza' because 1 issues were reported by this person."],"errors":{}}

 

Should put in an output for any errorMessages to the console to help with these errors when running your script.

Prince Nyeche
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.
July 10, 2022

Hey @Robert Rodrigues 

Glad to know you've gotten a clearer message of the error by returning the response content. Usually the response content should contain additional information about the error and why it happens.

Robert Rodrigues July 11, 2022

Indeed, except when running your script, there is no indication of what the error pertains to, other than error response 400.

It would be good to log the response, rather than check to see if the string of <300 is returned.

Like Prince Nyeche likes this
Prince Nyeche
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.
July 11, 2022

That's true. Definitely an approach I'm taking with all my new scripts and functions as proper error handling actually helps to point out without a doubt where the problem lies. Thanks for the insight.

Robert Rodrigues July 12, 2022

No worries! Your script is immensely helpful, so thank you!

Robert Rodrigues July 19, 2022

Hey @Prince Nyeche 

Quick one:

Im now seeing the error:

 for name in search:
TypeError: 'int' object is not iterable

Im unsure if this is to do with the jiraone library or the fact that the result coming back form the API is now changed but when I check to see what the variable search is, it returns 0, which would explain the error.

The question is, without diving further into the unknown here is, do we know if the USER function inside jiraone is no longer handling the results from the API correctly?

Ive checked my users exist and are present, and match the "customer" and active=true arguments.

Any ideas?

Prince Nyeche
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.
July 19, 2022

Hey @Robert Rodrigues 

The USER.search_user() method does an exact string match of the displayName. It does not use any regular expression to pin point potential matches. The reason it returns 0 is because it couldn't find the user from the string used. Most probably, the string that is being used as the search term isn't an exact match with what the displayName actually is. Please note that for an exact match, this includes any character that depicts the displayName of the user including space characters.

Like Robert Rodrigues likes this
Robert Rodrigues July 19, 2022

Gotcha. 

Have worked it out. I was feeding email address instead of displayName, which I'd incorrectly assumed going from example one to two, would also allow email_address!

Thanks for highlighting.

Robert Rodrigues July 27, 2022

hey @Prince Nyeche 

Ive stumbled across an error when feeding in bigger list.

Traceback (most recent call last):
File "/Users/rrodrigues/gitrepos/it-infra/scripts/jira-customer-delete/delete_customers_bulk.py", line 13, in <module>
search = USER.search_user(names, pull="active", user_type="customer", skip=True)
File "/usr/local/lib/python3.9/site-packages/jiraone/reporting.py", line 1475, in search_user
f = CheckUser._make(_)
File "/usr/local/Cellar/python@3.9/3.9.13_1/Frameworks/Python.framework/Versions/3.9/lib/python3.9/collections/__init__.py", line 441, in _make
raise TypeError(f'Expected {num_fields} arguments, got {len(result)}')
TypeError: Expected 4 arguments, got 3

Im assuming this relates to the line:

USER.search_user(names, pull="active", user_type="customer", skip=True)

Is pulling, in this instance, one less item for a record. Havent seen this throughout the testing Ive done and Ive checked the users are coming back with active and customer when you search for them individually through the API.

 

Any ideas?

Prince Nyeche
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.
July 28, 2022

Yes, it seems to have one less item in the record at that point in the code. A header is created to ensure what is received matches the header column item. I think the problem might lie on the list_user variable that's reading the user list within the USER.search_user method. You should see a folder and CSV file created automatically in your directory once you encounter this error, as that file should contain the users. Then can you examine the actual file that's being read? See if any of the rows are 3 instead of 4.

Robert Rodrigues July 28, 2022

Yup - have worked it out. In the reporting.py file, the variable f is storing 4 arguments, but we're not defining those in the expected dict.

We're missing the reading of account_type from this.

 

I fixed it by adding the following line to the for loop on line 1474:

account_type = f.account_type

and then appended it to the OrderedDict in both if isInstance

"accountType": account_type,
self.user_list.append(OrderedDict({"accountId": get_user, "accountType": account_type,
"displayName": display_name, "active": status})) 

That seems to have rectified the issue. Not sure why I could run the script before without having this error.

Confirmed by opening that CSV, adding that code and then reopening the csv which now has the accountType column heading. Below is the headings before I amended with above code.

Screenshot 2022-07-28 at 12.52.06.png

Prince Nyeche
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.
July 28, 2022

I don't think that was the cause. The for loop is suppose to receive the data after being read from the file and the file is written by searching get_all_users method. Looking at the file you've shown, it seems that the failure occurs when trying to read row 2. For some reason, the file itself is mutated with a header. Now I remember, the reason I added skip=True argument is because of this reason, I never got around fully resolving this issue just added a workaround. Although, in your instance it wrote the header twice, that why this problem happened. Please can you create a bug here with the steps you did? So I can add this to the next releases or the one after that.

The main cause is that a header is written to the file which shouldn't happen with the get_all_users.I think this is because of the ordered dict class but I need to trace why and how it can mutate a file that's already saved.

Prince Nyeche
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.
July 28, 2022

Okay, I think I know the reason why.Since the call to USER.search_user happens consecutively, there's a rewrite of the file and the self.user_list stores all the entries. Somehow it takes the keys of ordered dict values and appends that when rewriting the file hence this problem. Changing the code as a workaround is okay but I need to fix the behaviour of that method as that's not how it should function. It shouldn't be updating the file even with multiple calls to it. The exception is only when there's a difference in user size.

Prince Nyeche
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.
July 29, 2022

Hey @Robert Rodrigues I did a patch of the code which you can use instead rather than rewriting the headers as the headers shouldn't be written to the file and the call shouldn't be updating the list all the time as that takes time when you're running larger dataset. With this behaviour, it creates the file once when performing a bigger list. Now you can do away with this line

Before:

USER.search_user(names, pull="active", user_type="customer", skip=True)

After:

USER.search_user(names, pull="active", user_type="customer")

 I still have other features which I'm bringing, so waiting to complete that first before pushing to a new version, thought I fix this since it's something I should have fixed but forgotten about it down the road.

Robert Rodrigues July 29, 2022

Ah great!

For clarity, do I replace my local copy of reporting.py with your highlighted 1447 line & the code under the After heading?

Prince Nyeche
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.
July 29, 2022

Yes, that's it.

Comment

Log in or Sign up to comment
TAGS
AUG Leaders

Atlassian Community Events