Jira Align API User Management Part 1: How we used API for user provisioning in Atlassian

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 !!!!

 

 

Steps that the script follows:

  1. Get a list of all Jira Align users (activated and deactivated)

  2. Get the list of users in the Active Directory group

  3. Compare the lists

  4. If the user account exists in Active Directory group and not in Jira Align

    1. ACTION: create user with a low-level access role

  5. If the user account exists in Active Directory and in Jira Align

    1. Then check if user is deactivated in Jira Align,

      1. If no, then no action

      2. If yes, then ACTION: reactivate user in Jira Align

  6. If the user account does not exist in Active Directory group and does exist in Jira Align

    1. Check if user is active in Jira Align

      1. If yes, ACTION: Deactivate user in Jira Align

      2. If no, no action


Getting Ready

Things to be aware of

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)

 

Additional Pre-requisites

  1. An Active Directory group.

  2. A low-level role. We called ours “View-Only” (link to come on configuration)

  3. The API key is set up for a Jira Align user with Super Admin permissions in Jira Align

 

Decisions

We wanted to start with the simplest provisioning script, with later iterations to refine the script.

  1. Decision on how to set System Role (roleID)

    1. 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.

  2. Decision on how to set Cost center

    1. We decided to set all to the same Cost Center for this script

  3. Decision on how to set Organization Structure

    1. We decided to set all to the same Organization Structure for this script

  4. Decision about how to set Region and City

    1. We decided that regions would be based on our internal regions which were available through a field in our HR system

    2. We decided that cities would be Region - Remote based on the Region as cities were not readily available in our HR system


Script

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"])

Challenges that we encountered and resolved

  • 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:

  • 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 

5 comments

Tim Keyes
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
January 30, 2023

Thank you! This will be a huge help to our customers!

Like Scott Willard likes this
Rae Gibbs
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
February 1, 2023

@Heidi Hendry Fantastic article! Thanks for writing this up!

Niyazi Fellahoglu
Atlassian Team
Atlassian Team members are employees working across the company in a wide variety of roles.
February 3, 2023

Really helpful document and code. Thank you, @Heidi Hendry  and @Kirill Duplyakin

Adnan Aziz March 23, 2023

Beneficial guide. Thank you.

DL January 3, 2024

Hi,

Is it possible to use this script to activate/deactivate both Full and Integrated users? If so, how can it be done?

Comment

Log in or Sign up to comment
TAGS
AUG Leaders

Atlassian Community Events