mirror of
https://github.com/ansible/awx.git
synced 2026-02-23 14:05:59 -03:30
feat: comprehensive refactor of SSO org/team mapping for Gateway authentication export (#7047)
This commit completely refactors how SSO organization and team mappings are processed
and exported for Gateway authentication, moving from a group-based approach to a more
flexible attribute-based system.
Key Changes:
- Introduced new process_sso_user_list() function for centralized user processing
- Enhanced boolean handling to support both native booleans and string representations
- Added email detection and regex pattern support for flexible user matching
- Refactored trigger generation from groups-based to attributes-based system
Gateway Mapping Enhancements (awx/main/utils/gateway_mapping.py):
- Added email regex detection for automatic email vs username classification
- Added pattern_to_slash_format() for regex pattern conversion
- Enhanced process_sso_user_list() with support for:
- Boolean values: True/False and ["true"]/["false"]
- String usernames and email addresses with automatic detection
- Regex patterns with both username and email matching
- Custom email_attr and username_attr parameters
- Refactored team_map_to_gateway_format() to use new processing system
- Refactored org_map_to_gateway_format() to use new processing system
- Changed trigger structure from {"groups": {"has_or": [...]}} to attribute-based triggers
- Improved naming convention to include trigger type in mapping names
Comprehensive Test Coverage (awx/main/tests/unit/utils/test_auth_migration.py):
- Added complete TestProcessSSOUserList class with 8 comprehensive test methods
- Enhanced TestOrgMapToGatewayFormat with string boolean and new functionality tests
- Enhanced TestTeamMapToGatewayFormat with string boolean and new functionality tests
- Added tests for email detection, regex patterns, and custom attributes
- Verified backward compatibility and integration functionality
- All existing tests updated to work with new attribute-based trigger system
Breaking Changes:
- Trigger structure changed from group-based to attribute-based
- Mapping names now include trigger descriptions for better clarity
- Function signatures updated to include email_attr and username_attr parameters
Co-Authored with Claude-4 via Cursor
Co-authored-by: Peter Braun <pbraun@redhat.com>
This commit is contained in:
committed by
thedoubl3j
parent
4f2d28db51
commit
505ec560c8
File diff suppressed because it is too large
Load Diff
@@ -5,13 +5,93 @@ This module contains functions to convert AWX authentication mappings
|
||||
(organization and team mappings) to AAP Gateway format.
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Union
|
||||
|
||||
def team_map_to_gateway_format(team_map, start_order=1):
|
||||
email_regex = re.compile(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$")
|
||||
|
||||
|
||||
def pattern_to_slash_format(pattern):
|
||||
"""Convert a re.Pattern object to /pattern/flags format."""
|
||||
if not isinstance(pattern, re.Pattern):
|
||||
return str(pattern)
|
||||
|
||||
flags_str = ""
|
||||
if pattern.flags & re.IGNORECASE:
|
||||
flags_str += "i"
|
||||
if pattern.flags & re.MULTILINE:
|
||||
flags_str += "m"
|
||||
if pattern.flags & re.DOTALL:
|
||||
flags_str += "s"
|
||||
if pattern.flags & re.VERBOSE:
|
||||
flags_str += "x"
|
||||
|
||||
return f"/{pattern.pattern}/{flags_str}"
|
||||
|
||||
|
||||
def process_sso_user_list(
|
||||
users: Union[str, list[str], bool], email_attr: str = 'email', username_attr: str = 'username'
|
||||
) -> list[dict[str : Union[str, dict[str : dict[str:str]]]]]:
|
||||
if type(users) is str:
|
||||
users = [users]
|
||||
|
||||
triggers = []
|
||||
if users in [False, ["false"]]:
|
||||
triggers.append(
|
||||
{
|
||||
"name": "Never Allow",
|
||||
"trigger": {"never": {}},
|
||||
}
|
||||
)
|
||||
elif users in [True, ["true"]]:
|
||||
triggers.append({"name": "Always Allow", "trigger": {"always": {}}})
|
||||
else:
|
||||
for user_or_email in users:
|
||||
if isinstance(user_or_email, re.Pattern):
|
||||
user_or_email = pattern_to_slash_format(user_or_email)
|
||||
# If we got a regex it could be either a username or an email object
|
||||
triggers.append(
|
||||
{"name": f"Match Username {user_or_email}", "trigger": {"attributes": {"join_condition": "or", username_attr: {"matches": user_or_email}}}}
|
||||
)
|
||||
triggers.append(
|
||||
{"name": f"Match Email {user_or_email}", "trigger": {"attributes": {"join_condition": "or", email_attr: {"matches": user_or_email}}}}
|
||||
)
|
||||
elif isinstance(user_or_email, str):
|
||||
# If we got a string its a direct match for either a username or an email
|
||||
if email_regex.match(user_or_email):
|
||||
triggers.append(
|
||||
{"name": f"Email Equals {user_or_email}", "trigger": {"attributes": {"join_condition": "or", email_attr: {"equals": user_or_email}}}}
|
||||
)
|
||||
else:
|
||||
triggers.append(
|
||||
{
|
||||
"name": f"Username equals {user_or_email}",
|
||||
"trigger": {"attributes": {"join_condition": "or", username_attr: {"equals": user_or_email}}},
|
||||
}
|
||||
)
|
||||
else:
|
||||
# Convert other objects to string representation and assume it could be either an email or a username
|
||||
# The other option we could take here would be to just error out
|
||||
triggers.append(
|
||||
{
|
||||
"name": f"Username Equals {user_or_email}",
|
||||
"trigger": {"attributes": {"join_condition": "or", username_attr: {"equals": str(user_or_email)}}},
|
||||
}
|
||||
)
|
||||
triggers.append(
|
||||
{"name": f"Email Equals {user_or_email}", "trigger": {"attributes": {"join_condition": "or", email_attr: {"equals": str(user_or_email)}}}}
|
||||
)
|
||||
return triggers
|
||||
|
||||
|
||||
def team_map_to_gateway_format(team_map, start_order=1, email_attr: str = 'email', username_attr: str = 'username'):
|
||||
"""Convert AWX team mapping to Gateway authenticator format.
|
||||
|
||||
Args:
|
||||
team_map: The SOCIAL_AUTH_*_TEAM_MAP setting value
|
||||
start_order: Starting order value for the mappers
|
||||
email_attr: The attribute representing the email
|
||||
username_attr: The attribute representing the username
|
||||
|
||||
Returns:
|
||||
tuple: (List of Gateway-compatible team mappers, next_order)
|
||||
@@ -28,65 +108,39 @@ def team_map_to_gateway_format(team_map, start_order=1):
|
||||
if team['users'] is None:
|
||||
continue
|
||||
|
||||
if team['users'] is False:
|
||||
triggers = {"never": {}}
|
||||
elif team['users'] is True:
|
||||
triggers = {"always": {}}
|
||||
else:
|
||||
import re
|
||||
|
||||
# Handle the case where the value itself is a regex pattern
|
||||
if isinstance(team['users'], re.Pattern):
|
||||
# Convert single regex pattern to string in a list
|
||||
triggers = {"groups": {"has_or": [str(team['users'])]}}
|
||||
else:
|
||||
# Handle list or string values
|
||||
if type(team['users']) is str:
|
||||
team['users'] = [team['users']]
|
||||
|
||||
# Convert any non-string items to strings (e.g., regex patterns)
|
||||
users_list = []
|
||||
for user in team['users']:
|
||||
if isinstance(user, str):
|
||||
users_list.append(user)
|
||||
elif isinstance(user, re.Pattern):
|
||||
# Convert regex patterns to string representation
|
||||
users_list.append(str(user.pattern))
|
||||
else:
|
||||
# Convert other objects to string representation
|
||||
users_list.append(str(user))
|
||||
|
||||
triggers = {"groups": {"has_or": users_list}}
|
||||
|
||||
# Get the organization name
|
||||
organization_name = team.get('organization', 'Unknown')
|
||||
|
||||
# Check for remove flag
|
||||
revoke = team.get('remove', False)
|
||||
|
||||
result.append(
|
||||
{
|
||||
"name": f"{organization_name} - {team_name}",
|
||||
"map_type": "team",
|
||||
"order": order,
|
||||
"authenticator": -1, # Will be updated when creating the mapper
|
||||
"triggers": triggers,
|
||||
"organization": organization_name,
|
||||
"team": team_name,
|
||||
"role": "Team Member", # Gateway team member role
|
||||
"revoke": revoke,
|
||||
}
|
||||
)
|
||||
|
||||
order += 1
|
||||
for trigger in process_sso_user_list(team['users'], email_attr=email_attr, username_attr=username_attr):
|
||||
result.append(
|
||||
{
|
||||
"name": f"{organization_name} - {team_name} {trigger['name']}",
|
||||
"map_type": "team",
|
||||
"order": order,
|
||||
"authenticator": -1, # Will be updated when creating the mapper
|
||||
"triggers": trigger['trigger'],
|
||||
"organization": organization_name,
|
||||
"team": team_name,
|
||||
"role": "Team Member", # Gateway team member role
|
||||
"revoke": revoke,
|
||||
}
|
||||
)
|
||||
order += 1
|
||||
|
||||
return result, order
|
||||
|
||||
|
||||
def org_map_to_gateway_format(org_map, start_order=1):
|
||||
def org_map_to_gateway_format(org_map, start_order=1, email_attr: str = 'email', username_attr: str = 'username'):
|
||||
"""Convert AWX organization mapping to Gateway authenticator format.
|
||||
|
||||
Args:
|
||||
org_map: The SOCIAL_AUTH_*_ORGANIZATION_MAP setting value
|
||||
start_order: Starting order value for the mappers
|
||||
email_attr: The attribute representing the email
|
||||
username_attr: The attribute representing the username
|
||||
|
||||
Returns:
|
||||
tuple: (List of Gateway-compatible organization mappers, next_order)
|
||||
@@ -100,66 +154,35 @@ def org_map_to_gateway_format(org_map, start_order=1):
|
||||
for organization_name in org_map.keys():
|
||||
organization = org_map[organization_name]
|
||||
for user_type in ['admins', 'users']:
|
||||
if user_type in organization:
|
||||
if organization.get(user_type, None) is None:
|
||||
# TODO: Confirm that if we have None with remove we still won't remove
|
||||
if organization[user_type] is None:
|
||||
continue
|
||||
continue
|
||||
|
||||
if organization[user_type] is False:
|
||||
triggers = {"never": {}}
|
||||
elif organization[user_type] is True:
|
||||
triggers = {"always": {}}
|
||||
else:
|
||||
import re
|
||||
# Get the permission type
|
||||
permission_type = user_type.title()
|
||||
|
||||
# Handle the case where the value itself is a regex pattern
|
||||
if isinstance(organization[user_type], re.Pattern):
|
||||
# Convert single regex pattern to string in a list
|
||||
triggers = {"groups": {"has_or": [str(organization[user_type])]}}
|
||||
else:
|
||||
# Handle list or string values
|
||||
if type(organization[user_type]) is str:
|
||||
organization[user_type] = [organization[user_type]]
|
||||
# Map AWX admin/users to appropriate Gateway organization roles
|
||||
role = "Organization Admin" if user_type == "admins" else "Organization Member"
|
||||
|
||||
# Convert any non-string items to strings (e.g., regex patterns)
|
||||
users_list = []
|
||||
for user in organization[user_type]:
|
||||
if isinstance(user, str):
|
||||
users_list.append(user)
|
||||
elif isinstance(user, re.Pattern):
|
||||
# Convert regex patterns to string representation
|
||||
users_list.append(str(user.pattern))
|
||||
else:
|
||||
# Convert other objects to string representation
|
||||
users_list.append(str(user))
|
||||
|
||||
triggers = {"groups": {"has_or": users_list}}
|
||||
|
||||
team_name = f"Organization {user_type.title()}"
|
||||
# Map AWX admin/users to appropriate Gateway organization roles
|
||||
role = "Organization Admin" if user_type == "admins" else "Organization Member"
|
||||
|
||||
# Check for remove flags
|
||||
revoke = False
|
||||
if user_type == "admins" and organization.get("remove_admins"):
|
||||
revoke = True
|
||||
elif user_type == "users" and organization.get("remove_users"):
|
||||
revoke = True
|
||||
# Check for remove flags
|
||||
revoke = False
|
||||
if organization.get(f"remove_{user_type}"):
|
||||
revoke = True
|
||||
|
||||
for trigger in process_sso_user_list(organization[user_type], email_attr=email_attr, username_attr=username_attr):
|
||||
result.append(
|
||||
{
|
||||
"name": f"{organization_name} - {team_name}",
|
||||
"name": f"{organization_name} - {permission_type} {trigger['name']}",
|
||||
"map_type": "organization",
|
||||
"order": order,
|
||||
"authenticator": -1, # Will be updated when creating the mapper
|
||||
"triggers": triggers,
|
||||
"triggers": trigger['trigger'],
|
||||
"organization": organization_name,
|
||||
"team": None, # Organization-level mapping, not team-specific
|
||||
"role": role,
|
||||
"revoke": revoke,
|
||||
}
|
||||
)
|
||||
|
||||
order += 1
|
||||
|
||||
return result, order
|
||||
|
||||
Reference in New Issue
Block a user