This article covers how we used the Jira Align API to automate the provisioning and de-provisioning of users in Jira Align based on their Active Directory attributes.
In an enterprise environment, user management is a challenge that is best resolved by automation.
While implementing Jira Align into Atlassian, we met that challenge with Jira Align’s API 2.0 Users endpoint.
!!!! Always test in your Test / UAT / Sandbox environment before implementing changes into your Production environment !!!!
Get a list of all Jira Align users (activated and deactivated)
Get the list of users in the Active Directory group
Compare the lists
If the user account exists in Active Directory group and not in Jira Align
ACTION: create user with a low-level access role
If the user account exists in Active Directory and in Jira Align
Then check if user is deactivated in Jira Align,
If no, then no action
If yes, then ACTION: reactivate user in Jira Align
If the user account does not exist in Active Directory group and does exist in Jira Align
Check if user is active in Jira Align
If yes, ACTION: Deactivate user in Jira Align
If no, no action
Jira Align user accounts have 4 fields that are all needed during user creation but are from a legacy time-tracking feature.
Cost center (costCenterId)
Organization Structure (divisionId)
Region (regionId)
City (cityId)
An Active Directory group.
A low-level role. We called ours “View-Only” (link to come on configuration)
We wanted to start with the simplest provisioning script, with later iterations to refine the script.
Decision on how to set System Role (roleID)
We decided to create accounts with the same low-level access System Role for this script. Stay tuned for Part 2, where we iterate on System Roles.
Decision on how to set Cost center
We decided to set all to the same Cost Center for this script
Decision on how to set Organization Structure
We decided to set all to the same Organization Structure for this script
Decision about how to set Region and City
We decided that regions would be based on our internal regions which were available through a field in our HR system
We decided that cities would be Region - Remote based on the Region as cities were not readily available in our HR system
We used API GET to poll all Jira Align users, API POST requests for creation, and API PATCH requests for modification.
We used Python for our script.
Script essentials:
# This code sample uses the 'requests' library: # http://docs.python-requests.org import requests base_url = "insert_your_Jira_Align_API2_URL" # eg "https://company.jiraalign.com/rest/align/api/2" session = requests.Session() # storing the API bearer token in plain text is a security risk. Be guided by your comany's security policies bearer_token = "insert_your_Jira_Align_API2_token_here" session.headers.update({"Authorization": f"Bearer {bearer_token}"}) # Gets and returns a list of all Jira Align users def get_all_users(): url = f"{base_url}/Users" params = {"$skip": 0, "$select": "id,externalId,isLocked,divisionId,cityId"} users = [] while True: response = session.get(url, params=params) response.raise_for_status() page = response.json() users += page if len(page) != 100: break params["$skip"] += 100 return users # Adds users to Jira Align # user_to_add dictionary is built in the logic of the function perform_full_sync mentioned later def add_user(user_to_add): url = f"{base_url}/Users" # print(f"Adding user {user_to_add['externalId']} with the following params:") # for k, v in user_to_add.items(): # print(f"{k}: {v}") # if input("Enter Y to proceed...").lower() != "y": # raise Exception("String other than 'y' detected, interrupting.") response = session.post(url, json=user_to_add) response.raise_for_status() return response.json() # Disables active users in Jira Align def disable_user(user_id): url = f"{base_url}/Users/{user_id}" # isLocked=-1 means that the user will be deactivated data = [{"op": "replace", "path": "/isLocked", "value": -1}] print(f"Disabling user {user_id}") response = session.patch(url, json=data) response.raise_for_status() # Enables deactivated users in Jira Align def enable_user(user_id): url = f"{base_url}/Users/{user_id}" # isLocked=0 means that the user will be activated # userEndDate is a field set during deactivation. # This parameter clears userEndDate so that the user can login data = [ {"op": "replace", "path": "/isLocked", "value": 0}, {"op": "replace", "path": "/userEndDate", "value": None}, ] print(f"Enabling user {user_id}") response = session.patch(url, json=data) response.raise_for_status() # if users were created by connector, they will be missing divisionId and cityId # we need to add divisionId and cityId before we can enable/disable them # this function adds divisionId and cityId def add_missing_div_city(user_id, divisionId, cityId): url = f"{base_url}/Users/{user_id}" data = [ {"op": "replace", "path": "/divisionId", "value": divisionId}, {"op": "replace", "path": "/cityId", "value": cityId}, ] print(f"Adding divisionId ({divisionId}) and cityId ({cityId}) to user {user_id}") response = session.patch(url, json=data) response.raise_for_status() # Synchronises all users def perform_full_sync(): # ad_users - you will need to get your user list from your Active Directory. # get_ad_users will be YOUR function to get your Active Directory user list. # It will be a list of userIds, such as SAmAccountName that corresponds to # externalId's in Jira Align # if your JA SSO is set to use emails instead of externalId, adapt the code to emails ad_users = get_ad_users() # ja_users we get from Jira Align API ja_users = get_all_users() # changes ja_users from a list to a dictionary ja_users = {user["externalId"]: user for user in ja_users} # output will be a dictionary in the format # { # "user1externalId": { # "id": 1032, # "externalId": "user1externalId", # "isLocked": 0, # }, # "user2externalId": { # "id": 1033, # "externalId": "user2externalId", # "isLocked": 0, # }, # } # if users are in Active Directory but not in Jira Align create the user for username in ad_users: if username not in ja_users: user_to_add = { "externalId": username, # Some of the parameters came from our HR System. # You could take this from Active Directory "firstName": hr_user.preferred_first_name, "lastName": hr_user.preferred_last_name, "title": hr_user.title, "email": hr_user.primary_work_email, # These mandatory region & city parameters were set according to a conversion that we made. "regionId": default_region_id, "cityId": default_city_id, # These mandatory role, cost center and organistion structure were all set to a default for the purposes of our provisioning script. "roleId": default_role_id, "costCenterId": default_cost_center_id, "divisionId": default_division_id, # isExternal=0 means that the user is an Internal User "isExternal": 0, } # Now we use our API to add the user into Jira Align via API add_user(user_to_add) # if users were created by connector, they will be missing divisionId and cityId # we need to add divisionId and cityId before we can enable/disable them # an alternative solution may be to ignore them based on their roleId - if they are an integrated user for username, ja_attrs in ja_users.items(): if not ja_attrs['divisionId'] or not ja_attrs['cityId']: # assign default divisionId and cityId add_missing_div_city(ja_attrs['id'], default_division_id, default_city_id) # Enable deactivated Jira Align users who are in Active Directory group for username, ja_attrs in ja_users.items(): if username in ad_users and ja_attrs["isLocked"] == -1: enable_user(ja_attrs["id"]) # Disable active Jira Align users that are not in Active Directory group for username, ja_attrs in ja_users.items(): if username not in ad_users and ja_attrs["isLocked"] == 0: disable_user(ja_attrs["id"])
Jira Connectors create “Integrated Users” as part of the integration. Some of these users were created without their external ID.
SOLUTION: Check that users in the respective on-premise Jira Software (Server/DC) have their email visibility set to LOGGED IN USERS ONLY
Reference: Jira integration prerequisites
The Jira Connector doesn’t assign divisionId
and cityId
when creating “Integrated Users”. This can prevent PATCH requests from working unless they contain divisionId
and cityId
.
We have included the workaround in the sample code - scan for missing values and add them as needed
Alternatively, these users can be ignored based on their roleId
Some users created prior to setting up the integration required a manual update of their external ID.
This work was performed by a team of awesome people:
@Kirill Duplyakin - Senior Engineer who wrote the code, performed the testing, and implemented the script!
Program Managers, Senior Engineers & Reviewers: @Tim Keyes @Rich Sparks @James McCulley Amber Gagnuss, Dan Eickelman, @Rodrigo Cortez , @Victor Fragoso
Heidi Hendry
Senior Cloud Support Engineer
Atlassian
Sydney, Australia
16 accepted answers
6 comments