Hi everyone,
I am trying to generate a comprehensive audit report of all the groups within our Atlassian organization. Specifically, I need a list that shows:
Every group (including legacy or empty groups that currently have 0 users).
The users inside those groups.
The Apps/Product Access assigned to each group (e.g., Jira Software, Confluence, Jira Service Management).
What I've tried so far:
UI User Export: Running a user export from admin.atlassian.com gives me the apps and groups, but it only shows groups that currently have active users assigned to them. Empty groups are completely left out of this report.
admin.atlassian.com
Organization API v2: I tried writing a script using the (<a href="https://api.atlassian.com/admin/v2/" rel="noopener nofollow noreferrer" target="_blank">https://api.atlassian.com/admin/v2/</a>) endpoints (specifically /groups/search with expansions). I had generated an personal API endpoint. I have site admin role so i can access admin.atlassian.combut when i query it via the script i get error saying "401 Unauthorized error". I was doing bearer authorization but ofcrouse my token is personal. I can query this endpoint though using basic authentication, my email ID, site name and the api token.
(<a href="https://api.atlassian.com/admin/v2/" rel="noopener nofollow noreferrer" target="_blank">https://api.atlassian.com/admin/v2/</a>)
/groups/search
{JIRA_URL}/rest/api/3/group/bulk
Why is this failing?As a site admin can I not consume the the api.atlassian.com endpoint via basic authentication??Do i need to get a bearer API token?
Two different auth planes, and that's the whole of it. Calls to your site host take basic auth with your email plus a personal API token, which is why group/bulk works for you. Point that same token at api.atlassian.com/admin and it dies both ways: basic gives you 401, bearer gives you 401. That host wants an Admin API key, a separate credential you create at admin.atlassian.com under Settings, API keys, and then send as a Bearer token. Worth knowing org admin and site admin are distinct roles up there too, so if that page isn't showing you anything, that's your next thread to pull.
Better news. Your three requirements don't need that API at all.
Every group including the empty ones is GET /rest/api/3/group/bulk on basic auth, the credential you already have working. It lists groups whether or not anyone is in them, which is the exact thing the UI export drops on the floor. Members per group is GET /rest/api/3/group/member?groupId={groupId}.
Product access by group is GET /rest/api/3/applicationrole. It arrives inverted, product by product with the groups listed underneath, so you flip it in the script. Cheap to do. That endpoint is Jira's own though, which means Software and Service Management land in it while Confluence access doesn't. For the Confluence half you're back at the admin API with a real key.
You need to use a bearer token to access the Admin API; a personal API token or basic authentication won't work. That's why you're getting the 401 Unauthorized error.
Hi Sana,
Gabriela already put the finger on the real problem - the two auth systems. Worthrestating in one line because it catches almost everyone: <site>.atlassian.net/rest/api/3/...takes your email plus a personal API token as basic auth, while api.atlassian.com/admin/...takes an organisation API key as a Bearer token. They are not interchangeable, and a401 is exactly what it looks like when you mix them.
One thing that has not been mentioned, and it may save you the script entirely. Ifwhat you need is an audit deliverable rather than an automated feed, go toadmin.atlassian.com > Directory > Users and use Export users. The CSV carries productaccess and group membership as columns, in one file, for the whole org. That coverstwo of your three requirements immediately.
What it does not give you is empty groups - a group with no members produces no rows -so you would still need /rest/api/3/group/bulk for those. But that is a much smallerjob than building the whole report from scratch.
If you do script it, three things that will otherwise make the report quietly wrong:
- /rest/api/3/group/member includes deactivated users by default. PassincludeInactiveUsers=false unless you want them, because deactivated accounts inflateevery group count and an auditor will ask why the numbers do not match.
- Both /group/bulk and /group/member are paginated. It is easy to end up with aclean-looking result that is really just the first page.
- Group membership in Jira Cloud is flat - there is no group nesting, so a member is amember and you do not need to resolve any hierarchy. That is good news for an auditreport, and worth stating explicitly if you have to document your methodology.
One caveat on the product access part: /rest/api/3/applicationrole gives you whichgroups grant access to which product, which is the mapping you want. But a person canhold a seat through any one of several groups, so "does this user have Jira access" isa union across their groups rather than a single lookup. If you go the CSV route, thatis already resolved for you.
Is this a one-time audit, or something you will need to re-run regularly? That changeswhich of the two routes is worth the effort.
@Dejan V _DVLS Labs_ has the right call for a one-time audit, and he's right that empty groups are the hole the CSV leaves.
One flag on the parameter though, because it points the error the wrong way. /rest/api/3/group/member excludes deactivated users by default. includeInactiveUsers is documented as default false, and it's false in the v3 spec I pulled this morning, so passing false changes nothing and what you're exposed to isn't inflated counts, it's the opposite. Group membership comes back smaller than admin.atlassian.com shows, and the difference is every deactivated account still sitting in a group. Pass includeInactiveUsers=true if the numbers have to reconcile against the directory. If they don't, put a line in the methodology saying the counts are active users only, because that gap is the first thing an auditor asks about.
You are right, @Gabriela - LeanZero, and thank you for catching it - I had that parameterbackwards. The default is false, so deactivated accounts are already excluded andpassing false changes nothing. The exposure is the opposite of what I described:the API returns fewer members than admin.atlassian.com does, and the difference isdeactivated accounts still sitting in groups.
@sana_faiyaz, take her version, not mine.
One thing worth adding on top of it, because it turns that gap from a nuisance intosomething useful. Deactivating an account does not remove it from any group - themembership stays exactly where it was. So the difference between the two calls isnot only a reconciliation artefact, it is a list: every dormant grant that wouldcome back the moment someone reactivates that account.
That is worth looking at deliberately, especially for the privileged groups. Adeactivated account still sitting in jira-administrators is a finding on its own,and it is invisible in an active-users-only report.
So in practice I would run it both ways: the default call for "who can log intoday", and includeInactiveUsers=true for "what grants still exist". The delta isyour cleanup list - and writing it down answers the reconciliation question beforean auditor asks it.
That dormant-grant framing is the better use of it, and you can get there in one call rather than two. Every member object /rest/api/3/group/member returns carries an active flag, and the v3 spec's own example for that endpoint shows active true and active false sitting side by side in the same response. So run it once with includeInactiveUsers=true and filter on active false. No delta arithmetic. On the paging you flagged, maxResults caps at 50 on this one, so a script that reads a single page reads fifty members and looks finished.
That is better, and simpler - one call and a filter beats two calls andarithmetic. The active flag is right there in each member object, so there is noreason to do it the way I described.
On the 50 cap, one thing worth adding for anyone scripting this. The endpointreturns isLast, so that is the condition to loop on - comparing page size againstmaxResults is fragile. But the failure mode that bites hardest is a loop thatnever advances startAt at all. It keeps re-reading page one, and it looks exactlylike a correct single-page result, just with duplicates. Easy to write, hard tonotice, and it only surfaces on an instance large enough to have a second page.
Thanks for both corrections - this thread ended up more useful than my originalanswer was.
The empty-groups gap is real: the admin export only lists groups with active users, and /rest/api/3/group/bulk is the one call that returns every group including empty ones (paginate with startAt). Product access per group comes from /rest/api/3/applicationrole (groups and defaultGroups per role), members from /rest/api/3/group/member?groupId=…&includeInactiveUsers=true. Stitching those three gives you the roster you describe.
If you then also need to know where each group is used (schemes, roles, filters, dashboards, product access) before you clean up: that is a per-group question, and it is what I built a read-only Marketplace app for — disclosure: I'm the developer. It will not give you the org-wide roster in one go, so for your first step the three calls above are the better tool. https://marketplace.atlassian.com/apps/1133367000
It looks like you're new here. Sign in or register to get started.