diff --git a/Makefile b/Makefile index 1af79f86e81a..e9a149564489 100644 --- a/Makefile +++ b/Makefile @@ -339,7 +339,7 @@ api-lint: awx-link: [ -d "/awx_devel/awx.egg-info" ] || $(PYTHON) /awx_devel/tools/scripts/egg_info_dev -TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests awx/sso/tests +TEST_DIRS ?= awx/main/tests/unit awx/main/tests/functional awx/conf/tests PYTEST_ARGS ?= -n auto ## Run all API unit tests. test: @@ -440,7 +440,7 @@ test_unit: @if [ "$(VENV_BASE)" ]; then \ . $(VENV_BASE)/awx/bin/activate; \ fi; \ - py.test awx/main/tests/unit awx/conf/tests/unit awx/sso/tests/unit + py.test awx/main/tests/unit awx/conf/tests/unit ## Output test coverage as HTML (into htmlcov directory). coverage_html: diff --git a/awx/api/conf.py b/awx/api/conf.py index 67093f42c48e..2e262c316b42 100644 --- a/awx/api/conf.py +++ b/awx/api/conf.py @@ -8,7 +8,6 @@ from awx.conf import fields, register, register_validate from awx.api.fields import OAuth2ProviderField from oauth2_provider.settings import oauth2_settings -from awx.sso.common import is_remote_auth_enabled register( @@ -109,7 +108,7 @@ def authentication_validate(serializer, attrs): - if attrs.get('DISABLE_LOCAL_AUTH', False) and not is_remote_auth_enabled(): + if attrs.get('DISABLE_LOCAL_AUTH', False): raise serializers.ValidationError(_("There are no remote authentication systems configured.")) return attrs diff --git a/awx/api/serializers.py b/awx/api/serializers.py index e167f463ec5f..acc13acbc963 100644 --- a/awx/api/serializers.py +++ b/awx/api/serializers.py @@ -134,8 +134,6 @@ # AWX Utils from awx.api.validators import HostnameRegexValidator -from awx.sso.common import get_external_account - logger = logging.getLogger('awx.api.serializers') # Fields that should be summarized regardless of object type. @@ -961,7 +959,6 @@ def get_types(self): class UserSerializer(BaseSerializer): password = serializers.CharField(required=False, default='', help_text=_('Field used to change the password.')) - external_account = serializers.SerializerMethodField(help_text=_('Set if the account is managed by an external service')) is_system_auditor = serializers.BooleanField(default=False) show_capabilities = ['edit', 'delete'] @@ -979,20 +976,12 @@ class Meta: 'is_system_auditor', 'password', 'last_login', - 'external_account', ) extra_kwargs = {'last_login': {'read_only': True}} def to_representation(self, obj): ret = super(UserSerializer, self).to_representation(obj) - if self.get_external_account(obj): - # If this is an external account it shouldn't have a password field - ret.pop('password', None) - else: - # If its an internal account lets assume there is a password and return $encrypted$ to the user - ret['password'] = '$encrypted$' - if obj and type(self) is UserSerializer: - ret['auth'] = obj.social_auth.values('provider', 'uid') + ret['password'] = '$encrypted$' return ret def get_validation_exclusions(self, obj=None): @@ -1025,12 +1014,7 @@ def validate_password(self, value): return value def _update_password(self, obj, new_password): - # For now we're not raising an error, just not saving password for - # users managed by external authentication services (who already have an unusable password set). - # get_external_account function will return something like social or enterprise when the user is external, - # and return None when the user isn't external. - # We want to allow a password update only for non-external accounts. - if new_password and new_password != '$encrypted$' and not self.get_external_account(obj): + if new_password and new_password != '$encrypted$': obj.set_password(new_password) obj.save(update_fields=['password']) @@ -1045,9 +1029,6 @@ def _update_password(self, obj, new_password): obj.set_unusable_password() obj.save(update_fields=['password']) - def get_external_account(self, obj): - return get_external_account(obj) - def create(self, validated_data): new_password = validated_data.pop('password', None) is_system_auditor = validated_data.pop('is_system_auditor', None) diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index fdb9fef67f39..5ae17d07e0cc 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -50,9 +50,6 @@ # ansi2html from ansi2html import Ansi2HTMLConverter -# Python Social Auth -from social_core.backends.utils import load_backends - # Django OAuth Toolkit from oauth2_provider.models import get_access_token_model @@ -129,6 +126,9 @@ from awx.api.pagination import UnifiedJobEventPagination from awx.main.utils import set_environ +if 'ansible_base.authentication' in getattr(settings, "INSTALLED_APPS", []): + from ansible_base.authentication.models.authenticator import Authenticator as AnsibleBaseAuthenticator + logger = logging.getLogger('awx.api.views') @@ -684,20 +684,18 @@ class AuthView(APIView): swagger_topic = 'System Configuration' def get(self, request): - from rest_framework.reverse import reverse - data = OrderedDict() - err_backend, err_message = request.session.get('social_auth_error', (None, None)) - auth_backends = list(load_backends(settings.AUTHENTICATION_BACKENDS, force_load=True).items()) - # Return auth backends in consistent order: oidc. - auth_backends.sort(key=lambda x: x[0]) - for name, backend in auth_backends: - login_url = reverse('social:begin', args=(name,)) - complete_url = request.build_absolute_uri(reverse('social:complete', args=(name,))) - backend_data = {'login_url': login_url, 'complete_url': complete_url} - if err_backend == name and err_message: - backend_data['error'] = err_message - data[name] = backend_data + if 'ansible_base.authentication' in getattr(settings, "INSTALLED_APPS", []): + # app is using ansible_base authentication + # add ansible_base authenticators + authenticators = AnsibleBaseAuthenticator.objects.filter(enabled=True, category="sso") + for authenticator in authenticators: + login_url = authenticator.get_login_url() + data[authenticator.name] = { + 'login_url': login_url, + 'name': authenticator.name, + } + return Response(data) diff --git a/awx/conf/signals.py b/awx/conf/signals.py index d7868e4faa4f..fb96019a7890 100644 --- a/awx/conf/signals.py +++ b/awx/conf/signals.py @@ -61,18 +61,3 @@ def on_post_delete_setting(sender, **kwargs): key = getattr(instance, '_saved_key_', None) if key: handle_setting_change(key, True) - - -@receiver(setting_changed) -def disable_local_auth(**kwargs): - if (kwargs['setting'], kwargs['value']) == ('DISABLE_LOCAL_AUTH', True): - from django.contrib.auth.models import User - from oauth2_provider.models import RefreshToken - from awx.main.models.oauth import OAuth2AccessToken - from awx.main.management.commands.revoke_oauth2_tokens import revoke_tokens - - logger.warning("Triggering token invalidation for local users.") - - qs = User.objects.filter(enterprise_auth__isnull=True, social_auth__isnull=True) - revoke_tokens(RefreshToken.objects.filter(revoked=None, user__in=qs)) - revoke_tokens(OAuth2AccessToken.objects.filter(user__in=qs)) diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 5b0b99c5e48a..3fc54bc2465e 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -93,8 +93,8 @@ def process_request(self, request): user = request.user if not user.pk: return - if not (user.social_auth.exists() or user.enterprise_auth.exists()): - logout(request) + + logout(request) class URLModificationMiddleware(MiddlewareMixin): diff --git a/awx/main/migrations/0196_delete_profile.py b/awx/main/migrations/0196_delete_profile.py index a2179870bd81..bdfdf90b480e 100644 --- a/awx/main/migrations/0196_delete_profile.py +++ b/awx/main/migrations/0196_delete_profile.py @@ -1,4 +1,4 @@ -# Generated by Django 4.2.10 on 2024-08-09 16:47 +# Generated by Django 4.2.10 on 2024-09-16 10:22 from django.db import migrations diff --git a/awx/main/migrations/0197_remove_sso_app_content.py b/awx/main/migrations/0197_remove_sso_app_content.py new file mode 100644 index 000000000000..71bbb33f19a1 --- /dev/null +++ b/awx/main/migrations/0197_remove_sso_app_content.py @@ -0,0 +1,27 @@ +# Generated by Django 4.2.10 on 2024-09-16 15:21 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ('main', '0196_delete_profile'), + ] + + operations = [ + # delete all sso application migrations + migrations.RunSQL("DELETE FROM django_migrations WHERE app = 'sso';"), + # delete all sso application content group permissions + migrations.RunSQL( + "DELETE FROM auth_group_permissions " + "WHERE permission_id IN " + "(SELECT id FROM auth_permission WHERE content_type_id in (SELECT id FROM django_content_type WHERE app_label = 'sso'));" + ), + # delete all sso application content permissions + migrations.RunSQL("DELETE FROM auth_permission " "WHERE content_type_id IN (SELECT id FROM django_content_type WHERE app_label = 'sso');"), + # delete sso application content type + migrations.RunSQL("DELETE FROM django_content_type WHERE app_label = 'sso';"), + # drop sso application created table + migrations.RunSQL("DROP TABLE IF EXISTS sso_userenterpriseauth;"), + ] diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 444d662ca729..a63cc31bf877 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -244,16 +244,6 @@ def user_is_system_auditor(user, tf): User.add_to_class('is_system_auditor', user_is_system_auditor) -def user_is_in_enterprise_category(user, category): - ret = (category,) in user.enterprise_auth.values_list('provider') and not user.has_usable_password() - # NOTE: this if block ensures existing enterprise users are still able to - # log in. Remove it in a future release - return ret - - -User.add_to_class('is_in_enterprise_category', user_is_in_enterprise_category) - - def o_auth2_application_get_absolute_url(self, request=None): return reverse('api:o_auth2_application_detail', kwargs={'pk': self.pk}, request=request) diff --git a/awx/main/models/oauth.py b/awx/main/models/oauth.py index fbd77721194f..adda62d5741e 100644 --- a/awx/main/models/oauth.py +++ b/awx/main/models/oauth.py @@ -12,9 +12,7 @@ # Django OAuth Toolkit from oauth2_provider.models import AbstractApplication, AbstractAccessToken from oauth2_provider.generators import generate_client_secret -from oauthlib import oauth2 -from awx.sso.common import get_external_account from awx.main.fields import OAuth2ClientSecretField @@ -123,15 +121,5 @@ def _update_last_used(): connection.on_commit(_update_last_used) return valid - def validate_external_users(self): - if self.user and settings.ALLOW_OAUTH2_FOR_EXTERNAL_USERS is False: - external_account = get_external_account(self.user) - if external_account is not None: - raise oauth2.AccessDeniedError( - _('OAuth2 Tokens cannot be created by users associated with an external authentication provider ({})').format(external_account) - ) - def save(self, *args, **kwargs): - if not self.pk: - self.validate_external_users() super(OAuth2AccessToken, self).save(*args, **kwargs) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 5c25df172216..109ee6c8f3fa 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -313,8 +313,6 @@ 'django.contrib.messages.context_processors.messages', 'awx.ui.context_processors.csp', 'awx.ui.context_processors.version', - 'social_django.context_processors.backends', - 'social_django.context_processors.login_redirect', ], 'builtins': ['awx.main.templatetags.swagger'], }, @@ -344,14 +342,12 @@ 'rest_framework', 'django_extensions', 'polymorphic', - 'social_django', 'django_guid', 'corsheaders', 'awx.conf', 'awx.main', 'awx.api', 'awx.ui', - 'awx.sso', 'solo', 'ansible_base.rest_filters', 'ansible_base.jwt_consumer', @@ -386,9 +382,7 @@ # 'URL_FORMAT_OVERRIDE': None, } -AUTHENTICATION_BACKENDS = ( - 'awx.main.backends.AWXModelBackend', -) +AUTHENTICATION_BACKENDS = ('awx.main.backends.AWXModelBackend',) # Django OAuth Toolkit settings @@ -455,10 +449,6 @@ DJANGO_REDIS_IGNORE_EXCEPTIONS = True CACHES = {'default': {'BACKEND': 'awx.main.cache.AWXRedisCache', 'LOCATION': 'unix:///var/run/redis/redis.sock?db=1'}} -# Social Auth configuration. -SOCIAL_AUTH_STRATEGY = 'social_django.strategy.DjangoStrategy' -SOCIAL_AUTH_STORAGE = 'social_django.models.DjangoStorage' -SOCIAL_AUTH_USER_MODEL = 'auth.User' ROLE_SINGLETON_USER_RELATIONSHIP = '' ROLE_SINGLETON_TEAM_RELATIONSHIP = '' @@ -466,41 +456,6 @@ ROLE_BYPASS_SUPERUSER_FLAGS = ['is_superuser'] ROLE_BYPASS_ACTION_FLAGS = {'view': 'is_system_auditor'} -_SOCIAL_AUTH_PIPELINE_BASE = ( - 'social_core.pipeline.social_auth.social_details', - 'social_core.pipeline.social_auth.social_uid', - 'social_core.pipeline.social_auth.auth_allowed', - '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', - 'awx.sso.social_base_pipeline.check_user_found_or_created', - 'social_core.pipeline.social_auth.associate_user', - 'social_core.pipeline.social_auth.load_extra_data', - 'awx.sso.social_base_pipeline.set_is_active_for_new_user', - 'social_core.pipeline.user.user_details', - 'awx.sso.social_base_pipeline.prevent_inactive_login', -) - -SOCIAL_AUTH_PIPELINE = _SOCIAL_AUTH_PIPELINE_BASE + ( - 'awx.sso.social_pipeline.update_user_orgs', - 'awx.sso.social_pipeline.update_user_teams', - 'ansible_base.resource_registry.utils.service_backed_sso_pipeline.redirect_to_resource_server', -) - -SOCIAL_AUTH_LOGIN_URL = '/' -SOCIAL_AUTH_LOGIN_REDIRECT_URL = '/sso/complete/' -SOCIAL_AUTH_LOGIN_ERROR_URL = '/sso/error/' -SOCIAL_AUTH_INACTIVE_USER_URL = '/sso/inactive/' - -SOCIAL_AUTH_RAISE_EXCEPTIONS = False -SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL = False -# SOCIAL_AUTH_SLUGIFY_USERNAMES = True -SOCIAL_AUTH_CLEAN_USERNAMES = True - -SOCIAL_AUTH_SANITIZE_REDIRECTS = True -SOCIAL_AUTH_REDIRECT_IS_HTTPS = False - # Any ANSIBLE_* settings will be passed to the task runner subprocess # environment @@ -941,7 +896,6 @@ 'awx.main.middleware.DisableLocalAuthMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'awx.main.middleware.OptionalURLPrefixPath', - 'awx.sso.middleware.SocialAuthMiddleware', 'crum.CurrentRequestUserMiddleware', 'awx.main.middleware.URLModificationMiddleware', 'awx.main.middleware.SessionTimeoutMiddleware', diff --git a/awx/sso/__init__.py b/awx/sso/__init__.py deleted file mode 100644 index e484e62be15d..000000000000 --- a/awx/sso/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. diff --git a/awx/sso/apps.py b/awx/sso/apps.py deleted file mode 100644 index 6203ca6d6a11..000000000000 --- a/awx/sso/apps.py +++ /dev/null @@ -1,8 +0,0 @@ -# Django -from django.apps import AppConfig -from django.utils.translation import gettext_lazy as _ - - -class SSOConfig(AppConfig): - name = 'awx.sso' - verbose_name = _('Single Sign-On') diff --git a/awx/sso/backends.py b/awx/sso/backends.py deleted file mode 100644 index 3eea5eab61a8..000000000000 --- a/awx/sso/backends.py +++ /dev/null @@ -1,35 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Python -import logging - -# Django -from django.contrib.auth.models import User -from django.conf import settings as django_settings - -# Ansible Tower -from awx.sso.models import UserEnterpriseAuth - -logger = logging.getLogger('awx.sso.backends') - - -def _decorate_enterprise_user(user, provider): - user.set_unusable_password() - user.save() - enterprise_auth, _ = UserEnterpriseAuth.objects.get_or_create(user=user, provider=provider) - return enterprise_auth - - -def _get_or_set_enterprise_user(username, password, provider): - created = False - try: - user = User.objects.prefetch_related('enterprise_auth').get(username=username) - except User.DoesNotExist: - user = User(username=username) - enterprise_auth = _decorate_enterprise_user(user, provider) - logger.debug("Created enterprise user %s via %s backend." % (username, enterprise_auth.get_provider_display())) - created = True - if created or user.is_in_enterprise_category(provider): - return user - logger.warning("Enterprise user %s already defined in Tower." % username) diff --git a/awx/sso/common.py b/awx/sso/common.py deleted file mode 100644 index da4982959c85..000000000000 --- a/awx/sso/common.py +++ /dev/null @@ -1,195 +0,0 @@ -# Copyright (c) 2022 Ansible, Inc. -# All Rights Reserved. - -import logging - -from django.contrib.contenttypes.models import ContentType -from django.db.utils import IntegrityError -from awx.main.models import Organization, Team - -logger = logging.getLogger('awx.sso.common') - - -def get_orgs_by_ids(): - existing_orgs = {} - for org_id, org_name in Organization.objects.all().values_list('id', 'name'): - existing_orgs[org_name] = org_id - return existing_orgs - - -def reconcile_users_org_team_mappings(user, desired_org_states, desired_team_states, source): - # - # Arguments: - # user - a user object - # desired_org_states: { '': { '': or None } } - # desired_team_states: { '': { '': { '': or None } } } - # source - a text label indicating the "authentication adapter" for debug messages - # - # This function will load the users existing roles and then based on the desired states modify the users roles - # True indicates the user needs to be a member of the role - # False indicates the user should not be a member of the role - # None means this function should not change the users membership of a role - # - - content_types = [] - reconcile_items = [] - if desired_org_states: - content_types.append(ContentType.objects.get_for_model(Organization)) - reconcile_items.append(('organization', desired_org_states)) - if desired_team_states: - content_types.append(ContentType.objects.get_for_model(Team)) - reconcile_items.append(('team', desired_team_states)) - - if not content_types: - # If both desired states were empty we can simply return because there is nothing to reconcile - return - - # users_roles is a flat set of IDs - users_roles = set(user.roles.filter(content_type__in=content_types).values_list('pk', flat=True)) - - for object_type, desired_states in reconcile_items: - roles = [] - # Get a set of named tuples for the org/team name plus all of the roles we got above - if object_type == 'organization': - for sub_dict in desired_states.values(): - for role_name in sub_dict: - if sub_dict[role_name] is None: - continue - if role_name not in roles: - roles.append(role_name) - model_roles = Organization.objects.filter(name__in=desired_states.keys()).values_list('name', *roles, named=True) - else: - team_names = [] - for teams_dict in desired_states.values(): - team_names.extend(teams_dict.keys()) - for sub_dict in teams_dict.values(): - for role_name in sub_dict: - if sub_dict[role_name] is None: - continue - if role_name not in roles: - roles.append(role_name) - model_roles = Team.objects.filter(name__in=team_names).values_list('name', 'organization__name', *roles, named=True) - - for row in model_roles: - for role_name in roles: - if object_type == 'organization': - desired_state = desired_states.get(row.name, {}) - else: - desired_state = desired_states.get(row.organization__name, {}).get(row.name, {}) - - if desired_state.get(role_name, None) is None: - # The mapping was not defined for this [org/team]/role so we can just pass - continue - - # If somehow the auth adapter knows about an items role but that role is not defined in the DB we are going to print a pretty error - # This is your classic safety net that we should never hit; but here you are reading this comment... good luck and Godspeed. - role_id = getattr(row, role_name, None) - if role_id is None: - logger.error("{} adapter wanted to manage role {} of {} {} but that role is not defined".format(source, role_name, object_type, row.name)) - continue - - if desired_state[role_name]: - # The desired state was the user mapped into the object_type, if the user was not mapped in map them in - if role_id not in users_roles: - logger.debug("{} adapter adding user {} to {} {} as {}".format(source, user.username, object_type, row.name, role_name)) - user.roles.add(role_id) - else: - # The desired state was the user was not mapped into the org, if the user has the permission remove it - if role_id in users_roles: - logger.debug("{} adapter removing user {} permission of {} from {} {}".format(source, user.username, role_name, object_type, row.name)) - user.roles.remove(role_id) - - -def create_org_and_teams(org_list, team_map, adapter, can_create=True): - # - # org_list is a set of organization names - # team_map is a dict of {: } - # - # Move this junk into save of the settings for performance later, there is no need to do that here - # with maybe the exception of someone defining this in settings before the server is started? - # ============================================================================================================== - - if not can_create: - logger.debug(f"Adapter {adapter} is not allowed to create orgs/teams") - return - - # Get all of the IDs and names of orgs in the DB and create any new org defined in org_list that does not exist in the DB - existing_orgs = get_orgs_by_ids() - - # Parse through orgs and teams provided and create a list of unique items we care about creating - all_orgs = list(set(org_list)) - all_teams = [] - for team_name in team_map: - org_name = team_map[team_name] - if org_name: - if org_name not in all_orgs: - all_orgs.append(org_name) - # We don't have to test if this is in all_teams because team_map is already a hash - all_teams.append(team_name) - else: - # The UI should prevent this condition so this is just a double check to prevent a stack trace.... - # although the rest of the login process might stack later on - logger.error("{} adapter is attempting to create a team {} but it does not have an org".format(adapter, team_name)) - - for org_name in all_orgs: - if org_name and org_name not in existing_orgs: - logger.info("{} adapter is creating org {}".format(adapter, org_name)) - try: - new_org = get_or_create_org_with_default_galaxy_cred(name=org_name) - except IntegrityError: - # Another thread must have created this org before we did so now we need to get it - new_org = get_or_create_org_with_default_galaxy_cred(name=org_name) - # Add the org name to the existing orgs since we created it and we may need it to build the teams below - existing_orgs[org_name] = new_org.id - - # Do the same for teams - existing_team_names = list(Team.objects.all().values_list('name', flat=True)) - for team_name in all_teams: - if team_name not in existing_team_names: - logger.info("{} adapter is creating team {} in org {}".format(adapter, team_name, team_map[team_name])) - try: - Team.objects.create(name=team_name, organization_id=existing_orgs[team_map[team_name]]) - except IntegrityError: - # If another process got here before us that is ok because we don't need the ID from this team or anything - pass - # End move some day - # ============================================================================================================== - - -def get_or_create_org_with_default_galaxy_cred(**kwargs): - from awx.main.models import Organization, Credential - - (org, org_created) = Organization.objects.get_or_create(**kwargs) - if org_created: - logger.debug("Created org {} (id {}) from {}".format(org.name, org.id, kwargs)) - public_galaxy_credential = Credential.objects.filter(managed=True, name='Ansible Galaxy').first() - if public_galaxy_credential is not None: - org.galaxy_credentials.add(public_galaxy_credential) - logger.debug("Added default Ansible Galaxy credential to org") - else: - logger.debug("Could not find default Ansible Galaxy credential to add to org") - return org - - -def get_external_account(user): - account_type = None - - if user.social_auth.all(): - account_type = "social" - - if user.enterprise_auth.all(): - account_type = "enterprise" - - return account_type - - -def is_remote_auth_enabled(): - from django.conf import settings - - settings_that_turn_on_remote_auth = [] - # Also include any SOCAIL_AUTH_*KEY - for social_auth_key in dir(settings): - if social_auth_key.startswith('SOCIAL_AUTH_') and social_auth_key.endswith('_KEY'): - settings_that_turn_on_remote_auth.append(social_auth_key) - - return any(getattr(settings, s, None) for s in settings_that_turn_on_remote_auth) diff --git a/awx/sso/conf.py b/awx/sso/conf.py deleted file mode 100644 index 891e3ac64f2a..000000000000 --- a/awx/sso/conf.py +++ /dev/null @@ -1,180 +0,0 @@ -# Python -import collections -import urllib.parse as urlparse - -# Django -from django.conf import settings -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ - -# AWX -from awx.conf import register, fields -from awx.sso.fields import ( - AuthenticationBackendsField, - SocialOrganizationMapField, - SocialTeamMapField, -) - - -class SocialAuthCallbackURL(object): - def __init__(self, provider): - self.provider = provider - - def __call__(self): - path = reverse('social:complete', args=(self.provider,)) - return urlparse.urljoin(settings.TOWER_URL_BASE, path) - - -SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT = _( - '''\ -Mapping to organization admins/users from social auth accounts. This setting -controls which users are placed into which organizations based on their -username and email address. Configuration details are available in the -documentation.\ -''' -) - -# FIXME: /regex/gim (flags) - -SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER = collections.OrderedDict( - [ - ('Default', collections.OrderedDict([('users', True)])), - ('Test Org', collections.OrderedDict([('admins', ['admin@example.com']), ('auditors', ['auditor@example.com']), ('users', True)])), - ( - 'Test Org 2', - collections.OrderedDict( - [ - ('admins', ['admin@example.com', r'/^tower-[^@]+*?@.*$/']), - ('remove_admins', True), - ('users', r'/^[^@].*?@example\.com$/i'), - ('remove_users', True), - ] - ), - ), - ] -) - -SOCIAL_AUTH_TEAM_MAP_HELP_TEXT = _( - '''\ -Mapping of team members (users) from social auth accounts. Configuration -details are available in the documentation.\ -''' -) - -SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER = collections.OrderedDict( - [ - ('My Team', collections.OrderedDict([('organization', 'Test Org'), ('users', [r'/^[^@]+?@test\.example\.com$/']), ('remove', True)])), - ('Other Team', collections.OrderedDict([('organization', 'Test Org 2'), ('users', r'/^[^@]+?@test2\.example\.com$/i'), ('remove', False)])), - ] -) - -if settings.ALLOW_LOCAL_RESOURCE_MANAGEMENT: - ############################################################################### - # AUTHENTICATION BACKENDS DYNAMIC SETTING - ############################################################################### - - register( - 'AUTHENTICATION_BACKENDS', - field_class=AuthenticationBackendsField, - label=_('Authentication Backends'), - help_text=_('List of authentication backends that are enabled based on license features and other authentication settings.'), - read_only=True, - depends_on=AuthenticationBackendsField.get_all_required_settings(), - category=_('Authentication'), - category_slug='authentication', - ) - - register( - 'SOCIAL_AUTH_ORGANIZATION_MAP', - field_class=SocialOrganizationMapField, - allow_null=True, - default=None, - label=_('Social Auth Organization Map'), - help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT, - category=_('Authentication'), - category_slug='authentication', - placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, - ) - - register( - 'SOCIAL_AUTH_TEAM_MAP', - field_class=SocialTeamMapField, - allow_null=True, - default=None, - label=_('Social Auth Team Map'), - help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT, - category=_('Authentication'), - category_slug='authentication', - placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, - ) - - register( - 'SOCIAL_AUTH_USER_FIELDS', - field_class=fields.StringListField, - allow_null=True, - default=None, - label=_('Social Auth User Fields'), - help_text=_( - 'When set to an empty list `[]`, this setting prevents new user ' - 'accounts from being created. Only users who have previously ' - 'logged in using social auth or have a user account with a ' - 'matching email address will be able to login.' - ), - category=_('Authentication'), - category_slug='authentication', - placeholder=['username', 'email'], - ) - - register( - 'SOCIAL_AUTH_USERNAME_IS_FULL_EMAIL', - field_class=fields.BooleanField, - default=False, - label=_('Use Email address for usernames'), - help_text=_('Enabling this setting will tell social auth to use the full Email as username instead of the full name'), - category=_('Authentication'), - category_slug='authentication', - ) - - register( - 'LOCAL_PASSWORD_MIN_LENGTH', - field_class=fields.IntegerField, - min_value=0, - default=0, - label=_('Minimum number of characters in local password'), - help_text=_('Minimum number of characters required in a local password. 0 means no minimum'), - category=_('Authentication'), - category_slug='authentication', - ) - - register( - 'LOCAL_PASSWORD_MIN_DIGITS', - field_class=fields.IntegerField, - min_value=0, - default=0, - label=_('Minimum number of digit characters in local password'), - help_text=_('Minimum number of digit characters required in a local password. 0 means no minimum'), - category=_('Authentication'), - category_slug='authentication', - ) - - register( - 'LOCAL_PASSWORD_MIN_UPPER', - field_class=fields.IntegerField, - min_value=0, - default=0, - label=_('Minimum number of uppercase characters in local password'), - help_text=_('Minimum number of uppercase characters required in a local password. 0 means no minimum'), - category=_('Authentication'), - category_slug='authentication', - ) - - register( - 'LOCAL_PASSWORD_MIN_SPECIAL', - field_class=fields.IntegerField, - min_value=0, - default=0, - label=_('Minimum number of special characters in local password'), - help_text=_('Minimum number of special characters required in a local password. 0 means no minimum'), - category=_('Authentication'), - category_slug='authentication', - ) diff --git a/awx/sso/fields.py b/awx/sso/fields.py deleted file mode 100644 index 54e31ba259fb..000000000000 --- a/awx/sso/fields.py +++ /dev/null @@ -1,229 +0,0 @@ -import collections -import copy -import json -import re - -import six - -# Django -from django.utils.translation import gettext_lazy as _ - -from rest_framework.exceptions import ValidationError -from rest_framework.fields import empty, Field, SkipField - -# AWX -from awx.conf import fields - - -def get_subclasses(cls): - for subclass in cls.__subclasses__(): - for subsubclass in get_subclasses(subclass): - yield subsubclass - yield subclass - - -class DependsOnMixin: - def get_depends_on(self): - """ - Get the value of the dependent field. - First try to find the value in the request. - Then fall back to the raw value from the setting in the DB. - """ - from django.conf import settings - - dependent_key = next(iter(self.depends_on)) - - if self.context: - request = self.context.get('request', None) - if request and request.data and request.data.get(dependent_key, None): - return request.data.get(dependent_key) - res = settings._get_local(dependent_key, validate=False) - return res - - -class _Forbidden(Field): - default_error_messages = {'invalid': _('Invalid field.')} - - def run_validation(self, value): - self.fail('invalid') - - -class HybridDictField(fields.DictField): - """A DictField, but with defined fixed Fields for certain keys.""" - - def __init__(self, *args, **kwargs): - self.allow_blank = kwargs.pop('allow_blank', False) - - fields = [ - sorted( - ((field_name, obj) for field_name, obj in cls.__dict__.items() if isinstance(obj, Field) and field_name != 'child'), - key=lambda x: x[1]._creation_counter, - ) - for cls in reversed(self.__class__.__mro__) - ] - self._declared_fields = collections.OrderedDict(f for group in fields for f in group) - - super().__init__(*args, **kwargs) - - def to_representation(self, value): - fields = copy.deepcopy(self._declared_fields) - return { - key: field.to_representation(val) if val is not None else None - for key, val, field in ((six.text_type(key), val, fields.get(key, self.child)) for key, val in value.items()) - if not field.write_only - } - - def run_child_validation(self, data): - result = {} - - if not data and self.allow_blank: - return result - - errors = collections.OrderedDict() - fields = copy.deepcopy(self._declared_fields) - keys = set(fields.keys()) | set(data.keys()) - - for key in keys: - value = data.get(key, empty) - key = six.text_type(key) - field = fields.get(key, self.child) - try: - if field.read_only: - continue # Ignore read_only fields, as Serializer seems to do. - result[key] = field.run_validation(value) - except ValidationError as e: - errors[key] = e.detail - except SkipField: - pass - - if not errors: - return result - raise ValidationError(errors) - - -class AuthenticationBackendsField(fields.StringListField): - # Mapping of settings that must be set in order to enable each - # authentication backend. - REQUIRED_BACKEND_SETTINGS = collections.OrderedDict( - [ - ('social_core.backends.open_id_connect.OpenIdConnectAuth', ['SOCIAL_AUTH_OIDC_KEY', 'SOCIAL_AUTH_OIDC_SECRET', 'SOCIAL_AUTH_OIDC_OIDC_ENDPOINT']), - ('django.contrib.auth.backends.ModelBackend', []), - ('awx.main.backends.AWXModelBackend', []), - ] - ) - - @classmethod - def get_all_required_settings(cls): - all_required_settings = set(['LICENSE']) - for required_settings in cls.REQUIRED_BACKEND_SETTINGS.values(): - all_required_settings.update(required_settings) - return all_required_settings - - def __init__(self, *args, **kwargs): - kwargs.setdefault('default', self._default_from_required_settings) - super(AuthenticationBackendsField, self).__init__(*args, **kwargs) - - def _default_from_required_settings(self): - from django.conf import settings - - try: - backends = settings._awx_conf_settings._get_default('AUTHENTICATION_BACKENDS') - except AttributeError: - backends = self.REQUIRED_BACKEND_SETTINGS.keys() - # Filter which authentication backends are enabled based on their - # required settings being defined and non-empty. - for backend, required_settings in self.REQUIRED_BACKEND_SETTINGS.items(): - if backend not in backends: - continue - if all([getattr(settings, rs, None) for rs in required_settings]): - continue - backends = [x for x in backends if x != backend] - return backends - - -class SocialMapStringRegexField(fields.CharField): - def to_representation(self, value): - if isinstance(value, type(re.compile(''))): - flags = [] - if value.flags & re.I: - flags.append('i') - if value.flags & re.M: - flags.append('m') - return '/{}/{}'.format(value.pattern, ''.join(flags)) - else: - return super(SocialMapStringRegexField, self).to_representation(value) - - def to_internal_value(self, data): - data = super(SocialMapStringRegexField, self).to_internal_value(data) - match = re.match(r'^/(?P.*)/(?P[im]+)?$', data) - if match: - flags = 0 - if match.group('flags'): - if 'i' in match.group('flags'): - flags |= re.I - if 'm' in match.group('flags'): - flags |= re.M - try: - return re.compile(match.group('pattern'), flags) - except re.error as e: - raise ValidationError('{}: {}'.format(e, data)) - return data - - -class SocialMapField(fields.ListField): - default_error_messages = {'type_error': _('Expected None, True, False, a string or list of strings but got {input_type} instead.')} - child = SocialMapStringRegexField() - - def to_representation(self, value): - if isinstance(value, (list, tuple)): - return super(SocialMapField, self).to_representation(value) - elif value in fields.BooleanField.TRUE_VALUES: - return True - elif value in fields.BooleanField.FALSE_VALUES: - return False - elif value in fields.BooleanField.NULL_VALUES: - return None - elif isinstance(value, (str, type(re.compile('')))): - return self.child.to_representation(value) - else: - self.fail('type_error', input_type=type(value)) - - def to_internal_value(self, data): - if isinstance(data, (list, tuple)): - return super(SocialMapField, self).to_internal_value(data) - elif data in fields.BooleanField.TRUE_VALUES: - return True - elif data in fields.BooleanField.FALSE_VALUES: - return False - elif data in fields.BooleanField.NULL_VALUES: - return None - elif isinstance(data, str): - return self.child.run_validation(data) - else: - self.fail('type_error', input_type=type(data)) - - -class SocialSingleOrganizationMapField(HybridDictField): - admins = SocialMapField(allow_null=True, required=False) - users = SocialMapField(allow_null=True, required=False) - remove_admins = fields.BooleanField(required=False) - remove_users = fields.BooleanField(required=False) - organization_alias = SocialMapField(allow_null=True, required=False) - - child = _Forbidden() - - -class SocialOrganizationMapField(fields.DictField): - child = SocialSingleOrganizationMapField() - - -class SocialSingleTeamMapField(HybridDictField): - organization = fields.CharField() - users = SocialMapField(allow_null=True, required=False) - remove = fields.BooleanField(required=False) - - child = _Forbidden() - - -class SocialTeamMapField(fields.DictField): - child = SocialSingleTeamMapField() diff --git a/awx/sso/middleware.py b/awx/sso/middleware.py deleted file mode 100644 index f8b2b7974167..000000000000 --- a/awx/sso/middleware.py +++ /dev/null @@ -1,80 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Python -import urllib.parse - -# Django -from django.conf import settings -from django.utils.functional import LazyObject -from django.shortcuts import redirect - -# Python Social Auth -from social_core.exceptions import SocialAuthBaseException -from social_core.utils import social_logger -from social_django import utils -from social_django.middleware import SocialAuthExceptionMiddleware - - -class SocialAuthMiddleware(SocialAuthExceptionMiddleware): - def process_request(self, request): - if request.path.startswith('/sso'): - # See upgrade blocker note in requirements/README.md - utils.BACKENDS = settings.AUTHENTICATION_BACKENDS - token_key = request.COOKIES.get('token', '') - token_key = urllib.parse.quote(urllib.parse.unquote(token_key).strip('"')) - - if not hasattr(request, 'successful_authenticator'): - request.successful_authenticator = None - - if not request.path.startswith('/sso/') and 'migrations_notran' not in request.path: - if request.user and request.user.is_authenticated: - # The rest of the code base rely hevily on type/inheritance checks, - # LazyObject sent from Django auth middleware can be buggy if not - # converted back to its original object. - if isinstance(request.user, LazyObject) and request.user._wrapped: - request.user = request.user._wrapped - request.session.pop('social_auth_error', None) - request.session.pop('social_auth_last_backend', None) - return self.get_response(request) - - def process_view(self, request, callback, callback_args, callback_kwargs): - if request.path.startswith('/sso/login/'): - request.session['social_auth_last_backend'] = callback_kwargs['backend'] - - def process_exception(self, request, exception): - strategy = getattr(request, 'social_strategy', None) - if strategy is None or self.raise_exception(request, exception): - return - - if isinstance(exception, SocialAuthBaseException) or request.path.startswith('/sso/'): - backend = getattr(request, 'backend', None) - backend_name = getattr(backend, 'name', 'unknown-backend') - - message = self.get_message(request, exception) - if request.session.get('social_auth_last_backend') != backend_name: - backend_name = request.session.get('social_auth_last_backend') - message = request.GET.get('error_description', message) - - full_backend_name = backend_name - try: - idp_name = strategy.request_data()['RelayState'] - full_backend_name = '%s:%s' % (backend_name, idp_name) - except KeyError: - pass - - social_logger.error(message) - - url = self.get_redirect_uri(request, exception) - request.session['social_auth_error'] = (full_backend_name, message) - return redirect(url) - - def get_message(self, request, exception): - msg = str(exception) - if msg and msg[-1] not in '.?!': - msg = msg + '.' - return msg - - def get_redirect_uri(self, request, exception): - strategy = getattr(request, 'social_strategy', None) - return strategy.session_get('next', '') or strategy.setting('LOGIN_ERROR_URL') diff --git a/awx/sso/migrations/0001_initial.py b/awx/sso/migrations/0001_initial.py deleted file mode 100644 index d759e22437b5..000000000000 --- a/awx/sso/migrations/0001_initial.py +++ /dev/null @@ -1,21 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models -from django.conf import settings - - -class Migration(migrations.Migration): - dependencies = [migrations.swappable_dependency(settings.AUTH_USER_MODEL)] - - operations = [ - migrations.CreateModel( - name='UserEnterpriseAuth', - fields=[ - ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)), - ('provider', models.CharField(max_length=32, choices=[(b'radius', 'RADIUS'), (b'tacacs+', 'TACACS+')])), - ('user', models.ForeignKey(related_name='enterprise_auth', on_delete=models.CASCADE, to=settings.AUTH_USER_MODEL)), - ], - ), - migrations.AlterUniqueTogether(name='userenterpriseauth', unique_together=set([('user', 'provider')])), - ] diff --git a/awx/sso/migrations/0002_expand_provider_options.py b/awx/sso/migrations/0002_expand_provider_options.py deleted file mode 100644 index 68f877717f2f..000000000000 --- a/awx/sso/migrations/0002_expand_provider_options.py +++ /dev/null @@ -1,16 +0,0 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [('sso', '0001_initial')] - - operations = [ - migrations.AlterField( - model_name='userenterpriseauth', - name='provider', - field=models.CharField(max_length=32, choices=[('radius', 'RADIUS'), ('tacacs+', 'TACACS+'), ('saml', 'SAML')]), - ) - ] diff --git a/awx/sso/migrations/0003_convert_saml_string_to_list.py b/awx/sso/migrations/0003_convert_saml_string_to_list.py deleted file mode 100644 index fac25f3b8df5..000000000000 --- a/awx/sso/migrations/0003_convert_saml_string_to_list.py +++ /dev/null @@ -1,9 +0,0 @@ -from django.db import migrations - - -class Migration(migrations.Migration): - dependencies = [ - ('sso', '0002_expand_provider_options'), - ] - # NOOP, migration is kept to preserve integrity. - operations = [] diff --git a/awx/sso/migrations/__init__.py b/awx/sso/migrations/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/awx/sso/models.py b/awx/sso/models.py deleted file mode 100644 index 4abdb4330fce..000000000000 --- a/awx/sso/models.py +++ /dev/null @@ -1,20 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Django -from django.db import models -from django.contrib.auth.models import User -from django.utils.translation import gettext_lazy as _ - - -# todo: this model to be removed as part of sso removal issue AAP-28380 -class UserEnterpriseAuth(models.Model): - """Enterprise Auth association model""" - - PROVIDER_CHOICES = (('radius', _('RADIUS')), ('tacacs+', _('TACACS+'))) - - class Meta: - unique_together = ('user', 'provider') - - user = models.ForeignKey(User, related_name='enterprise_auth', on_delete=models.CASCADE) - provider = models.CharField(max_length=32, choices=PROVIDER_CHOICES) diff --git a/awx/sso/social_base_pipeline.py b/awx/sso/social_base_pipeline.py deleted file mode 100644 index ccdaf1d20079..000000000000 --- a/awx/sso/social_base_pipeline.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Python Social Auth -from social_core.exceptions import AuthException - -# Django -from django.utils.translation import gettext_lazy as _ - - -class AuthNotFound(AuthException): - def __init__(self, backend, email_or_uid, *args, **kwargs): - self.email_or_uid = email_or_uid - super(AuthNotFound, self).__init__(backend, *args, **kwargs) - - def __str__(self): - return _('An account cannot be found for {0}').format(self.email_or_uid) - - -class AuthInactive(AuthException): - def __str__(self): - return _('Your account is inactive') - - -def check_user_found_or_created(backend, details, user=None, *args, **kwargs): - if not user: - email_or_uid = details.get('email') or kwargs.get('email') or kwargs.get('uid') or '???' - raise AuthNotFound(backend, email_or_uid) - - -def set_is_active_for_new_user(strategy, details, user=None, *args, **kwargs): - if kwargs.get('is_new', False): - details['is_active'] = True - return {'details': details} - - -def prevent_inactive_login(backend, details, user=None, *args, **kwargs): - if user and not user.is_active: - raise AuthInactive(backend) diff --git a/awx/sso/social_pipeline.py b/awx/sso/social_pipeline.py deleted file mode 100644 index b4fb4c1fe323..000000000000 --- a/awx/sso/social_pipeline.py +++ /dev/null @@ -1,90 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Python -import re -import logging - -from awx.sso.common import get_or_create_org_with_default_galaxy_cred - -logger = logging.getLogger('awx.sso.social_pipeline') - - -def _update_m2m_from_expression(user, related, expr, remove=True): - """ - Helper function to update m2m relationship based on user matching one or - more expressions. - """ - should_add = False - if expr is None: - return - elif not expr: - pass - elif expr is True: - should_add = True - else: - if isinstance(expr, (str, type(re.compile('')))): - expr = [expr] - for ex in expr: - if isinstance(ex, str): - if user.username == ex or user.email == ex: - should_add = True - elif isinstance(ex, type(re.compile(''))): - if ex.match(user.username) or ex.match(user.email): - should_add = True - if should_add: - related.add(user) - elif remove: - related.remove(user) - - -def update_user_orgs(backend, details, user=None, *args, **kwargs): - """ - Update organization memberships for the given user based on mapping rules - defined in settings. - """ - if not user: - return - - org_map = backend.setting('ORGANIZATION_MAP') or {} - for org_name, org_opts in org_map.items(): - organization_alias = org_opts.get('organization_alias') - if organization_alias: - organization_name = organization_alias - else: - organization_name = org_name - org = get_or_create_org_with_default_galaxy_cred(name=organization_name) - - # Update org admins from expression(s). - remove = bool(org_opts.get('remove', True)) - admins_expr = org_opts.get('admins', None) - remove_admins = bool(org_opts.get('remove_admins', remove)) - _update_m2m_from_expression(user, org.admin_role.members, admins_expr, remove_admins) - - # Update org users from expression(s). - users_expr = org_opts.get('users', None) - remove_users = bool(org_opts.get('remove_users', remove)) - _update_m2m_from_expression(user, org.member_role.members, users_expr, remove_users) - - -def update_user_teams(backend, details, user=None, *args, **kwargs): - """ - Update team memberships for the given user based on mapping rules defined - in settings. - """ - if not user: - return - from awx.main.models import Team - - team_map = backend.setting('TEAM_MAP') or {} - for team_name, team_opts in team_map.items(): - # Get or create the org to update. - if 'organization' not in team_opts: - continue - org = get_or_create_org_with_default_galaxy_cred(name=team_opts['organization']) - - # Update team members from expression(s). - team = Team.objects.get_or_create(name=team_name, organization=org)[0] - users_expr = team_opts.get('users', None) - remove = bool(team_opts.get('remove', True)) - _update_m2m_from_expression(user, team.member_role.members, users_expr, remove) diff --git a/awx/sso/tests/__init__.py b/awx/sso/tests/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/awx/sso/tests/functional/__init__.py b/awx/sso/tests/functional/__init__.py deleted file mode 100644 index e69de29bb2d1..000000000000 diff --git a/awx/sso/tests/functional/test_common.py b/awx/sso/tests/functional/test_common.py deleted file mode 100644 index 9de74d93e69e..000000000000 --- a/awx/sso/tests/functional/test_common.py +++ /dev/null @@ -1,344 +0,0 @@ -import pytest -from collections import Counter -from django.core.exceptions import FieldError -from django.utils.timezone import now -from django.test.utils import override_settings - -from awx.main.models import Credential, CredentialType, Organization, Team, User -from awx.sso.common import ( - get_orgs_by_ids, - reconcile_users_org_team_mappings, - create_org_and_teams, - get_or_create_org_with_default_galaxy_cred, - is_remote_auth_enabled, - get_external_account, -) - - -class MicroMockObject(object): - def all(self): - return True - - -@pytest.mark.django_db -class TestCommonFunctions: - @pytest.fixture - def orgs(self): - o1 = Organization.objects.create(name='Default1') - o2 = Organization.objects.create(name='Default2') - o3 = Organization.objects.create(name='Default3') - return (o1, o2, o3) - - @pytest.fixture - def galaxy_credential(self): - galaxy_type = CredentialType.objects.create(kind='galaxy') - cred = Credential( - created=now(), modified=now(), name='Ansible Galaxy', managed=True, credential_type=galaxy_type, inputs={'url': 'https://galaxy.ansible.com/'} - ) - cred.save() - - def test_get_orgs_by_ids(self, orgs): - orgs_and_ids = get_orgs_by_ids() - o1, o2, o3 = orgs - assert Counter(orgs_and_ids.keys()) == Counter([o1.name, o2.name, o3.name]) - assert Counter(orgs_and_ids.values()) == Counter([o1.id, o2.id, o3.id]) - - def test_reconcile_users_org_team_mappings(self): - # Create objects for us to play with - user = User.objects.create(username='user1@foo.com', last_name='foo', first_name='bar', email='user1@foo.com', is_active=True) - org1 = Organization.objects.create(name='Default1') - org2 = Organization.objects.create(name='Default2') - team1 = Team.objects.create(name='Team1', organization=org1) - team2 = Team.objects.create(name='Team1', organization=org2) - - # Try adding nothing - reconcile_users_org_team_mappings(user, {}, {}, 'Nada') - assert list(user.roles.all()) == [] - - # Add a user to an org that does not exist (should have no affect) - reconcile_users_org_team_mappings( - user, - { - 'junk': {'member_role': True}, - }, - {}, - 'Nada', - ) - assert list(user.roles.all()) == [] - - # Remove a user to an org that does not exist (should have no affect) - reconcile_users_org_team_mappings( - user, - { - 'junk': {'member_role': False}, - }, - {}, - 'Nada', - ) - assert list(user.roles.all()) == [] - - # Add the user to the orgs - reconcile_users_org_team_mappings(user, {org1.name: {'member_role': True}, org2.name: {'member_role': True}}, {}, 'Nada') - assert len(user.roles.all()) == 2 - assert user in org1.member_role - assert user in org2.member_role - - # Remove the user from the orgs - reconcile_users_org_team_mappings(user, {org1.name: {'member_role': False}, org2.name: {'member_role': False}}, {}, 'Nada') - assert list(user.roles.all()) == [] - assert user not in org1.member_role - assert user not in org2.member_role - - # Remove the user from the orgs (again, should have no affect) - reconcile_users_org_team_mappings(user, {org1.name: {'member_role': False}, org2.name: {'member_role': False}}, {}, 'Nada') - assert list(user.roles.all()) == [] - assert user not in org1.member_role - assert user not in org2.member_role - - # Add a user back to the member role - reconcile_users_org_team_mappings( - user, - { - org1.name: { - 'member_role': True, - }, - }, - {}, - 'Nada', - ) - users_roles = set(user.roles.values_list('pk', flat=True)) - assert len(users_roles) == 1 - assert user in org1.member_role - - # Add the user to additional roles - reconcile_users_org_team_mappings( - user, - { - org1.name: {'admin_role': True, 'auditor_role': True}, - }, - {}, - 'Nada', - ) - assert len(user.roles.all()) == 3 - assert user in org1.member_role - assert user in org1.admin_role - assert user in org1.auditor_role - - # Add a user to a non-existent role (results in FieldError exception) - with pytest.raises(FieldError): - reconcile_users_org_team_mappings( - user, - { - org1.name: { - 'dne_role': True, - }, - }, - {}, - 'Nada', - ) - - # Try adding a user to a role that should not exist on an org (technically this works at this time) - reconcile_users_org_team_mappings( - user, - { - org1.name: { - 'read_role_id': True, - }, - }, - {}, - 'Nada', - ) - assert len(user.roles.all()) == 4 - assert user in org1.member_role - assert user in org1.admin_role - assert user in org1.auditor_role - - # Remove all of the org perms to test team perms - reconcile_users_org_team_mappings( - user, - { - org1.name: { - 'read_role_id': False, - 'member_role': False, - 'admin_role': False, - 'auditor_role': False, - }, - }, - {}, - 'Nada', - ) - assert list(user.roles.all()) == [] - - # Add the user as a member to one of the teams - reconcile_users_org_team_mappings(user, {}, {org1.name: {team1.name: {'member_role': True}}}, 'Nada') - assert len(user.roles.all()) == 1 - assert user in team1.member_role - # Validate that the user did not become a member of a team with the same name in a different org - assert user not in team2.member_role - - # Remove the user from the team - reconcile_users_org_team_mappings(user, {}, {org1.name: {team1.name: {'member_role': False}}}, 'Nada') - assert list(user.roles.all()) == [] - assert user not in team1.member_role - - # Remove the user from the team again - reconcile_users_org_team_mappings(user, {}, {org1.name: {team1.name: {'member_role': False}}}, 'Nada') - assert list(user.roles.all()) == [] - - # Add the user to a team that does not exist (should have no affect) - reconcile_users_org_team_mappings(user, {}, {org1.name: {'junk': {'member_role': True}}}, 'Nada') - assert list(user.roles.all()) == [] - - # Remove the user from a team that does not exist (should have no affect) - reconcile_users_org_team_mappings(user, {}, {org1.name: {'junk': {'member_role': False}}}, 'Nada') - assert list(user.roles.all()) == [] - - # Test a None setting - reconcile_users_org_team_mappings(user, {}, {org1.name: {'junk': {'member_role': None}}}, 'Nada') - assert list(user.roles.all()) == [] - - # Add the user multiple teams in different orgs - reconcile_users_org_team_mappings(user, {}, {org1.name: {team1.name: {'member_role': True}}, org2.name: {team2.name: {'member_role': True}}}, 'Nada') - assert len(user.roles.all()) == 2 - assert user in team1.member_role - assert user in team2.member_role - - # Remove the user from just one of the teams - reconcile_users_org_team_mappings(user, {}, {org2.name: {team2.name: {'member_role': False}}}, 'Nada') - assert len(user.roles.all()) == 1 - assert user in team1.member_role - assert user not in team2.member_role - - @pytest.mark.parametrize( - "org_list, team_map, can_create, org_count, team_count", - [ - # In this case we will only pass in organizations - ( - ["org1", "org2"], - {}, - True, - 2, - 0, - ), - # In this case we will only pass in teams but the orgs will be created from the teams - ( - [], - {"team1": "org1", "team2": "org2"}, - True, - 2, - 2, - ), - # In this case we will reuse an org - ( - ["org1"], - {"team1": "org1", "team2": "org1"}, - True, - 1, - 2, - ), - # In this case we have a combination of orgs, orgs reused and an org created by a team - ( - ["org1", "org2", "org3"], - {"team1": "org1", "team2": "org4"}, - True, - 4, - 2, - ), - # In this case we will test a case that the UI should prevent and have a team with no Org - # This should create org1/2 but only team1 - ( - ["org1"], - {"team1": "org2", "team2": None}, - True, - 2, - 1, - ), - # Block any creation with the can_create flag - ( - ["org1"], - {"team1": "org2", "team2": None}, - False, - 0, - 0, - ), - ], - ) - def test_create_org_and_teams(self, galaxy_credential, org_list, team_map, can_create, org_count, team_count): - create_org_and_teams(org_list, team_map, 'py.test', can_create=can_create) - assert Organization.objects.count() == org_count - assert Team.objects.count() == team_count - - def test_get_or_create_org_with_default_galaxy_cred_add_galaxy_cred(self, galaxy_credential): - # If this method creates the org it should get the default galaxy credential - num_orgs = 4 - for number in range(1, (num_orgs + 1)): - get_or_create_org_with_default_galaxy_cred(name=f"Default {number}") - - assert Organization.objects.count() == 4 - - for o in Organization.objects.all(): - assert o.galaxy_credentials.count() == 1 - assert o.galaxy_credentials.first().name == 'Ansible Galaxy' - - def test_get_or_create_org_with_default_galaxy_cred_no_galaxy_cred(self, galaxy_credential): - # If the org is pre-created, we should not add the galaxy_credential - num_orgs = 4 - for number in range(1, (num_orgs + 1)): - Organization.objects.create(name=f"Default {number}") - get_or_create_org_with_default_galaxy_cred(name=f"Default {number}") - - assert Organization.objects.count() == 4 - - for o in Organization.objects.all(): - assert o.galaxy_credentials.count() == 0 - - @pytest.mark.parametrize( - "enable_social, enable_enterprise, expected_results", - [ - (False, False, None), - (True, False, 'social'), - (True, True, 'enterprise'), - (True, True, 'enterprise'), - (False, True, 'enterprise'), - (True, False, 'social'), - ], - ) - - def test_get_external_account(self, enable_enterprise, expected_results): - try: - user = User.objects.get(username="external_tester") - except User.DoesNotExist: - user = User(username="external_tester") - user.set_unusable_password() - user.save() - if enable_enterprise: - from awx.sso.models import UserEnterpriseAuth - - enterprise_auth = UserEnterpriseAuth(user=user, provider='saml') - enterprise_auth.save() - - assert get_external_account(user) == expected_results - - @pytest.mark.parametrize( - "setting, expected", - [ - # Set none of the social auth settings - ('JUNK_SETTING', False), - # Try a hypothetical future one - ('SOCIAL_AUTH_GIBBERISH_KEY', True), - ], - ) - def test_is_remote_auth_enabled(self, setting, expected): - with override_settings(**{setting: True}): - assert is_remote_auth_enabled() == expected - - @pytest.mark.parametrize( - "key_one, key_one_value, key_two, key_two_value, expected", - [ - ('JUNK_SETTING', True, 'JUNK2_SETTING', True, False), - ], - ) - def test_is_remote_auth_enabled_multiple_keys(self, key_one, key_one_value, key_two, key_two_value, expected): - with override_settings(**{key_one: key_one_value}): - with override_settings(**{key_two: key_two_value}): - assert is_remote_auth_enabled() == expected diff --git a/awx/sso/tests/functional/test_social_base_pipeline.py b/awx/sso/tests/functional/test_social_base_pipeline.py deleted file mode 100644 index 38a49e15f331..000000000000 --- a/awx/sso/tests/functional/test_social_base_pipeline.py +++ /dev/null @@ -1,76 +0,0 @@ -import pytest - -from awx.main.models import User -from awx.sso.social_base_pipeline import AuthNotFound, check_user_found_or_created, set_is_active_for_new_user, prevent_inactive_login, AuthInactive - - -@pytest.mark.django_db -class TestSocialBasePipeline: - def test_check_user_found_or_created_no_exception(self): - # If we have a user (the True param, we should not get an exception) - try: - check_user_found_or_created(None, {}, True) - except AuthNotFound: - assert False, 'check_user_found_or_created should not have raised an exception with a user' - - @pytest.mark.parametrize( - "details, kwargs, expected_id", - [ - ( - {}, - {}, - '???', - ), - ( - {}, - {'uid': 'kwargs_uid'}, - 'kwargs_uid', - ), - ( - {}, - {'uid': 'kwargs_uid', 'email': 'kwargs_email'}, - 'kwargs_email', - ), - ( - {'email': 'details_email'}, - {'uid': 'kwargs_uid', 'email': 'kwargs_email'}, - 'details_email', - ), - ], - ) - def test_check_user_found_or_created_exceptions(self, details, expected_id, kwargs): - with pytest.raises(AuthNotFound) as e: - check_user_found_or_created(None, details, False, None, **kwargs) - assert f'An account cannot be found for {expected_id}' == str(e.value) - - @pytest.mark.parametrize( - "kwargs, expected_details, expected_response", - [ - ({}, {}, None), - ({'is_new': False}, {}, None), - ({'is_new': True}, {'is_active': True}, {'details': {'is_active': True}}), - ], - ) - def test_set_is_active_for_new_user(self, kwargs, expected_details, expected_response): - details = {} - response = set_is_active_for_new_user(None, details, None, None, **kwargs) - assert details == expected_details - assert response == expected_response - - def test_prevent_inactive_login_no_exception_no_user(self): - try: - prevent_inactive_login(None, None, None, None, None) - except AuthInactive: - assert False, 'prevent_inactive_login should not have raised an exception with no user' - - def test_prevent_inactive_login_no_exception_active_user(self): - user = User.objects.create(username='user1@foo.com', last_name='foo', first_name='bar', email='user1@foo.com', is_active=True) - try: - prevent_inactive_login(None, None, user, None, None) - except AuthInactive: - assert False, 'prevent_inactive_login should not have raised an exception with an active user' - - def test_prevent_inactive_login_no_exception_inactive_user(self): - user = User.objects.create(username='user1@foo.com', last_name='foo', first_name='bar', email='user1@foo.com', is_active=False) - with pytest.raises(AuthInactive): - prevent_inactive_login(None, None, user, None, None) diff --git a/awx/sso/tests/functional/test_social_pipeline.py b/awx/sso/tests/functional/test_social_pipeline.py deleted file mode 100644 index f26886e71944..000000000000 --- a/awx/sso/tests/functional/test_social_pipeline.py +++ /dev/null @@ -1,113 +0,0 @@ -import pytest -import re - -from awx.sso.social_pipeline import update_user_orgs, update_user_teams -from awx.main.models import User, Team, Organization - - -@pytest.fixture -def users(): - u1 = User.objects.create(username='user1@foo.com', last_name='foo', first_name='bar', email='user1@foo.com') - u2 = User.objects.create(username='user2@foo.com', last_name='foo', first_name='bar', email='user2@foo.com') - u3 = User.objects.create(username='user3@foo.com', last_name='foo', first_name='bar', email='user3@foo.com') - return (u1, u2, u3) - - -@pytest.mark.django_db -class TestSocialPipeline: - @pytest.fixture - def backend(self): - class Backend: - s = { - 'ORGANIZATION_MAP': { - 'Default': { - 'remove': True, - 'admins': 'foobar', - 'remove_admins': True, - 'users': 'foo', - 'remove_users': True, - 'organization_alias': '', - } - }, - 'TEAM_MAP': {'Blue': {'organization': 'Default', 'remove': True, 'users': ''}, 'Red': {'organization': 'Default', 'remove': True, 'users': ''}}, - } - - def setting(self, key): - return self.s[key] - - return Backend() - - @pytest.fixture - def org(self): - return Organization.objects.create(name="Default") - - def test_update_user_orgs(self, org, backend, users): - u1, u2, u3 = users - - # Test user membership logic with regular expressions - backend.setting('ORGANIZATION_MAP')['Default']['admins'] = re.compile('.*') - backend.setting('ORGANIZATION_MAP')['Default']['users'] = re.compile('.*') - - update_user_orgs(backend, None, u1) - update_user_orgs(backend, None, u2) - update_user_orgs(backend, None, u3) - - assert org.admin_role.members.count() == 3 - assert org.member_role.members.count() == 3 - - # Test remove feature enabled - backend.setting('ORGANIZATION_MAP')['Default']['admins'] = '' - backend.setting('ORGANIZATION_MAP')['Default']['users'] = '' - backend.setting('ORGANIZATION_MAP')['Default']['remove_admins'] = True - backend.setting('ORGANIZATION_MAP')['Default']['remove_users'] = True - update_user_orgs(backend, None, u1) - - assert org.admin_role.members.count() == 2 - assert org.member_role.members.count() == 2 - - # Test remove feature disabled - backend.setting('ORGANIZATION_MAP')['Default']['remove_admins'] = False - backend.setting('ORGANIZATION_MAP')['Default']['remove_users'] = False - update_user_orgs(backend, None, u2) - - assert org.admin_role.members.count() == 2 - assert org.member_role.members.count() == 2 - - # Test organization alias feature - backend.setting('ORGANIZATION_MAP')['Default']['organization_alias'] = 'Default_Alias' - update_user_orgs(backend, None, u1) - assert Organization.objects.get(name="Default_Alias") is not None - - def test_update_user_teams(self, backend, users): - u1, u2, u3 = users - - # Test user membership logic with regular expressions - backend.setting('TEAM_MAP')['Blue']['users'] = re.compile('.*') - backend.setting('TEAM_MAP')['Red']['users'] = re.compile('.*') - - update_user_teams(backend, None, u1) - update_user_teams(backend, None, u2) - update_user_teams(backend, None, u3) - - assert Team.objects.get(name="Red").member_role.members.count() == 3 - assert Team.objects.get(name="Blue").member_role.members.count() == 3 - - # Test remove feature enabled - backend.setting('TEAM_MAP')['Blue']['remove'] = True - backend.setting('TEAM_MAP')['Red']['remove'] = True - backend.setting('TEAM_MAP')['Blue']['users'] = '' - backend.setting('TEAM_MAP')['Red']['users'] = '' - - update_user_teams(backend, None, u1) - - assert Team.objects.get(name="Red").member_role.members.count() == 2 - assert Team.objects.get(name="Blue").member_role.members.count() == 2 - - # Test remove feature disabled - backend.setting('TEAM_MAP')['Blue']['remove'] = False - backend.setting('TEAM_MAP')['Red']['remove'] = False - - update_user_teams(backend, None, u2) - - assert Team.objects.get(name="Red").member_role.members.count() == 2 - assert Team.objects.get(name="Blue").member_role.members.count() == 2 diff --git a/awx/sso/tests/test_env.py b/awx/sso/tests/test_env.py deleted file mode 100644 index b63da8ed8a16..000000000000 --- a/awx/sso/tests/test_env.py +++ /dev/null @@ -1,4 +0,0 @@ -# Ensure that our autouse overwrites are working -def test_cache(settings): - assert settings.CACHES['default']['BACKEND'] == 'django.core.cache.backends.locmem.LocMemCache' - assert settings.CACHES['default']['LOCATION'].startswith('unique-') diff --git a/awx/sso/tests/unit/test_fields.py b/awx/sso/tests/unit/test_fields.py deleted file mode 100644 index 4e451b00391c..000000000000 --- a/awx/sso/tests/unit/test_fields.py +++ /dev/null @@ -1,4 +0,0 @@ -import pytest - -from rest_framework.exceptions import ValidationError - diff --git a/awx/sso/tests/unit/test_pipelines.py b/awx/sso/tests/unit/test_pipelines.py deleted file mode 100644 index fad9126d7950..000000000000 --- a/awx/sso/tests/unit/test_pipelines.py +++ /dev/null @@ -1,11 +0,0 @@ -import pytest - - -@pytest.mark.parametrize( - "lib", - [ - ("social_pipeline"), - ], -) -def test_module_loads(lib): - module = __import__("awx.sso." + lib) # noqa diff --git a/awx/sso/urls.py b/awx/sso/urls.py deleted file mode 100644 index f2cfa3974a93..000000000000 --- a/awx/sso/urls.py +++ /dev/null @@ -1,14 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -from django.urls import re_path - -from awx.sso.views import sso_complete, sso_error, sso_inactive - - -app_name = 'sso' -urlpatterns = [ - re_path(r'^complete/$', sso_complete, name='sso_complete'), - re_path(r'^error/$', sso_error, name='sso_error'), - re_path(r'^inactive/$', sso_inactive, name='sso_inactive'), -] diff --git a/awx/sso/validators.py b/awx/sso/validators.py deleted file mode 100644 index 07a582532a78..000000000000 --- a/awx/sso/validators.py +++ /dev/null @@ -1,5 +0,0 @@ -# Django -from django.core.exceptions import ValidationError -from django.utils.translation import gettext_lazy as _ - -__all__ = [] diff --git a/awx/sso/views.py b/awx/sso/views.py deleted file mode 100644 index ea291a28ba97..000000000000 --- a/awx/sso/views.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Python -import urllib.parse -import logging - -# Django -from django.urls import reverse -from django.views.generic.base import RedirectView -from django.utils.encoding import smart_str -from django.conf import settings - -logger = logging.getLogger('awx.sso.views') - - -class BaseRedirectView(RedirectView): - permanent = True - - def get_redirect_url(self, *args, **kwargs): - last_path = self.request.COOKIES.get('lastPath', '') - last_path = urllib.parse.quote(urllib.parse.unquote(last_path).strip('"')) - url = reverse('ui:index') - if last_path: - return '%s#%s' % (url, last_path) - else: - return url - - -sso_error = BaseRedirectView.as_view() -sso_inactive = BaseRedirectView.as_view() - - -class CompleteView(BaseRedirectView): - def dispatch(self, request, *args, **kwargs): - response = super(CompleteView, self).dispatch(request, *args, **kwargs) - if self.request.user and self.request.user.is_authenticated: - logger.info(smart_str(u"User {} logged in".format(self.request.user.username))) - response.set_cookie( - 'userLoggedIn', 'true', secure=getattr(settings, 'SESSION_COOKIE_SECURE', False), samesite=getattr(settings, 'USER_COOKIE_SAMESITE', 'Lax') - ) - response.setdefault('X-API-Session-Cookie-Name', getattr(settings, 'SESSION_COOKIE_NAME', 'awx_sessionid')) - return response - - -sso_complete = CompleteView.as_view() diff --git a/awx/urls.py b/awx/urls.py index 1eff5fb44ff9..daef360d5788 100644 --- a/awx/urls.py +++ b/awx/urls.py @@ -26,8 +26,6 @@ def get_urlpatterns(prefix=None): path(f'api{prefix}v2/', include(api_version_urls)), path(f'api{prefix}', include(api_urls)), path('', include(root_urls)), - re_path(r'^sso/', include('awx.sso.urls', namespace='sso')), - re_path(r'^sso/', include('social_django.urls', namespace='social')), re_path(r'^(?:api/)?400.html$', handle_400), re_path(r'^(?:api/)?403.html$', handle_403), re_path(r'^(?:api/)?404.html$', handle_404), @@ -36,7 +34,7 @@ def get_urlpatterns(prefix=None): re_path(r'^login/', handle_login_redirect), # want api/v2/doesnotexist to return a 404, not match the ui urls, # so use a negative lookahead assertion here - re_path(r'^(?!api/|sso/).*', include('awx.ui.urls', namespace='ui')), + re_path(r'^(?!api/).*', include('awx.ui.urls', namespace='ui')), ] if settings.SETTINGS_MODULE == 'awx.settings.development': diff --git a/awx/wsgi.py b/awx/wsgi.py index 4817fbae1e86..2fad3f27daf1 100644 --- a/awx/wsgi.py +++ b/awx/wsgi.py @@ -13,7 +13,6 @@ from django.conf import settings # NOQA from django.urls import resolve # NOQA from django.core.wsgi import get_wsgi_application # NOQA -import social_django # NOQA """ diff --git a/licenses/defusedxml.txt b/licenses/defusedxml.txt deleted file mode 100644 index 029a548be418..000000000000 --- a/licenses/defusedxml.txt +++ /dev/null @@ -1,48 +0,0 @@ -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF -hereby grants Licensee a nonexclusive, royalty-free, world-wide -license to reproduce, analyze, test, perform and/or display publicly, -prepare derivative works, distribute, and otherwise use Python -alone or in any derivative version, provided, however, that PSF's -License Agreement and PSF's notice of copyright, i.e., "Copyright (c) -2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008 Python Software Foundation; -All Rights Reserved" are retained in Python alone or in any derivative -version prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. diff --git a/licenses/python-jose.txt b/licenses/python-jose.txt deleted file mode 100644 index 59160df34b42..000000000000 --- a/licenses/python-jose.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2015 Michael Davis - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/licenses/social-auth-app-django.txt b/licenses/social-auth-app-django.txt deleted file mode 100644 index 796a37a54f3f..000000000000 --- a/licenses/social-auth-app-django.txt +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2012-2016, Matías Aguirre -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - 3. Neither the name of this project nor the names of its contributors may be - used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/licenses/social-auth-core.txt b/licenses/social-auth-core.txt deleted file mode 100644 index 284c8ac16593..000000000000 --- a/licenses/social-auth-core.txt +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2012-2016, Matías Aguirre -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - - 3. Neither the name of this project nor the names of its contributors may be - used to endorse or promote products derived from this software without - specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON -ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. \ No newline at end of file diff --git a/requirements/requirements.in b/requirements/requirements.in index c5256c937baf..e840bf0f6cc8 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -53,8 +53,6 @@ python-tss-sdk>=1.2.1 pyyaml>=6.0.1 pyzstd # otel collector log file compression library receptorctl -social-auth-core[openidconnect]==4.4.2 # see UPGRADE BLOCKERs -social-auth-app-django==5.4.0 # see UPGRADE BLOCKERs sqlparse>=0.4.4 # Required by django https://github.com/ansible/awx/security/dependabot/96 redis[hiredis] requests diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 730a5b93f716..7e0d8eb8f948 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -106,17 +106,12 @@ cryptography==41.0.7 # pyjwt # pyopenssl # service-identity - # social-auth-core cython==0.29.37 # via -r /awx_devel/requirements/requirements.in daphne==3.0.2 # via # -r /awx_devel/requirements/requirements.in # channels -defusedxml==0.7.1 - # via - # python3-openid - # social-auth-core deprecated==1.2.14 # via # opentelemetry-api @@ -137,7 +132,6 @@ django==4.2.10 # django-polymorphic # django-solo # djangorestframework - # social-auth-app-django # via -r /awx_devel/requirements/requirements_git.txt django-cors-headers==4.3.1 # via -r /awx_devel/requirements/requirements.in @@ -295,7 +289,6 @@ oauthlib==3.2.2 # django-oauth-toolkit # kubernetes # requests-oauthlib - # social-auth-core openshift==0.13.2 # via -r /awx_devel/requirements/requirements.in opentelemetry-api==1.24.0 @@ -382,7 +375,6 @@ pyjwt[crypto]==2.8.0 # adal # django-ansible-base # msal - # social-auth-core # twilio pyopenssl==24.0.0 # via @@ -402,14 +394,11 @@ python-dateutil==2.8.2 # receptorctl python-dsv-sdk==1.0.4 # via -r /awx_devel/requirements/requirements.in -python-jose==3.3.0 - # via social-auth-core python-string-utils==1.0.0 # via openshift python-tss-sdk==1.2.2 # via -r /awx_devel/requirements/requirements.in python3-openid==3.2.0 - # via social-auth-core # via -r /awx_devel/requirements/requirements_git.txt pytz==2024.1 # via @@ -448,13 +437,11 @@ requests==2.31.0 # python-dsv-sdk # python-tss-sdk # requests-oauthlib - # social-auth-core # twilio requests-oauthlib==1.3.1 # via # kubernetes # msrest - # social-auth-core rpds-py==0.18.0 # via # jsonschema @@ -490,12 +477,6 @@ slack-sdk==3.27.0 # via -r /awx_devel/requirements/requirements.in smmap==5.0.1 # via gitdb -social-auth-app-django==5.4.0 - # via -r /awx_devel/requirements/requirements.in -social-auth-core[openidconnect]==4.4.2 - # via - # -r /awx_devel/requirements/requirements.in - # social-auth-app-django sqlparse==0.4.4 # via # -r /awx_devel/requirements/requirements.in