Forums

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

Migrate Portal Only Customer to Atlassian With Automation

Adam Kassoff
Contributor
April 17, 2025

Just as the title says, I am looking to automate the process of migrating a JSM Portal Only Customer to an Atlassian Account. 

This feature request is 6 years old and was wondering if anyone had any workarounds. 

2 answers

3 votes
Darryl Lee
Community Champion
April 18, 2025

Ohey, so I happened to meet @Adam Kassoff at Team 25 last week and he told me about what he was trying to do, so I had a little bit of a head start on this one.

Yes, @Christopher Yen that endpoint is pretty close to what you need, but I had to do a little more digging with Chrome's handy dandy Developer Tools to observe the calls being made when I manually followed the steps here to Migrate a portal-only customer to Atlassian account.

Here's the curl command that I extracted from that snooping:

% curl -n 'https://admin.atlassian.com/gateway/api/ex/jira/{{cloudId}}/rest/servicedesk/customer-management/noeyeball/1/local-servicedesk-user/bulk-migrate-to-atlassian-account' \
-X 'PUT' -H 'content-type: application/json' --data-raw '{"customerAccountIds":["{{reporter.accountId}}"]}'

  • You can find your {{cloudId}} by going to https://<my-site-name>.atlassian.net/_edge/tenant_info
  • {{reporter.accountId}} would be coming from your Automation rule

[One note: -n means curl is reading my ~/.netrc file that contains my login (email address), and password (API token). To use this call in Automation you need to do base64 login:password. This is all detailed here: Send web request using Jira REST API]

So, the interesting (and weird) thing is that I was able to make this call using a regular API token for my account. Now, granted, I am an Org Admin, but ideally you'd follow good security practices like @Rebekka Heilmann _viadee_ describes here and create a dedicated service account (with the same rights) and use it to create a token. (And once you've confirmed the whole Web Request to talk to the API thing is working, you'll want to make sure to hide the Authorization header.)

What's weird about it is that the endpoint is on admin.atlassian.com, and other unofficial, undocumented APIs on that same host, did not accept any kind of token, either regular or the Organization API keys, and much to @Harrison Ponce 's dismay, I suggested putting Session Cookies (which expire after 30 days) in the API Call. Needless to say this is a BAD thing to do from a security policy.

All this is to say, the Migrate customer->account endpoint accepts regular tokens, and this is a good thing.

Darryl Lee
Community Champion
April 18, 2025

Oh, and so, what this would look like as an Automation rule is something like this. (You'd probably have a real trigger with a real condition like "When this ticket gets moved into the Converted status or something")

image.png

So, I've only tested my curl command, not the Automation bits, so please give it a whirl, and let us know how it goes.

Adam Kassoff
Contributor
April 26, 2025

Time for me to share what I was working on that needed this api call. I have my customer base in JSM Assets, which the company, user, department, application, devices as objects in the scheme. 

I have user's account IDs as an attribute on the user object to then associate with the company to make the forms dynamic based on some of the other Assets that all reference back to the company. 

Some users have "elevated" permissions by making them a confluence guest to collaborate and share what we have for the customer in assets through confluence macros. 

To automate the migration of the customers from portal only to atlassian accounts, add them to the guest group. write back to Assets with the new ID I wrote a script to handle multiple companies at once with look ups in assets.

Here is that script. The final piece which i still haven't figured out is if it is possible to automate adding the now guest to their specific confluence space. The space is already stored as an attribute on the Company Asset object. 

 

import requests

# Constants
JIRA_API_BASE_URL = "https://admin.atlassian.com/gateway/api/ex/jira"
ASSETS_API_BASE_URL = "https://api.atlassian.com"
CLOUD_ID = "<CLOUD_ID>" # Replace with your cloud ID
ATLASSIAN_GROUP_ID = "<ATLASSIAN_GROUP_ID>" # Replace with your group ID
API_TOKEN = "<API_TOKEN>" # Replace with your API token
HEADERS = {
"Authorization": f"Basic {API_TOKEN}",
"Accept": "application/json",
"Content-Type": "application/json"
}

def get_users_by_company_key(company_key):
"""Fetch users where Company = Key and (Xboarder = true or IS_Admin = true)."""
print(f"Fetching users for company key: {company_key}")
workspace_id = "<WORKSPACE_ID>" # Replace with your actual workspace ID
url = f"{ASSETS_API_BASE_URL}/jsm/assets/workspace/{workspace_id}/v1/object/aql"
payload = {
"qlQuery": f'objectTypeID = 4 AND Company = "{company_key}" AND (Xboarding = true OR IS_ADMIN = true)'
}
response = requests.post(url, headers=HEADERS, json=payload)
response.raise_for_status()
return response.json().get("values", [])

def get_customer_portal_id(user):
"""Fetch the customer portal ID (Attribute ID = 91) for a user."""
attributes = user.get("attributes", [])
for attribute in attributes:
if attribute.get("objectTypeAttributeId") == "91": # Match Attribute ID = 91
values = attribute.get("objectAttributeValues", [])
if values:
return values[0].get("value") # Extract the 'value' field
return None

def migrate_user_to_atlassian_account(account_id):
"""Send a web request to migrate the user to an Atlassian account."""
print(f"Migrating user with account ID: {account_id}")
url = f"{JIRA_API_BASE_URL}/{CLOUD_ID}/rest/servicedesk/customer-management/noeyeball/1/local-servicedesk-user/migrate-to-atlassian-account-user/{account_id}"
response = requests.put(url, headers=HEADERS)
response.raise_for_status()
print(f"Migration response: {response.json()}")
return response.json()

def update_asset_object_with_new_id(asset_id, new_id):
"""Update the asset object with the new ID."""
print(f"Updating asset object ID: {asset_id} with new ID: {new_id}")
workspace_id = "<WORKSPACE_ID>" # Replace with your actual workspace ID
url = f"{ASSETS_API_BASE_URL}/jsm/assets/workspace/{workspace_id}/v1/object/{asset_id}"
payload = {
"attributes": [
{
"objectTypeAttributeId": "91", # Replace with the correct attribute ID
"objectAttributeValues": [
{
"value": new_id
}
]
}
]
}
response = requests.put(url, headers=HEADERS, json=payload)
response.raise_for_status()

def add_user_to_group(account_id):
"""Add the user to the Atlassian group using a different API key."""
print(f"Adding user with account ID: {account_id} to group.")
headers = {
"Authorization": f"Basic {API_TOKEN}",
"Content-Type": "application/json"
}
url = f"https://<YOUR_DOMAIN>.atlassian.net/rest/api/3/group/user?groupId=<GROUP_ID>" # Replace with your domain and group ID
payload = {
"accountId": f"{account_id}"
}
response = requests.post(url, headers=headers, json=payload)
response.raise_for_status()

def main():
company_keys = input("Enter the Company Assets Object Keys (comma-separated): ")
company_keys = [key.strip() for key in company_keys.split(",")] # Split and clean input
print(f"Company Keys entered: {company_keys}")

for company_key in company_keys:
print(f"Processing company key: {company_key}")
users = get_users_by_company_key(company_key)
if not users:
print(f"No users found for the company key: {company_key}")
continue
for user in users:
customer_portal_id = get_customer_portal_id(user)
if customer_portal_id:
print(f"Found Customer Portal ID: {customer_portal_id}")
# Migrate user to Atlassian account
migration_response = migrate_user_to_atlassian_account(customer_portal_id)
aa_user_account_id = migration_response.get("aaUser", {}).get("accountId")
if aa_user_account_id:
print(f"Migrated user to Atlassian account. New Account ID: {aa_user_account_id}")
# Ensure the correct asset_id is used
asset_id = user.get("id") # Assuming 'id' is the asset ID
if asset_id:
update_asset_object_with_new_id(asset_id, aa_user_account_id)
print(f"Updated asset object {asset_id} with new Atlassian account ID.")
# Add user to the group
add_user_to_group(aa_user_account_id)
print(f"Added user {aa_user_account_id} to the Atlassian group.")
else:
print("Asset ID not found for the user.")
else:
print("Migration failed or no Atlassian account ID returned.")
else:
print("No Customer Portal ID found for this user.")

if __name__ == "__main__":
main()
Like Darryl Lee likes this
Darryl Lee
Community Champion
April 26, 2025

This looks awesome @Adam Kassoff  - thank you for sharing!

So I poked around "adding the now guest to their specific confluence space" with Developer Tools, and found this endpoint, which, thank Gord, allows standard API tokens for authentication. (So yay, the same Basic Authorization header, along with content-type: application/json that you have now should work!) 

https://YOURSITE.atlassian.net/cgraphql?q=UpdateSpacePermissionsPrincipals

OH NO, it's my nemesis, GraphQL! But at least the POST request seems fairly straightforward if you can parse out all of those awful newlines:

[{"operationName":"UpdateSpacePermissionsPrincipals","variables":{"spaceKey":"GUESTSPACE","subjectPermissionDeltasList":[{"subjectKeyInput":{"permissionDisplayType":"GUEST_USER","subjectId":"GUESTUSERDIRECTORYID"},"permissionsToAdd":["VIEW_SPACE"],"permissionsToRemove":[]}]},"query":"mutation UpdateSpacePermissionsPrincipals($spaceKey: String\u0021, $subjectPermissionDeltasList: [SubjectPermissionDeltas\u0021]\u0021) {\\n  updateSpacePermissions(\\n    input: {spaceKey: $spaceKey, subjectPermissionDeltasList: $subjectPermissionDeltasList}\\n  ) {\\n    success\\n    spaceKey\\n    errors {\\n      message\\n      __typename\\n    }\\n    __typename\\n  }\\n}\\n"}]

Happy hacking!

0 votes
Christopher Yen
Community Champion
April 17, 2025

Hi @Adam Kassoff ,

This was posted by Luis Plaza In the comment section of that feature request , may be worth trying out in postman or in automation 

PUT
https://admin.atlassian.com/gateway/api/ex/jira/{{cloudId}}/rest/servicedesk/customer-management/noeyeball/1/local-servicedesk-user/migrate-to-atlassian-account-user/{{accountId}} 

 

Suggest an answer

Log in or Sign up to answer
DEPLOYMENT TYPE
CLOUD
PRODUCT PLAN
PREMIUM
PERMISSIONS LEVEL
Product Admin
TAGS
AUG Leaders

Atlassian Community Events