mirror of
https://github.com/netbox-community/netbox.git
synced 2024-05-10 07:54:54 +00:00
11508 Add group assignments for Azure SSO (#13373)
* 11508 temp azure changes * 11508 map AzureAD groups to NetBox groups * 11508 add is_active, reset superuser and staff based on Azure * 11508 remove is_active, add documentation use azuread * 11508 remove addition to settings * 11508 review changes, add additional logging and error checking * 11508 review changes, remove extra flag * 11508 review changes, change SOCIAL_AUTH_ to REMOTE_AUTH_BACKEND * 11508 clear user groups * 11508 clear user groups * 11508 review feedback change config key * 11508 review changes * 11508 review changes - add error checking * 11508 review changes - flexible config params
This commit is contained in:
@ -61,6 +61,63 @@ Restart the NetBox services so that the new configuration takes effect. This is
|
||||
sudo systemctl restart netbox
|
||||
```
|
||||
|
||||
## Group Assignment
|
||||
|
||||
If you want NetBox to assign groups based on Azure AD groups, then some additonal configuration is needed. Enter the following configuration parameters in `configuration.py`, substituting your own values:
|
||||
|
||||
```python
|
||||
SOCIAL_AUTH_AZUREAD_OAUTH2_RESOURCE = 'https://graph.microsoft.com/'
|
||||
SOCIAL_AUTH_PIPELINE = (
|
||||
'social_core.pipeline.social_auth.social_details',
|
||||
'social_core.pipeline.social_auth.social_uid',
|
||||
'social_core.pipeline.social_auth.social_user',
|
||||
'social_core.pipeline.user.get_username',
|
||||
'social_core.pipeline.social_auth.associate_by_email',
|
||||
'social_core.pipeline.user.create_user',
|
||||
'social_core.pipeline.social_auth.associate_user',
|
||||
'netbox.authentication.user_default_groups_handler',
|
||||
'social_core.pipeline.social_auth.load_extra_data',
|
||||
'social_core.pipeline.user.user_details',
|
||||
'netbox.authentication.azuread_map_groups',
|
||||
)
|
||||
|
||||
# Define special user types using groups. Exercise great caution when assigning superuser status.
|
||||
SOCIAL_AUTH_PIPELINE_CONFIG = {
|
||||
'AZUREAD_USER_FLAGS_BY_GROUP': {
|
||||
"is_staff": ['{AZURE_GROUP_ID1}','{AZURE_GROUP_ID2}'],
|
||||
"is_superuser": ['{AZURE_GROUP_ID1}','{AZURE_GROUP_ID2}']
|
||||
},
|
||||
|
||||
'AZUREAD_GROUP_MAP': {
|
||||
'{AZURE_GROUP_ID1}': '{NETBOX_GROUP1}',
|
||||
'{AZURE_GROUP_ID2}': '{NETBOX_GROUP2}',
|
||||
}
|
||||
|
||||
}
|
||||
```
|
||||
|
||||
For example, here is a config that maps a single Azure AD group (the token '1a36bed9-3bdc-4970-ab66-faf9704e0af4' shown here is the ID of the group within the Azure dashboard) to be both is_staff and is_superuser status as well as assign it to the group 'tgroup' within NetBox:
|
||||
|
||||
```
|
||||
SOCIAL_AUTH_PIPELINE_CONFIG = {
|
||||
# Define special user types using groups. Exercise great caution when assigning superuser status.
|
||||
'AZUREAD_USER_FLAGS_BY_GROUP': {
|
||||
'is_staff': ['1a36bed9-3bdc-4970-ab66-faf9704e0af4',],
|
||||
'is_superuser': ['1a36bed9-3bdc-4970-ab66-faf9704e0af4',]
|
||||
},
|
||||
|
||||
'AZUREAD_GROUP_MAP': {
|
||||
'1a36bed9-3bdc-4970-ab66-faf9704e0af4': 'tgroup',
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**AZUREAD_USER_FLAGS_BY_GROUP.is_staff**: users who are in any of the Azure AD group-ids in the array will have staff permission assigned to them.
|
||||
|
||||
**AZUREAD_USER_FLAGS_BY_GROUP.is_superuser**: users who are in any of the Azure AD group-ids in the array will have superuser permission assigned to them.
|
||||
|
||||
**AZUREAD_GROUP_MAP**: Any user with the given Azure AD group-id is included in the given NetBox group name.
|
||||
|
||||
## Testing
|
||||
|
||||
Log out of NetBox if already authenticated, and click the "Log In" button at top right. You should see the normal login form as well as an option to authenticate using Azure AD. Click that link.
|
||||
|
@ -1,4 +1,5 @@
|
||||
import logging
|
||||
import requests
|
||||
from collections import defaultdict
|
||||
|
||||
from django.conf import settings
|
||||
@ -386,3 +387,94 @@ def user_default_groups_handler(backend, user, response, *args, **kwargs):
|
||||
user.groups.add(*group_list)
|
||||
else:
|
||||
logger.info(f"No valid group assignments for {user} - REMOTE_AUTH_DEFAULT_GROUPS may be incorrectly set?")
|
||||
|
||||
|
||||
def azuread_map_groups(response, user, backend, *args, **kwargs):
|
||||
'''
|
||||
Map Azure AD group ID to Netbox group
|
||||
Also set is_superuser or is_staff based on config map
|
||||
'''
|
||||
BASE_MICROSOFT_GRAPH_URL = 'https://graph.microsoft.com/v1.0/'
|
||||
logger = logging.getLogger('netbox.auth.azuread_map_groups')
|
||||
|
||||
if not hasattr(settings, "SOCIAL_AUTH_PIPELINE_CONFIG"):
|
||||
raise ImproperlyConfigured(
|
||||
"Azure AD group mapping has been configured, but SOCIAL_AUTH_PIPELINE_CONFIG is not defined."
|
||||
)
|
||||
|
||||
config = getattr(settings, "SOCIAL_AUTH_PIPELINE_CONFIG")
|
||||
if "AZUREAD_USER_FLAGS_BY_GROUP" not in config and "AZUREAD_GROUP_MAP" not in config:
|
||||
raise ImproperlyConfigured(
|
||||
"Azure AD group mapping has been configured, but AZUREAD_USER_FLAGS_BY_GROUP or AZUREAD_GROUP_MAP is not defined."
|
||||
)
|
||||
|
||||
flags_by_group = config.get("AZUREAD_USER_FLAGS_BY_GROUP", {'is_superuser': [], 'is_staff': []})
|
||||
group_mapping = config.get("AZUREAD_GROUP_MAP", {})
|
||||
|
||||
if 'is_staff' not in flags_by_group and 'is_superuser' not in flags_by_group:
|
||||
raise ImproperlyConfigured(
|
||||
"Azure AD group mapping AZUREAD_USER_FLAGS_BY_GROUP is defined but does not contain either is_staff or is_superuser."
|
||||
)
|
||||
|
||||
superuser_map = flags_by_group.get('is_superuser', [])
|
||||
staff_map = flags_by_group.get('is_staff', [])
|
||||
|
||||
access_token = response.get('access_token')
|
||||
headers = {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"Accept": "application/json",
|
||||
'Authorization': f'Bearer {access_token}',
|
||||
}
|
||||
|
||||
try:
|
||||
# Query Microsoft Graph API to get user-id for following API
|
||||
response = requests.get(
|
||||
f'{BASE_MICROSOFT_GRAPH_URL}me',
|
||||
headers=headers,
|
||||
)
|
||||
uid = response.json().get('id')
|
||||
|
||||
# Call Graph API to get groups for current user
|
||||
response = requests.get(
|
||||
f"{BASE_MICROSOFT_GRAPH_URL}users/{uid}/memberOf",
|
||||
headers=headers,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Azure AD group mapping error getting groups for user {user} from Microsoft Graph API: {e}")
|
||||
raise e
|
||||
|
||||
# Set groups and permissions based on returned group list
|
||||
is_superuser = False
|
||||
is_staff = False
|
||||
try:
|
||||
values = response.json().get('value', [])
|
||||
except Exception as e:
|
||||
logger.error(f"Azure AD group mapping error getting groups json response for user {user} from Microsoft Graph API: {e}")
|
||||
raise e
|
||||
|
||||
user.groups.through.objects.filter(user=user).delete()
|
||||
for value in values:
|
||||
# AD response contains both directories and groups - we only want groups
|
||||
if value.get('@odata.type', None) == '#microsoft.graph.group':
|
||||
group_id = value.get('id', None)
|
||||
|
||||
if group_id in superuser_map:
|
||||
logger.info(f"Azure AD group mapping - setting superuser status for: {user}.")
|
||||
is_superuser = True
|
||||
|
||||
if group_id in staff_map:
|
||||
logger.info(f"Azure AD group mapping - setting staff status for: {user}.")
|
||||
is_staff = True
|
||||
|
||||
if group_id in group_mapping:
|
||||
group_name = group_mapping[group_id]
|
||||
try:
|
||||
group = Group.objects.get(name=group_name)
|
||||
group.user_set.add(user)
|
||||
logger.info(f"Azure AD group mapping - adding group {group_name} to user: {user}.")
|
||||
except Group.DoesNotExist:
|
||||
logger.info(f"Azure AD group mapping - group: {group_name} not found.")
|
||||
|
||||
user.is_superuser = is_superuser
|
||||
user.is_staff = is_staff
|
||||
user.save()
|
||||
|
Reference in New Issue
Block a user