Retrieving Entra (Azure AD) Users, Group Membership, and License Assignments
Introduction
This guide demonstrates how to programmatically retrieve a list of all users from Microsoft Entra (Azure AD), including their group memberships and license assignments, using Python and the Microsoft Graph API. The approach is modular, production-ready, and company-agnostic. All code is explained step by step, with constants and endpoints included for clarity.
Prerequisites
- Python 3.8+
- The following Python packages:
requests
msal
(for authentication, not shown here)- An Azure AD application (service principal) with permissions to read users, groups, and licenses
- Secure storage for credentials (e.g., Azure Key Vault)
Constants and Endpoints
AZURE_GRAPH_BETA = 'https://graph.microsoft.com/beta/'
LIST_OF_AAD_USER_ATTRIBUTES = [
'displayName', 'accountEnabled', 'userPrincipalName', 'licenseAssignmentStates',
]
Step 1: Retrieve Users with License Assignments and Group Memberships
Function: get_users_licenseassignments_and_groups
This function retrieves a list of users from Entra (Azure AD), including their license assignment states and group memberships, using the Microsoft Graph API.
def get_users_licenseassignments_and_groups():
user_list = [] # Initialize list to store user data
try:
LIST_OF_AAD_USER_ATTRIBUTES = [
'displayName', 'accountEnabled', 'userPrincipalName', 'licenseAssignmentStates',
]
selected_attributes = ",".join(LIST_OF_AAD_USER_ATTRIBUTES)
query_params = f"$select={selected_attributes}"
query_params += "&$expand=memberOf"
user_list = execute_odata_query_get(f"{AZURE_GRAPH_BETA}users?{query_params}")
except Exception as error:
handle_global_exception(sys._getframe().f_code.co_name, error)
return user_list
Explanation:
- Builds a query to select user attributes and expand group memberships (memberOf
).
- Calls execute_odata_query_get
to make the API request and handle pagination.
- Returns a list of user dictionaries with license and group data.
Step 2: Retrieve All Groups
Function: get_list_of_groups
This function retrieves all groups from Entra (Azure AD).
def get_list_of_groups():
group_list = [] # Initialize list to store group data
try:
group_list = execute_odata_query_get(f"{AZURE_GRAPH_BETA}groups?")
except Exception as error:
handle_global_exception(sys._getframe().f_code.co_name, error)
return group_list
Explanation:
- Calls the Microsoft Graph /groups
endpoint to retrieve all groups.
- Uses execute_odata_query_get
for API calls and pagination.
- Returns a list of group dictionaries.
Step 3: Combine User, License, and Group Data
Function: get_users_with_license_and_groups
This function combines user, license, and group data into a unified list.
def get_users_with_license_and_groups():
final_list = [] # Final list of dictionaries
SKU_MAPPING = {
"ee02fd1b-340e-4a4b-b355-4a514e4c8943": "Exchange Online Archiving",
"05e9a617-0261-4cee-bb44-138d3ef5d965": "365 E3",
# ... (other SKU mappings) ...
}
try:
users = get_users_licenseassignments_and_groups()
groups = get_list_of_groups()
for user in users:
# Extract license assignments and group memberships
license_assignments = []
group_memberships = []
# Parse license assignments
for license in user.get('licenseAssignmentStates', []):
sku_id = license.get('skuId')
sku_name = SKU_MAPPING.get(sku_id, sku_id)
license_assignments.append({
'sku_id': sku_id,
'sku_name': sku_name,
'assigned_by_group': license.get('assignedByGroup', None),
})
# Parse group memberships
for group in user.get('memberOf', []):
group_memberships.append({
'group_id': group.get('id'),
'display_name': group.get('displayName'),
})
final_list.append({
'user_principal_name': user.get('userPrincipalName'),
'display_name': user.get('displayName'),
'account_enabled': user.get('accountEnabled'),
'license_assignments': license_assignments,
'group_memberships': group_memberships,
})
except Exception as error:
handle_global_exception(sys._getframe().f_code.co_name, error)
return final_list
Explanation: - Calls the previous two functions to get users and groups. - Maps license SKUs to human-readable names. - Extracts and structures license and group membership data for each user. - Returns a list of user dictionaries with all relevant information.
Step 4: High-Level Orchestration
Function: get_list_of_users_with_license_and_groups
This function orchestrates the retrieval and structuring of user, license, and group data.
def get_list_of_users_with_license_and_groups():
user_groups = []
user_license = []
user_lic_and_groups = get_users_with_license_and_groups()
for user in user_lic_and_groups:
for membership in user['group_memberships']:
user_groups.append({
'user_principal_name': user['user_principal_name'],
'group_id': membership['group_id'],
'group_display_name': membership['display_name'],
})
for licassignment in user['license_assignments']:
user_license.append({
'user_principal_name': user['user_principal_name'],
'sku_id': licassignment['sku_id'],
'sku_name': licassignment['sku_name'],
'assigned_by_group': licassignment['assigned_by_group'],
})
return user_groups, user_license
Explanation:
- Calls get_users_with_license_and_groups
to get the unified user data.
- Flattens group memberships and license assignments into separate lists for easy processing or storage.
- Returns two lists: one for user-group relationships, one for user-license assignments.
Supporting Function: execute_odata_query_get
This function is used throughout to make authenticated, paginated requests to the Microsoft Graph API.
def execute_odata_query_get(urltoInvoke, token=''):
try:
localUserList = []
if not token:
acesstokenforClientapp = get_access_token_API_Access_AAD()
else:
acesstokenforClientapp = token
continueLooping = True
while continueLooping:
response = requests.get(
url=urltoInvoke,
headers={'Authorization': f'Bearer {acesstokenforClientapp}'}
)
if response.status_code == 429: # Throttling response
retry_after = int(response.headers.get("Retry-After", 5))
print(f"Throttled! Retrying after {retry_after} seconds...")
time.sleep(retry_after)
continue
if response.status_code == 401: # Token expired or invalid
print("Token expired or invalid. Fetching a new one...")
acesstokenforClientapp = get_access_token_API_Access_AAD()
response = requests.get(
url=urltoInvoke,
headers={'Authorization': f'Bearer {acesstokenforClientapp}'}
)
if response.status_code != 200:
response.raise_for_status()
if response.status_code == 403:
raise Exception(f"403 Forbidden: Access denied for URL {urltoInvoke}")
if response.status_code != 200:
response.raise_for_status()
graph_data = response.json()
localUserList.extend(graph_data.get('value', []))
if "@odata.nextLink" in graph_data:
urltoInvoke = graph_data["@odata.nextLink"]
else:
continueLooping = False
return localUserList
except Exception as e:
if hasattr(e, 'args') and e.args and '403 Forbidden' in str(e.args[0]):
raise
handle_global_exception(sys._getframe().f_code.co_name, e)
Explanation: - Handles authentication, pagination, throttling, and error handling for Microsoft Graph API requests. - Used by all higher-level functions to retrieve data from Entra (Azure AD). For a deep dive into certificate-based authentication setup, see the dedicated article: Certificate Based Authorization for Azure AD.
Conclusion
By following this step-by-step approach, you can programmatically retrieve all users from Entra (Azure AD), along with their group memberships and license assignments, using Python and the Microsoft Graph API. The modular design allows for easy extension and integration into enterprise automation workflows.
For more details, see the Microsoft Graph API documentation.