diff --git a/.gitignore b/.gitignore index b752be13fd60..2ee95fb91e70 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,6 @@ tools/docker-compose/_build tools/docker-compose/_sources tools/docker-compose/overrides/ tools/docker-compose-minikube/_sources -tools/docker-compose/keycloak.awx.realm.json !tools/docker-compose/editable_dependencies tools/docker-compose/editable_dependencies/* diff --git a/Makefile b/Makefile index 60aae0395cb0..e9a149564489 100644 --- a/Makefile +++ b/Makefile @@ -31,10 +31,6 @@ COMPOSE_TAG ?= $(GIT_BRANCH) MAIN_NODE_TYPE ?= hybrid # If set to true docker-compose will also start a pgbouncer instance and use it PGBOUNCER ?= false -# If set to true docker-compose will also start a keycloak instance -KEYCLOAK ?= false -# If set to true docker-compose will also start an ldap instance -LDAP ?= false # If set to true docker-compose will also start a splunk instance SPLUNK ?= false # If set to true docker-compose will also start a prometheus instance @@ -45,8 +41,6 @@ GRAFANA ?= false VAULT ?= false # If set to true docker-compose will also start a hashicorp vault instance with TLS enabled VAULT_TLS ?= false -# If set to true docker-compose will also start a tacacs+ instance -TACACS ?= false # If set to true docker-compose will also start an OpenTelemetry Collector instance OTEL ?= false # If set to true docker-compose will also start a Loki instance @@ -345,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: @@ -446,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: @@ -507,14 +501,11 @@ docker-compose-sources: .git/hooks/pre-commit -e execution_node_count=$(EXECUTION_NODE_COUNT) \ -e minikube_container_group=$(MINIKUBE_CONTAINER_GROUP) \ -e enable_pgbouncer=$(PGBOUNCER) \ - -e enable_keycloak=$(KEYCLOAK) \ - -e enable_ldap=$(LDAP) \ -e enable_splunk=$(SPLUNK) \ -e enable_prometheus=$(PROMETHEUS) \ -e enable_grafana=$(GRAFANA) \ -e enable_vault=$(VAULT) \ -e vault_tls=$(VAULT_TLS) \ - -e enable_tacacs=$(TACACS) \ -e enable_otel=$(OTEL) \ -e enable_loki=$(LOKI) \ -e install_editable_dependencies=$(EDITABLE_DEPENDENCIES) \ @@ -525,8 +516,7 @@ docker-compose: awx/projects docker-compose-sources ansible-galaxy install --ignore-certs -r tools/docker-compose/ansible/requirements.yml; $(ANSIBLE_PLAYBOOK) -i tools/docker-compose/inventory tools/docker-compose/ansible/initialize_containers.yml \ -e enable_vault=$(VAULT) \ - -e vault_tls=$(VAULT_TLS) \ - -e enable_ldap=$(LDAP); \ + -e vault_tls=$(VAULT_TLS); \ $(MAKE) docker-compose-up docker-compose-up: @@ -598,7 +588,7 @@ docker-clean: -$(foreach image_id,$(shell docker images --filter=reference='*/*/*awx_devel*' --filter=reference='*/*awx_devel*' --filter=reference='*awx_devel*' -aq),docker rmi --force $(image_id);) docker-clean-volumes: docker-compose-clean docker-compose-container-group-clean - docker volume rm -f tools_var_lib_awx tools_awx_db tools_awx_db_15 tools_vault_1 tools_ldap_1 tools_grafana_storage tools_prometheus_storage $(shell docker volume ls --filter name=tools_redis_socket_ -q) + docker volume rm -f tools_var_lib_awx tools_awx_db tools_awx_db_15 tools_vault_1 tools_grafana_storage tools_prometheus_storage $(shell docker volume ls --filter name=tools_redis_socket_ -q) docker-refresh: docker-clean docker-compose diff --git a/awx/api/conf.py b/awx/api/conf.py index 72aaf3eec3d9..a1ed832ff662 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( @@ -35,10 +34,7 @@ 'DISABLE_LOCAL_AUTH', field_class=fields.BooleanField, label=_('Disable the built-in authentication system'), - help_text=_( - "Controls whether users are prevented from using the built-in authentication system. " - "You probably want to do this if you are using an LDAP or SAML integration." - ), + help_text=_("Controls whether users are prevented from using the built-in authentication system. "), category=_('Authentication'), category_slug='authentication', ) @@ -71,20 +67,6 @@ category_slug='authentication', unit=_('seconds'), ) -register( - 'ALLOW_OAUTH2_FOR_EXTERNAL_USERS', - field_class=fields.BooleanField, - default=False, - label=_('Allow External Users to Create OAuth2 Tokens'), - help_text=_( - 'For security reasons, users from external auth providers (LDAP, SAML, ' - 'SSO, Radius, and others) are not allowed to create OAuth2 tokens. ' - 'To change this behavior, enable this setting. Existing tokens will ' - 'not be deleted when this setting is toggled off.' - ), - category=_('Authentication'), - category_slug='authentication', -) register( 'LOGIN_REDIRECT_OVERRIDE', field_class=fields.CharField, @@ -109,7 +91,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/generics.py b/awx/api/generics.py index 9e1698a61f3a..a5196c1ea930 100644 --- a/awx/api/generics.py +++ b/awx/api/generics.py @@ -115,7 +115,6 @@ def post(self, request, *args, **kwargs): class LoggedLogoutView(auth_views.LogoutView): - success_url_allowed_hosts = set(settings.LOGOUT_ALLOWED_HOSTS.split(",")) if settings.LOGOUT_ALLOWED_HOSTS else set() def dispatch(self, request, *args, **kwargs): diff --git a/awx/api/serializers.py b/awx/api/serializers.py index 231bf3bbcda0..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,8 +959,6 @@ def get_types(self): class UserSerializer(BaseSerializer): password = serializers.CharField(required=False, default='', help_text=_('Field used to change the password.')) - ldap_dn = serializers.CharField(source='profile.ldap_dn', read_only=True) - 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,22 +975,13 @@ class Meta: 'is_superuser', 'is_system_auditor', 'password', - 'ldap_dn', '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): @@ -1027,10 +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 LDAP who already have an unusable password set. - # Get external password will return something like ldap or enterprise or None if the user isn't external. We only want to allow a password update for a None option - 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) @@ -1085,37 +1066,6 @@ def get_related(self, obj): ) return res - def _validate_ldap_managed_field(self, value, field_name): - if not getattr(settings, 'AUTH_LDAP_SERVER_URI', None): - return value - try: - is_ldap_user = bool(self.instance and self.instance.profile.ldap_dn) - except AttributeError: - is_ldap_user = False - if is_ldap_user: - ldap_managed_fields = ['username'] - ldap_managed_fields.extend(getattr(settings, 'AUTH_LDAP_USER_ATTR_MAP', {}).keys()) - ldap_managed_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys()) - if field_name in ldap_managed_fields: - if value != getattr(self.instance, field_name): - raise serializers.ValidationError(_('Unable to change %s on user managed by LDAP.') % field_name) - return value - - def validate_username(self, value): - return self._validate_ldap_managed_field(value, 'username') - - def validate_first_name(self, value): - return self._validate_ldap_managed_field(value, 'first_name') - - def validate_last_name(self, value): - return self._validate_ldap_managed_field(value, 'last_name') - - def validate_email(self, value): - return self._validate_ldap_managed_field(value, 'email') - - def validate_is_superuser(self, value): - return self._validate_ldap_managed_field(value, 'is_superuser') - class UserActivityStreamSerializer(UserSerializer): """Changes to system auditor status are shown as separate entries, diff --git a/awx/api/urls/urls.py b/awx/api/urls/urls.py index c2218e5ed865..3f257da9560b 100644 --- a/awx/api/urls/urls.py +++ b/awx/api/urls/urls.py @@ -15,7 +15,6 @@ ApiV2AttachView, ) from awx.api.views import ( - AuthView, UserMeList, DashboardView, DashboardJobsGraphView, @@ -106,7 +105,6 @@ re_path(r'^config/$', ApiV2ConfigView.as_view(), name='api_v2_config_view'), re_path(r'^config/subscriptions/$', ApiV2SubscriptionView.as_view(), name='api_v2_subscription_view'), re_path(r'^config/attach/$', ApiV2AttachView.as_view(), name='api_v2_attach_view'), - re_path(r'^auth/$', AuthView.as_view()), re_path(r'^me/$', UserMeList.as_view(), name='user_me_list'), re_path(r'^dashboard/$', DashboardView.as_view(), name='dashboard_view'), re_path(r'^dashboard/graphs/jobs/$', DashboardJobsGraphView.as_view(), name='dashboard_jobs_graph_view'), diff --git a/awx/api/views/__init__.py b/awx/api/views/__init__.py index b93a7d8d5e53..bbe79bd2a453 100644 --- a/awx/api/views/__init__.py +++ b/awx/api/views/__init__.py @@ -36,7 +36,7 @@ # Django REST Framework from rest_framework.exceptions import APIException, PermissionDenied, ParseError, NotFound from rest_framework.parsers import FormParser -from rest_framework.permissions import AllowAny, IsAuthenticated +from rest_framework.permissions import IsAuthenticated from rest_framework.renderers import JSONRenderer, StaticHTMLRenderer from rest_framework.response import Response from rest_framework.settings import api_settings @@ -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 @@ -676,41 +673,6 @@ class ScheduleUnifiedJobsList(SubListAPIView): name = _('Schedule Jobs List') -class AuthView(APIView): - '''List enabled single-sign-on endpoints''' - - authentication_classes = [] - permission_classes = (AllowAny,) - 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: Google, GitHub, SAML. - auth_backends.sort(key=lambda x: 'g' if x[0] == 'google-oauth2' else 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 name == 'saml': - backend_data['metadata_url'] = reverse('sso:saml_metadata') - for idp in sorted(settings.SOCIAL_AUTH_SAML_ENABLED_IDPS.keys()): - saml_backend_data = dict(backend_data.items()) - saml_backend_data['login_url'] = '%s?idp=%s' % (login_url, idp) - full_backend_name = '%s:%s' % (name, idp) - if (err_backend == full_backend_name or err_backend == name) and err_message: - saml_backend_data['error'] = err_message - data[full_backend_name] = saml_backend_data - else: - if err_backend == name and err_message: - backend_data['error'] = err_message - data[name] = backend_data - return Response(data) - - def immutablesharedfields(cls): ''' Class decorator to prevent modifying shared resources when ALLOW_LOCAL_RESOURCE_MANAGEMENT setting is set to False. diff --git a/awx/api/views/root.py b/awx/api/views/root.py index e55461923e8b..d0f3a88e4c21 100644 --- a/awx/api/views/root.py +++ b/awx/api/views/root.py @@ -295,15 +295,6 @@ def get(self, request, format=None): become_methods=PRIVILEGE_ESCALATION_METHODS, ) - # If LDAP is enabled, user_ldap_fields will return a list of field - # names that are managed by LDAP and should be read-only for users with - # a non-empty ldap_dn attribute. - if getattr(settings, 'AUTH_LDAP_SERVER_URI', None): - user_ldap_fields = ['username', 'password'] - user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_ATTR_MAP', {}).keys()) - user_ldap_fields.extend(getattr(settings, 'AUTH_LDAP_USER_FLAGS_BY_GROUP', {}).keys()) - data['user_ldap_fields'] = user_ldap_fields - if ( request.user.is_superuser or request.user.is_system_auditor diff --git a/awx/conf/migrations/0006_v331_ldap_group_type.py b/awx/conf/migrations/0006_v331_ldap_group_type.py index 9b7bf4e6ecc1..f70b3db273c1 100644 --- a/awx/conf/migrations/0006_v331_ldap_group_type.py +++ b/awx/conf/migrations/0006_v331_ldap_group_type.py @@ -1,13 +1,11 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals -# AWX -from awx.conf.migrations._ldap_group_type import fill_ldap_group_type_params - from django.db import migrations class Migration(migrations.Migration): dependencies = [('conf', '0005_v330_rename_two_session_settings')] - operations = [migrations.RunPython(fill_ldap_group_type_params)] + # this migration is doing nothing, and is here to preserve migrations files integrity + operations = [] diff --git a/awx/conf/migrations/0011_remove_ldap_auth_conf.py b/awx/conf/migrations/0011_remove_ldap_auth_conf.py new file mode 100644 index 000000000000..e8955635af63 --- /dev/null +++ b/awx/conf/migrations/0011_remove_ldap_auth_conf.py @@ -0,0 +1,115 @@ +from django.db import migrations + +LDAP_AUTH_CONF_KEYS = [ + 'AUTH_LDAP_SERVER_URI', + 'AUTH_LDAP_BIND_DN', + 'AUTH_LDAP_BIND_PASSWORD', + 'AUTH_LDAP_START_TLS', + 'AUTH_LDAP_CONNECTION_OPTIONS', + 'AUTH_LDAP_USER_SEARCH', + 'AUTH_LDAP_USER_DN_TEMPLATE', + 'AUTH_LDAP_USER_ATTR_MAP', + 'AUTH_LDAP_GROUP_SEARCH', + 'AUTH_LDAP_GROUP_TYPE', + 'AUTH_LDAP_GROUP_TYPE_PARAMS', + 'AUTH_LDAP_REQUIRE_GROUP', + 'AUTH_LDAP_DENY_GROUP', + 'AUTH_LDAP_USER_FLAGS_BY_GROUP', + 'AUTH_LDAP_ORGANIZATION_MAP', + 'AUTH_LDAP_TEAM_MAP', + 'AUTH_LDAP_1_SERVER_URI', + 'AUTH_LDAP_1_BIND_DN', + 'AUTH_LDAP_1_BIND_PASSWORD', + 'AUTH_LDAP_1_START_TLS', + 'AUTH_LDAP_1_CONNECTION_OPTIONS', + 'AUTH_LDAP_1_USER_SEARCH', + 'AUTH_LDAP_1_USER_DN_TEMPLATE', + 'AUTH_LDAP_1_USER_ATTR_MAP', + 'AUTH_LDAP_1_GROUP_SEARCH', + 'AUTH_LDAP_1_GROUP_TYPE', + 'AUTH_LDAP_1_GROUP_TYPE_PARAMS', + 'AUTH_LDAP_1_REQUIRE_GROUP', + 'AUTH_LDAP_1_DENY_GROUP', + 'AUTH_LDAP_1_USER_FLAGS_BY_GROUP', + 'AUTH_LDAP_1_ORGANIZATION_MAP', + 'AUTH_LDAP_1_TEAM_MAP', + 'AUTH_LDAP_2_SERVER_URI', + 'AUTH_LDAP_2_BIND_DN', + 'AUTH_LDAP_2_BIND_PASSWORD', + 'AUTH_LDAP_2_START_TLS', + 'AUTH_LDAP_2_CONNECTION_OPTIONS', + 'AUTH_LDAP_2_USER_SEARCH', + 'AUTH_LDAP_2_USER_DN_TEMPLATE', + 'AUTH_LDAP_2_USER_ATTR_MAP', + 'AUTH_LDAP_2_GROUP_SEARCH', + 'AUTH_LDAP_2_GROUP_TYPE', + 'AUTH_LDAP_2_GROUP_TYPE_PARAMS', + 'AUTH_LDAP_2_REQUIRE_GROUP', + 'AUTH_LDAP_2_DENY_GROUP', + 'AUTH_LDAP_2_USER_FLAGS_BY_GROUP', + 'AUTH_LDAP_2_ORGANIZATION_MAP', + 'AUTH_LDAP_2_TEAM_MAP', + 'AUTH_LDAP_3_SERVER_URI', + 'AUTH_LDAP_3_BIND_DN', + 'AUTH_LDAP_3_BIND_PASSWORD', + 'AUTH_LDAP_3_START_TLS', + 'AUTH_LDAP_3_CONNECTION_OPTIONS', + 'AUTH_LDAP_3_USER_SEARCH', + 'AUTH_LDAP_3_USER_DN_TEMPLATE', + 'AUTH_LDAP_3_USER_ATTR_MAP', + 'AUTH_LDAP_3_GROUP_SEARCH', + 'AUTH_LDAP_3_GROUP_TYPE', + 'AUTH_LDAP_3_GROUP_TYPE_PARAMS', + 'AUTH_LDAP_3_REQUIRE_GROUP', + 'AUTH_LDAP_3_DENY_GROUP', + 'AUTH_LDAP_3_USER_FLAGS_BY_GROUP', + 'AUTH_LDAP_3_ORGANIZATION_MAP', + 'AUTH_LDAP_3_TEAM_MAP', + 'AUTH_LDAP_4_SERVER_URI', + 'AUTH_LDAP_4_BIND_DN', + 'AUTH_LDAP_4_BIND_PASSWORD', + 'AUTH_LDAP_4_START_TLS', + 'AUTH_LDAP_4_CONNECTION_OPTIONS', + 'AUTH_LDAP_4_USER_SEARCH', + 'AUTH_LDAP_4_USER_DN_TEMPLATE', + 'AUTH_LDAP_4_USER_ATTR_MAP', + 'AUTH_LDAP_4_GROUP_SEARCH', + 'AUTH_LDAP_4_GROUP_TYPE', + 'AUTH_LDAP_4_GROUP_TYPE_PARAMS', + 'AUTH_LDAP_4_REQUIRE_GROUP', + 'AUTH_LDAP_4_DENY_GROUP', + 'AUTH_LDAP_4_USER_FLAGS_BY_GROUP', + 'AUTH_LDAP_4_ORGANIZATION_MAP', + 'AUTH_LDAP_4_TEAM_MAP', + 'AUTH_LDAP_5_SERVER_URI', + 'AUTH_LDAP_5_BIND_DN', + 'AUTH_LDAP_5_BIND_PASSWORD', + 'AUTH_LDAP_5_START_TLS', + 'AUTH_LDAP_5_CONNECTION_OPTIONS', + 'AUTH_LDAP_5_USER_SEARCH', + 'AUTH_LDAP_5_USER_DN_TEMPLATE', + 'AUTH_LDAP_5_USER_ATTR_MAP', + 'AUTH_LDAP_5_GROUP_SEARCH', + 'AUTH_LDAP_5_GROUP_TYPE', + 'AUTH_LDAP_5_GROUP_TYPE_PARAMS', + 'AUTH_LDAP_5_REQUIRE_GROUP', + 'AUTH_LDAP_5_DENY_GROUP', + 'AUTH_LDAP_5_USER_FLAGS_BY_GROUP', + 'AUTH_LDAP_5_ORGANIZATION_MAP', + 'AUTH_LDAP_5_TEAM_MAP', +] + + +def remove_ldap_auth_conf(apps, scheme_editor): + setting = apps.get_model('conf', 'Setting') + setting.objects.filter(key__in=LDAP_AUTH_CONF_KEYS).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('conf', '0010_change_to_JSONField'), + ] + + operations = [ + migrations.RunPython(remove_ldap_auth_conf), + ] diff --git a/awx/conf/migrations/0012_remove_oidc_auth_conf.py b/awx/conf/migrations/0012_remove_oidc_auth_conf.py new file mode 100644 index 000000000000..be80ef7a989b --- /dev/null +++ b/awx/conf/migrations/0012_remove_oidc_auth_conf.py @@ -0,0 +1,20 @@ +# Generated by Django 4.2.10 on 2024-08-27 19:31 + +from django.db import migrations + +OIDC_AUTH_CONF_KEYS = ['SOCIAL_AUTH_OIDC_KEY', 'SOCIAL_AUTH_OIDC_SECRET', 'SOCIAL_AUTH_OIDC_OIDC_ENDPOINT', 'SOCIAL_AUTH_OIDC_VERIFY_SSL'] + + +def remove_oidc_auth_conf(apps, scheme_editor): + setting = apps.get_model('conf', 'Setting') + setting.objects.filter(key__in=OIDC_AUTH_CONF_KEYS).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('conf', '0011_remove_ldap_auth_conf'), + ] + + operations = [ + migrations.RunPython(remove_oidc_auth_conf), + ] diff --git a/awx/conf/migrations/0013_remove_radius_auth_conf.py b/awx/conf/migrations/0013_remove_radius_auth_conf.py new file mode 100644 index 000000000000..ac8dd5aafd6a --- /dev/null +++ b/awx/conf/migrations/0013_remove_radius_auth_conf.py @@ -0,0 +1,22 @@ +from django.db import migrations + +RADIUS_AUTH_CONF_KEYS = [ + 'RADIUS_SERVER', + 'RADIUS_PORT', + 'RADIUS_SECRET', +] + + +def remove_radius_auth_conf(apps, scheme_editor): + setting = apps.get_model('conf', 'Setting') + setting.objects.filter(key__in=RADIUS_AUTH_CONF_KEYS).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('conf', '0012_remove_oidc_auth_conf'), + ] + + operations = [ + migrations.RunPython(remove_radius_auth_conf), + ] diff --git a/awx/conf/migrations/0014_remove_saml_auth_conf.py b/awx/conf/migrations/0014_remove_saml_auth_conf.py new file mode 100644 index 000000000000..a63ac02804eb --- /dev/null +++ b/awx/conf/migrations/0014_remove_saml_auth_conf.py @@ -0,0 +1,39 @@ +# Generated by Django 4.2.10 on 2024-08-27 14:20 + +from django.db import migrations + +SAML_AUTH_CONF_KEYS = [ + 'SAML_AUTO_CREATE_OBJECTS', + 'SOCIAL_AUTH_SAML_CALLBACK_URL', + 'SOCIAL_AUTH_SAML_METADATA_URL', + 'SOCIAL_AUTH_SAML_SP_ENTITY_ID', + 'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT', + 'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY', + 'SOCIAL_AUTH_SAML_ORG_INFO', + 'SOCIAL_AUTH_SAML_TECHNICAL_CONTACT', + 'SOCIAL_AUTH_SAML_SUPPORT_CONTACT', + 'SOCIAL_AUTH_SAML_ENABLED_IDPS', + 'SOCIAL_AUTH_SAML_SECURITY_CONFIG', + 'SOCIAL_AUTH_SAML_SP_EXTRA', + 'SOCIAL_AUTH_SAML_EXTRA_DATA', + 'SOCIAL_AUTH_SAML_ORGANIZATION_MAP', + 'SOCIAL_AUTH_SAML_TEAM_MAP', + 'SOCIAL_AUTH_SAML_ORGANIZATION_ATTR', + 'SOCIAL_AUTH_SAML_TEAM_ATTR', + 'SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR', +] + + +def remove_saml_auth_conf(apps, scheme_editor): + setting = apps.get_model('conf', 'Setting') + setting.objects.filter(key__in=SAML_AUTH_CONF_KEYS).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('conf', '0013_remove_radius_auth_conf'), + ] + + operations = [ + migrations.RunPython(remove_saml_auth_conf), + ] diff --git a/awx/conf/migrations/0015_remove_social_oauth_conf.py b/awx/conf/migrations/0015_remove_social_oauth_conf.py new file mode 100644 index 000000000000..ce8ceb716306 --- /dev/null +++ b/awx/conf/migrations/0015_remove_social_oauth_conf.py @@ -0,0 +1,81 @@ +# Generated by Django 4.2.10 on 2024-08-13 11:14 + +from django.db import migrations + +SOCIAL_OAUTH_CONF_KEYS = [ + # MICROSOFT AZURE ACTIVE DIRECTORY SETTINGS + 'SOCIAL_AUTH_AZUREAD_OAUTH2_CALLBACK_URL', + 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', + 'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET', + 'SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP', + 'SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP', + # GOOGLE OAUTH2 AUTHENTICATION SETTINGS + 'SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL', + 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', + 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET', + 'SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS', + 'SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS', + 'SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP', + 'SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP', + # GITHUB OAUTH2 AUTHENTICATION SETTINGS + 'SOCIAL_AUTH_GITHUB_CALLBACK_URL', + 'SOCIAL_AUTH_GITHUB_KEY', + 'SOCIAL_AUTH_GITHUB_SECRET', + 'SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP', + 'SOCIAL_AUTH_GITHUB_TEAM_MAP', + # GITHUB ORG OAUTH2 AUTHENTICATION SETTINGS + 'SOCIAL_AUTH_GITHUB_ORG_CALLBACK_URL', + 'SOCIAL_AUTH_GITHUB_ORG_KEY', + 'SOCIAL_AUTH_GITHUB_ORG_SECRET', + 'SOCIAL_AUTH_GITHUB_ORG_NAME', + 'SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP', + 'SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP', + # GITHUB TEAM OAUTH2 AUTHENTICATION SETTINGS + 'SOCIAL_AUTH_GITHUB_TEAM_CALLBACK_URL', + 'SOCIAL_AUTH_GITHUB_TEAM_KEY', + 'SOCIAL_AUTH_GITHUB_TEAM_SECRET', + 'SOCIAL_AUTH_GITHUB_TEAM_ID', + 'SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP', + 'SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP', + # GITHUB ENTERPRISE OAUTH2 AUTHENTICATION SETTINGS + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_CALLBACK_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP', + # GITHUB ENTERPRISE ORG OAUTH2 AUTHENTICATION SETTINGS + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_CALLBACK_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP', + # GITHUB ENTERPRISE TEAM OAUTH2 AUTHENTICATION SETTINGS + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_CALLBACK_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP', + 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP', +] + + +def remove_social_oauth_conf(apps, scheme_editor): + setting = apps.get_model('conf', 'Setting') + setting.objects.filter(key__in=SOCIAL_OAUTH_CONF_KEYS).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('conf', '0014_remove_saml_auth_conf'), + ] + + operations = [ + migrations.RunPython(remove_social_oauth_conf), + ] diff --git a/awx/conf/migrations/0016_remove_tacacs_plus_auth_conf.py b/awx/conf/migrations/0016_remove_tacacs_plus_auth_conf.py new file mode 100644 index 000000000000..ae7b8d181906 --- /dev/null +++ b/awx/conf/migrations/0016_remove_tacacs_plus_auth_conf.py @@ -0,0 +1,25 @@ +from django.db import migrations + +TACACS_PLUS_AUTH_CONF_KEYS = [ + 'TACACSPLUS_HOST', + 'TACACSPLUS_PORT', + 'TACACSPLUS_SECRET', + 'TACACSPLUS_SESSION_TIMEOUT', + 'TACACSPLUS_AUTH_PROTOCOL', + 'TACACSPLUS_REM_ADDR', +] + + +def remove_tacacs_plus_auth_conf(apps, scheme_editor): + setting = apps.get_model('conf', 'Setting') + setting.objects.filter(key__in=TACACS_PLUS_AUTH_CONF_KEYS).delete() + + +class Migration(migrations.Migration): + dependencies = [ + ('conf', '0015_remove_social_oauth_conf'), + ] + + operations = [ + migrations.RunPython(remove_tacacs_plus_auth_conf), + ] diff --git a/awx/conf/migrations/_ldap_group_type.py b/awx/conf/migrations/_ldap_group_type.py deleted file mode 100644 index 378f934342fb..000000000000 --- a/awx/conf/migrations/_ldap_group_type.py +++ /dev/null @@ -1,31 +0,0 @@ -import inspect - -from django.conf import settings - -import logging - - -logger = logging.getLogger('awx.conf.migrations') - - -def fill_ldap_group_type_params(apps, schema_editor): - group_type = getattr(settings, 'AUTH_LDAP_GROUP_TYPE', None) - Setting = apps.get_model('conf', 'Setting') - - group_type_params = {'name_attr': 'cn', 'member_attr': 'member'} - qs = Setting.objects.filter(key='AUTH_LDAP_GROUP_TYPE_PARAMS') - entry = None - if qs.exists(): - entry = qs[0] - group_type_params = entry.value - else: - return # for new installs we prefer to use the default value - - init_attrs = set(inspect.getfullargspec(group_type.__init__).args[1:]) - for k in list(group_type_params.keys()): - if k not in init_attrs: - del group_type_params[k] - - entry.value = group_type_params - logger.warning(f'Migration updating AUTH_LDAP_GROUP_TYPE_PARAMS with value {entry.value}') - entry.save() diff --git a/awx/conf/signals.py b/awx/conf/signals.py index d8297becb40e..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(profile__ldap_dn='', 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/conf/tests/functional/test_api.py b/awx/conf/tests/functional/test_api.py index b600c3766db6..183fd7b8b682 100644 --- a/awx/conf/tests/functional/test_api.py +++ b/awx/conf/tests/functional/test_api.py @@ -8,7 +8,6 @@ from awx.conf import fields from awx.conf.registry import settings_registry from awx.conf.models import Setting -from awx.sso import fields as sso_fields @pytest.fixture @@ -103,24 +102,6 @@ def test_setting_singleton_update(api_request, dummy_setting): assert response.data['FOO_BAR'] == 4 -@pytest.mark.django_db -def test_setting_singleton_update_hybriddictfield_with_forbidden(api_request, dummy_setting): - # Some HybridDictField subclasses have a child of _Forbidden, - # indicating that only the defined fields can be filled in. Make - # sure that the _Forbidden validator doesn't get used for the - # fields. See also https://github.com/ansible/awx/issues/4099. - with dummy_setting('FOO_BAR', field_class=sso_fields.SAMLOrgAttrField, category='FooBar', category_slug='foobar'), mock.patch( - 'awx.conf.views.clear_setting_cache' - ): - api_request( - 'patch', - reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'}), - data={'FOO_BAR': {'saml_admin_attr': 'Admins', 'saml_attr': 'Orgs'}}, - ) - response = api_request('get', reverse('api:setting_singleton_detail', kwargs={'category_slug': 'foobar'})) - assert response.data['FOO_BAR'] == {'saml_admin_attr': 'Admins', 'saml_attr': 'Orgs'} - - @pytest.mark.django_db def test_setting_singleton_update_dont_change_readonly_fields(api_request, dummy_setting): with dummy_setting('FOO_BAR', field_class=fields.IntegerField, read_only=True, default=4, category='FooBar', category_slug='foobar'), mock.patch( diff --git a/awx/conf/tests/functional/test_migrations.py b/awx/conf/tests/functional/test_migrations.py deleted file mode 100644 index d3fddb292bd1..000000000000 --- a/awx/conf/tests/functional/test_migrations.py +++ /dev/null @@ -1,25 +0,0 @@ -import pytest - -from awx.conf.migrations._ldap_group_type import fill_ldap_group_type_params -from awx.conf.models import Setting - -from django.apps import apps - - -@pytest.mark.django_db -def test_fill_group_type_params_no_op(): - fill_ldap_group_type_params(apps, 'dont-use-me') - assert Setting.objects.count() == 0 - - -@pytest.mark.django_db -def test_keep_old_setting_with_default_value(): - Setting.objects.create(key='AUTH_LDAP_GROUP_TYPE', value={'name_attr': 'cn', 'member_attr': 'member'}) - fill_ldap_group_type_params(apps, 'dont-use-me') - assert Setting.objects.count() == 1 - s = Setting.objects.first() - assert s.value == {'name_attr': 'cn', 'member_attr': 'member'} - - -# NOTE: would be good to test the removal of attributes by migration -# but this requires fighting with the validator and is not done here diff --git a/awx/conf/tests/unit/test_fields.py b/awx/conf/tests/unit/test_fields.py index 28c54dc6177a..86f241a1085f 100644 --- a/awx/conf/tests/unit/test_fields.py +++ b/awx/conf/tests/unit/test_fields.py @@ -111,7 +111,6 @@ class TestURLField: @pytest.mark.parametrize( "url,schemes,regex, allow_numbers_in_top_level_domain, expect_no_error", [ - ("ldap://www.example.org42", "ldap", None, True, True), ("https://www.example.org42", "https", None, False, False), ("https://www.example.org", None, regex, None, True), ("https://www.example3.org", None, regex, None, False), diff --git a/awx/main/access.py b/awx/main/access.py index 3a217fe2afa4..74c604436807 100644 --- a/awx/main/access.py +++ b/awx/main/access.py @@ -642,10 +642,7 @@ class UserAccess(BaseAccess): """ model = User - prefetch_related = ( - 'profile', - 'resource', - ) + prefetch_related = ('resource',) def filtered_queryset(self): if settings.ORG_ADMINS_CAN_SEE_ALL_USERS and (self.user.admin_of_organizations.exists() or self.user.auditor_of_organizations.exists()): diff --git a/awx/main/conf.py b/awx/main/conf.py index 4885e3e0a3b6..3f601b6d095a 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -46,10 +46,7 @@ 'MANAGE_ORGANIZATION_AUTH', field_class=fields.BooleanField, label=_('Organization Admins Can Manage Users and Teams'), - help_text=_( - 'Controls whether any Organization Admin has the privileges to create and manage users and teams. ' - 'You may want to disable this ability if you are using an LDAP or SAML integration.' - ), + help_text=_('Controls whether any Organization Admin has the privileges to create and manage users and teams.'), category=_('System'), category_slug='system', ) diff --git a/awx/main/management/commands/dump_auth_config.py b/awx/main/management/commands/dump_auth_config.py deleted file mode 100644 index 45afc9b41d41..000000000000 --- a/awx/main/management/commands/dump_auth_config.py +++ /dev/null @@ -1,195 +0,0 @@ -import json -import os -import sys -import re -from typing import Any - -from django.core.management.base import BaseCommand -from django.conf import settings - -from awx.conf import settings_registry - - -class Command(BaseCommand): - help = 'Dump the current auth configuration in django_ansible_base.authenticator format, currently supports LDAP and SAML' - - DAB_SAML_AUTHENTICATOR_KEYS = { - "SP_ENTITY_ID": True, - "SP_PUBLIC_CERT": True, - "SP_PRIVATE_KEY": True, - "ORG_INFO": True, - "TECHNICAL_CONTACT": True, - "SUPPORT_CONTACT": True, - "SP_EXTRA": False, - "SECURITY_CONFIG": False, - "EXTRA_DATA": False, - "ENABLED_IDPS": True, - "CALLBACK_URL": False, - } - - DAB_LDAP_AUTHENTICATOR_KEYS = { - "SERVER_URI": True, - "BIND_DN": False, - "BIND_PASSWORD": False, - "CONNECTION_OPTIONS": False, - "GROUP_TYPE": True, - "GROUP_TYPE_PARAMS": True, - "GROUP_SEARCH": False, - "START_TLS": False, - "USER_DN_TEMPLATE": True, - "USER_ATTR_MAP": True, - "USER_SEARCH": False, - } - - def is_enabled(self, settings, keys): - missing_fields = [] - for key, required in keys.items(): - if required and not settings.get(key): - missing_fields.append(key) - if missing_fields: - return False, missing_fields - return True, None - - def get_awx_ldap_settings(self) -> dict[str, dict[str, Any]]: - awx_ldap_settings = {} - - for awx_ldap_setting in settings_registry.get_registered_settings(category_slug='ldap'): - key = awx_ldap_setting.removeprefix("AUTH_LDAP_") - value = getattr(settings, awx_ldap_setting, None) - awx_ldap_settings[key] = value - - grouped_settings = {} - - for key, value in awx_ldap_settings.items(): - match = re.search(r'(\d+)', key) - index = int(match.group()) if match else 0 - new_key = re.sub(r'\d+_', '', key) - - if index not in grouped_settings: - grouped_settings[index] = {} - - grouped_settings[index][new_key] = value - if new_key == "GROUP_TYPE" and value: - grouped_settings[index][new_key] = type(value).__name__ - - if new_key == "SERVER_URI" and value: - value = value.split(", ") - grouped_settings[index][new_key] = value - - if type(value).__name__ == "LDAPSearch": - data = [] - data.append(value.base_dn) - data.append("SCOPE_SUBTREE") - data.append(value.filterstr) - grouped_settings[index][new_key] = data - - return grouped_settings - - def get_awx_saml_settings(self) -> dict[str, Any]: - awx_saml_settings = {} - for awx_saml_setting in settings_registry.get_registered_settings(category_slug='saml'): - awx_saml_settings[awx_saml_setting.removeprefix("SOCIAL_AUTH_SAML_")] = getattr(settings, awx_saml_setting, None) - - return awx_saml_settings - - def format_config_data(self, enabled, awx_settings, type, keys, name): - config = { - "type": f"ansible_base.authentication.authenticator_plugins.{type}", - "name": name, - "enabled": enabled, - "create_objects": True, - "users_unique": False, - "remove_users": True, - "configuration": {}, - } - for k in keys: - v = awx_settings.get(k) - config["configuration"].update({k: v}) - - if type == "saml": - idp_to_key_mapping = { - "url": "IDP_URL", - "x509cert": "IDP_X509_CERT", - "entity_id": "IDP_ENTITY_ID", - "attr_email": "IDP_ATTR_EMAIL", - "attr_groups": "IDP_GROUPS", - "attr_username": "IDP_ATTR_USERNAME", - "attr_last_name": "IDP_ATTR_LAST_NAME", - "attr_first_name": "IDP_ATTR_FIRST_NAME", - "attr_user_permanent_id": "IDP_ATTR_USER_PERMANENT_ID", - } - for idp_name in awx_settings.get("ENABLED_IDPS", {}): - for key in idp_to_key_mapping: - value = awx_settings["ENABLED_IDPS"][idp_name].get(key) - if value is not None: - config["name"] = idp_name - config["configuration"].update({idp_to_key_mapping[key]: value}) - - return config - - def add_arguments(self, parser): - parser.add_argument( - "output_file", - nargs="?", - type=str, - default=None, - help="Output JSON file path", - ) - - def handle(self, *args, **options): - try: - data = [] - - # dump SAML settings - awx_saml_settings = self.get_awx_saml_settings() - awx_saml_enabled, saml_missing_fields = self.is_enabled(awx_saml_settings, self.DAB_SAML_AUTHENTICATOR_KEYS) - if awx_saml_enabled: - awx_saml_name = awx_saml_settings["ENABLED_IDPS"] - data.append( - self.format_config_data( - awx_saml_enabled, - awx_saml_settings, - "saml", - self.DAB_SAML_AUTHENTICATOR_KEYS, - awx_saml_name, - ) - ) - else: - data.append({"SAML_missing_fields": saml_missing_fields}) - - # dump LDAP settings - awx_ldap_group_settings = self.get_awx_ldap_settings() - for awx_ldap_name, awx_ldap_settings in awx_ldap_group_settings.items(): - awx_ldap_enabled, ldap_missing_fields = self.is_enabled(awx_ldap_settings, self.DAB_LDAP_AUTHENTICATOR_KEYS) - if awx_ldap_enabled: - data.append( - self.format_config_data( - awx_ldap_enabled, - awx_ldap_settings, - "ldap", - self.DAB_LDAP_AUTHENTICATOR_KEYS, - f"LDAP_{awx_ldap_name}", - ) - ) - else: - data.append({f"LDAP_{awx_ldap_name}_missing_fields": ldap_missing_fields}) - - # write to file if requested - if options["output_file"]: - # Define the path for the output JSON file - output_file = options["output_file"] - - # Ensure the directory exists - os.makedirs(os.path.dirname(output_file), exist_ok=True) - - # Write data to the JSON file - with open(output_file, "w") as f: - json.dump(data, f, indent=4) - - self.stdout.write(self.style.SUCCESS(f"Auth config data dumped to {output_file}")) - else: - self.stdout.write(json.dumps(data, indent=4)) - - except Exception as e: - self.stdout.write(self.style.ERROR(f"An error occurred: {str(e)}")) - sys.exit(1) diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 433ade596fe4..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.profile.ldap_dn or user.social_auth.exists() or user.enterprise_auth.exists()): - logout(request) + + logout(request) class URLModificationMiddleware(MiddlewareMixin): diff --git a/awx/main/migrations/0193_alter_notification_notification_type_and_more.py b/awx/main/migrations/0193_alter_notification_notification_type_and_more.py index 59fde544c8e9..8c76527f8de2 100644 --- a/awx/main/migrations/0193_alter_notification_notification_type_and_more.py +++ b/awx/main/migrations/0193_alter_notification_notification_type_and_more.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ('main', '0192_custom_roles'), ] diff --git a/awx/main/migrations/0194_alter_inventorysource_source_and_more.py b/awx/main/migrations/0194_alter_inventorysource_source_and_more.py index d6f399d71c5a..41841e093eb3 100644 --- a/awx/main/migrations/0194_alter_inventorysource_source_and_more.py +++ b/awx/main/migrations/0194_alter_inventorysource_source_and_more.py @@ -4,7 +4,6 @@ class Migration(migrations.Migration): - dependencies = [ ('main', '0193_alter_notification_notification_type_and_more'), ] diff --git a/awx/main/migrations/0195_EE_permissions.py b/awx/main/migrations/0195_EE_permissions.py index 8216d474f30f..39a0aed1f934 100644 --- a/awx/main/migrations/0195_EE_permissions.py +++ b/awx/main/migrations/0195_EE_permissions.py @@ -12,7 +12,6 @@ def delete_execution_environment_read_role(apps, schema_editor): class Migration(migrations.Migration): - dependencies = [ ('main', '0194_alter_inventorysource_source_and_more'), ] diff --git a/awx/main/migrations/0196_delete_profile.py b/awx/main/migrations/0196_delete_profile.py new file mode 100644 index 000000000000..726cc05d1cd1 --- /dev/null +++ b/awx/main/migrations/0196_delete_profile.py @@ -0,0 +1,15 @@ +# Generated by Django 4.2.10 on 2024-09-16 10:22 + +from django.db import migrations + + +class Migration(migrations.Migration): + dependencies = [ + ('main', '0195_EE_permissions'), + ] + + operations = [ + migrations.DeleteModel( + name='Profile', + ), + ] 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..80b301f517d4 --- /dev/null +++ b/awx/main/migrations/0197_remove_sso_app_content.py @@ -0,0 +1,26 @@ +# 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 a799b077f30a..a63cc31bf877 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -18,7 +18,7 @@ # AWX from awx.main.models.base import BaseModel, PrimordialModel, accepts_json, CLOUD_INVENTORY_SOURCES, VERBOSITY_CHOICES # noqa from awx.main.models.unified_jobs import UnifiedJob, UnifiedJobTemplate, StdoutMaxBytesExceeded # noqa -from awx.main.models.organization import Organization, Profile, Team, UserSessionMembership # noqa +from awx.main.models.organization import Organization, Team, UserSessionMembership # noqa from awx.main.models.credential import Credential, CredentialType, CredentialInputSource, ManagedCredentialType, build_safe_env # noqa from awx.main.models.projects import Project, ProjectUpdate # noqa from awx.main.models.receptor_address import ReceptorAddress # noqa @@ -244,20 +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-else block ensures existing enterprise users are still able to - # log in. Remove it in a future release - if category == 'radius': - ret = ret or not user.has_usable_password() - elif category == 'saml': - ret = ret or user.social_auth.all() - 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) @@ -292,7 +278,6 @@ def o_auth2_token_get_absolute_url(self, request=None): activity_stream_registrar.connect(AdHocCommand) # activity_stream_registrar.connect(JobHostSummary) # activity_stream_registrar.connect(JobEvent) -# activity_stream_registrar.connect(Profile) activity_stream_registrar.connect(Schedule) activity_stream_registrar.connect(NotificationTemplate) activity_stream_registrar.connect(Notification) 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/main/models/organization.py b/awx/main/models/organization.py index 939595ea9e9c..23ce7598a296 100644 --- a/awx/main/models/organization.py +++ b/awx/main/models/organization.py @@ -15,8 +15,8 @@ # AWX from awx.api.versioning import reverse -from awx.main.fields import AutoOneToOneField, ImplicitRoleField, OrderedManyToManyField -from awx.main.models.base import BaseModel, CommonModel, CommonModelNameNotUnique, CreatedModifiedModel, NotificationFieldsModel +from awx.main.fields import ImplicitRoleField, OrderedManyToManyField +from awx.main.models.base import BaseModel, CommonModel, CommonModelNameNotUnique, NotificationFieldsModel from awx.main.models.rbac import ( ROLE_SINGLETON_SYSTEM_ADMINISTRATOR, ROLE_SINGLETON_SYSTEM_AUDITOR, @@ -24,7 +24,7 @@ from awx.main.models.unified_jobs import UnifiedJob from awx.main.models.mixins import ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin -__all__ = ['Organization', 'Team', 'Profile', 'UserSessionMembership'] +__all__ = ['Organization', 'Team', 'UserSessionMembership'] class Organization(CommonModel, NotificationFieldsModel, ResourceMixin, CustomVirtualEnvMixin, RelatedJobsMixin): @@ -167,22 +167,6 @@ def get_absolute_url(self, request=None): return reverse('api:team_detail', kwargs={'pk': self.pk}, request=request) -class Profile(CreatedModifiedModel): - """ - Profile model related to User object. Currently stores LDAP DN for users - loaded from LDAP. - """ - - class Meta: - app_label = 'main' - - user = AutoOneToOneField('auth.User', related_name='profile', editable=False, on_delete=models.CASCADE) - ldap_dn = models.CharField( - max_length=1024, - default='', - ) - - class UserSessionMembership(BaseModel): """ A lookup table for API session membership given user. Note, there is a diff --git a/awx/main/tests/functional/api/test_oauth.py b/awx/main/tests/functional/api/test_oauth.py index e95d2cdc4abf..b4b0b49b6eb9 100644 --- a/awx/main/tests/functional/api/test_oauth.py +++ b/awx/main/tests/functional/api/test_oauth.py @@ -13,8 +13,6 @@ from awx.main.utils.encryption import decrypt_value, get_encryption_key from awx.api.versioning import reverse from awx.main.models.oauth import OAuth2Application as Application, OAuth2AccessToken as AccessToken -from awx.main.tests.functional import immediate_on_commit -from awx.sso.models import UserEnterpriseAuth from oauth2_provider.models import RefreshToken @@ -33,52 +31,6 @@ def test_personal_access_token_creation(oauth_application, post, alice): assert 'refresh_token' in resp_json -@pytest.mark.django_db -@pytest.mark.parametrize('allow_oauth, status', [(True, 201), (False, 403)]) -def test_token_creation_disabled_for_external_accounts(oauth_application, post, alice, allow_oauth, status): - UserEnterpriseAuth(user=alice, provider='radius').save() - url = drf_reverse('api:oauth_authorization_root_view') + 'token/' - - with override_settings(RADIUS_SERVER='example.org', ALLOW_OAUTH2_FOR_EXTERNAL_USERS=allow_oauth): - resp = post( - url, - data='grant_type=password&username=alice&password=alice&scope=read', - content_type='application/x-www-form-urlencoded', - HTTP_AUTHORIZATION='Basic ' + smart_str(base64.b64encode(smart_bytes(':'.join([oauth_application.client_id, oauth_application.client_secret])))), - status=status, - ) - if allow_oauth: - assert AccessToken.objects.count() == 1 - else: - assert 'OAuth2 Tokens cannot be created by users associated with an external authentication provider' in smart_str(resp.content) # noqa - assert AccessToken.objects.count() == 0 - - -@pytest.mark.django_db -def test_existing_token_enabled_for_external_accounts(oauth_application, get, post, admin): - UserEnterpriseAuth(user=admin, provider='radius').save() - url = drf_reverse('api:oauth_authorization_root_view') + 'token/' - with override_settings(RADIUS_SERVER='example.org', ALLOW_OAUTH2_FOR_EXTERNAL_USERS=True): - resp = post( - url, - data='grant_type=password&username=admin&password=admin&scope=read', - content_type='application/x-www-form-urlencoded', - HTTP_AUTHORIZATION='Basic ' + smart_str(base64.b64encode(smart_bytes(':'.join([oauth_application.client_id, oauth_application.client_secret])))), - status=201, - ) - token = json.loads(resp.content)['access_token'] - assert AccessToken.objects.count() == 1 - - with immediate_on_commit(): - resp = get(drf_reverse('api:user_me_list', kwargs={'version': 'v2'}), HTTP_AUTHORIZATION='Bearer ' + token, status=200) - assert json.loads(resp.content)['results'][0]['username'] == 'admin' - - with override_settings(RADIUS_SERVER='example.org', ALLOW_OAUTH2_FOR_EXTERNAL_USER=False): - with immediate_on_commit(): - resp = get(drf_reverse('api:user_me_list', kwargs={'version': 'v2'}), HTTP_AUTHORIZATION='Bearer ' + token, status=200) - assert json.loads(resp.content)['results'][0]['username'] == 'admin' - - @pytest.mark.django_db def test_pat_creation_no_default_scope(oauth_application, post, admin): # tests that the default scope is overriden diff --git a/awx/main/tests/functional/api/test_settings.py b/awx/main/tests/functional/api/test_settings.py index a84a6f7f6acd..e096637dc28c 100644 --- a/awx/main/tests/functional/api/test_settings.py +++ b/awx/main/tests/functional/api/test_settings.py @@ -7,7 +7,6 @@ # AWX from awx.api.versioning import reverse -from awx.conf.models import Setting from awx.conf.registry import settings_registry TEST_GIF_LOGO = 'data:image/gif;base64,R0lGODlhIQAjAPIAAP//////AP8AAMzMAJmZADNmAAAAAAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQJCgAHACwAAAAAIQAjAAADo3i63P4wykmrvTjrzZsxXfR94WMQBFh6RECuixHMLyzPQ13ewZCvow9OpzEAjIBj79cJJmU+FceIVEZ3QRozxBttmyOBwPBtisdX4Bha3oxmS+llFIPHQXQKkiSEXz9PeklHBzx3hYNyEHt4fmmAhHp8Nz45KgV5FgWFOFEGmwWbGqEfniChohmoQZ+oqRiZDZhEgk81I4mwg4EKVbxzrDHBEAkAIfkECQoABwAsAAAAACEAIwAAA6V4utz+MMpJq724GpP15p1kEAQYQmOwnWjgrmxjuMEAx8rsDjZ+fJvdLWQAFAHGWo8FRM54JqIRmYTigDrDMqZTbbbMj0CgjTLHZKvPQH6CTx+a2vKR0XbbOsoZ7SphG057gjl+c0dGgzeGNiaBiSgbBQUHBV08NpOVlkMSk0FKjZuURHiiOJxQnSGfQJuoEKREejK0dFRGjoiQt7iOuLx0rgxYEQkAIfkECQoABwAsAAAAACEAIwAAA7h4utxnxslJDSGR6nrz/owxYB64QUEwlGaVqlB7vrAJscsd3Lhy+wBArGEICo3DUFH4QDqK0GMy51xOgcGlEAfJ+iAFie62chR+jYKaSAuQGOqwJp7jGQRDuol+F/jxZWsyCmoQfwYwgoM5Oyg1i2w0A2WQIW2TPYOIkleQmy+UlYygoaIPnJmapKmqKiusMmSdpjxypnALtrcHioq3ury7hGm3dnVosVpMWFmwREZbddDOSsjVswcJACH5BAkKAAcALAAAAAAhACMAAAOxeLrc/jDKSZUxNS9DCNYV54HURQwfGRlDEFwqdLVuGjOsW9/Odb0wnsUAKBKNwsMFQGwyNUHckVl8bqI4o43lA26PNkv1S9DtNuOeVirw+aTI3qWAQwnud1vhLSnQLS0GeFF+GoVKNF0fh4Z+LDQ6Bn5/MTNmL0mAl2E3j2aclTmRmYCQoKEDiaRDKFhJez6UmbKyQowHtzy1uEl8DLCnEktrQ2PBD1NxSlXKIW5hz6cJACH5BAkKAAcALAAAAAAhACMAAAOkeLrc/jDKSau9OOvNlTFd9H3hYxAEWDJfkK5LGwTq+g0zDR/GgM+10A04Cm56OANgqTRmkDTmSOiLMgFOTM9AnFJHuexzYBAIijZf2SweJ8ttbbXLmd5+wBiJosSCoGF/fXEeS1g8gHl9hxODKkh4gkwVIwUekESIhA4FlgV3PyCWG52WI2oGnR2lnUWpqhqVEF4Xi7QjhpsshpOFvLosrnpoEAkAIfkECQoABwAsAAAAACEAIwAAA6l4utz+MMpJq71YGpPr3t1kEAQXQltQnk8aBCa7bMMLy4wx1G8s072PL6SrGQDI4zBThCU/v50zCVhidIYgNPqxWZkDg0AgxB2K4vEXbBSvr1JtZ3uOext0x7FqovF6OXtfe1UzdjAxhINPM013ChtJER8FBQeVRX8GlpggFZWWfjwblTiigGZnfqRmpUKbljKxDrNMeY2eF4R8jUiSur6/Z8GFV2WBtwwJACH5BAkKAAcALAAAAAAhACMAAAO6eLrcZi3KyQwhkGpq8f6ONWQgaAxB8JTfg6YkO50pzD5xhaurhCsGAKCnEw6NucNDCAkyI8ugdAhFKpnJJdMaeiofBejowUseCr9GYa0j1GyMdVgjBxoEuPSZXWKf7gKBeHtzMms0gHgGfDIVLztmjScvNZEyk28qjT40b5aXlHCbDgOhnzedoqOOlKeopaqrCy56sgtotbYKhYW6e7e9tsHBssO6eSTIm1peV0iuFUZDyU7NJnmcuQsJACH5BAkKAAcALAAAAAAhACMAAAOteLrc/jDKSZsxNS9DCNYV54Hh4H0kdAXBgKaOwbYX/Miza1vrVe8KA2AoJL5gwiQgeZz4GMXlcHl8xozQ3kW3KTajL9zsBJ1+sV2fQfALem+XAlRApxu4ioI1UpC76zJ4fRqDBzI+LFyFhH1iiS59fkgziW07jjRAG5QDeECOLk2Tj6KjnZafW6hAej6Smgevr6yysza2tiCuMasUF2Yov2gZUUQbU8YaaqjLpQkAOw==' # NOQA @@ -66,129 +65,6 @@ def test_awx_task_env_validity(get, patch, admin, value, expected): assert resp.data['AWX_TASK_ENV'] == dict() -@pytest.mark.django_db -def test_ldap_settings(get, put, patch, delete, admin): - url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'}) - get(url, user=admin, expect=200) - # The PUT below will fail at the moment because AUTH_LDAP_GROUP_TYPE - # defaults to None but cannot be set to None. - # put(url, user=admin, data=response.data, expect=200) - delete(url, user=admin, expect=204) - patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': ''}, expect=200) - patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldap.example.com'}, expect=400) - patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldap://ldap.example.com'}, expect=200) - patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldaps://ldap.example.com'}, expect=200) - patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldap://ldap.example.com:389'}, expect=200) - patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldaps://ldap.example.com:636'}, expect=200) - patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldap://ldap.example.com ldap://ldap2.example.com'}, expect=200) - patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldap://ldap.example.com,ldap://ldap2.example.com'}, expect=200) - patch(url, user=admin, data={'AUTH_LDAP_SERVER_URI': 'ldap://ldap.example.com, ldap://ldap2.example.com'}, expect=200) - patch(url, user=admin, data={'AUTH_LDAP_BIND_DN': 'cn=Manager,dc=example,dc=com'}, expect=200) - patch(url, user=admin, data={'AUTH_LDAP_BIND_DN': u'cn=暴力膜,dc=大新闻,dc=真的粉丝'}, expect=200) - - -@pytest.mark.django_db -@pytest.mark.parametrize( - 'value', - [ - None, - '', - 'INVALID', - 1, - [1], - ['INVALID'], - ], -) -def test_ldap_user_flags_by_group_invalid_dn(get, patch, admin, value): - url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'}) - patch(url, user=admin, data={'AUTH_LDAP_USER_FLAGS_BY_GROUP': {'is_superuser': value}}, expect=400) - - -@pytest.mark.django_db -def test_ldap_user_flags_by_group_string(get, patch, admin): - expected = 'CN=Admins,OU=Groups,DC=example,DC=com' - url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'}) - patch(url, user=admin, data={'AUTH_LDAP_USER_FLAGS_BY_GROUP': {'is_superuser': expected}}, expect=200) - resp = get(url, user=admin) - assert resp.data['AUTH_LDAP_USER_FLAGS_BY_GROUP']['is_superuser'] == [expected] - - -@pytest.mark.django_db -def test_ldap_user_flags_by_group_list(get, patch, admin): - expected = ['CN=Admins,OU=Groups,DC=example,DC=com', 'CN=Superadmins,OU=Groups,DC=example,DC=com'] - url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'}) - patch(url, user=admin, data={'AUTH_LDAP_USER_FLAGS_BY_GROUP': {'is_superuser': expected}}, expect=200) - resp = get(url, user=admin) - assert resp.data['AUTH_LDAP_USER_FLAGS_BY_GROUP']['is_superuser'] == expected - - -@pytest.mark.parametrize( - 'setting', - [ - 'AUTH_LDAP_USER_DN_TEMPLATE', - 'AUTH_LDAP_REQUIRE_GROUP', - 'AUTH_LDAP_DENY_GROUP', - ], -) -@pytest.mark.django_db -def test_empty_ldap_dn(get, put, patch, delete, admin, setting): - url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'}) - patch(url, user=admin, data={setting: ''}, expect=200) - resp = get(url, user=admin, expect=200) - assert resp.data[setting] is None - - patch(url, user=admin, data={setting: None}, expect=200) - resp = get(url, user=admin, expect=200) - assert resp.data[setting] is None - - -@pytest.mark.django_db -def test_radius_settings(get, put, patch, delete, admin, settings): - url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'radius'}) - response = get(url, user=admin, expect=200) - put(url, user=admin, data=response.data, expect=200) - # Set secret via the API. - patch(url, user=admin, data={'RADIUS_SECRET': 'mysecret'}, expect=200) - response = get(url, user=admin, expect=200) - assert response.data['RADIUS_SECRET'] == '$encrypted$' - assert Setting.objects.filter(key='RADIUS_SECRET').first().value.startswith('$encrypted$') - assert settings.RADIUS_SECRET == 'mysecret' - # Set secret via settings wrapper. - settings_wrapper = settings._awx_conf_settings - settings_wrapper.RADIUS_SECRET = 'mysecret2' - response = get(url, user=admin, expect=200) - assert response.data['RADIUS_SECRET'] == '$encrypted$' - assert Setting.objects.filter(key='RADIUS_SECRET').first().value.startswith('$encrypted$') - assert settings.RADIUS_SECRET == 'mysecret2' - # If we send back $encrypted$, the setting is not updated. - patch(url, user=admin, data={'RADIUS_SECRET': '$encrypted$'}, expect=200) - response = get(url, user=admin, expect=200) - assert response.data['RADIUS_SECRET'] == '$encrypted$' - assert Setting.objects.filter(key='RADIUS_SECRET').first().value.startswith('$encrypted$') - assert settings.RADIUS_SECRET == 'mysecret2' - # If we send an empty string, the setting is also set to an empty string. - patch(url, user=admin, data={'RADIUS_SECRET': ''}, expect=200) - response = get(url, user=admin, expect=200) - assert response.data['RADIUS_SECRET'] == '' - assert Setting.objects.filter(key='RADIUS_SECRET').first().value == '' - assert settings.RADIUS_SECRET == '' - - -@pytest.mark.django_db -def test_tacacsplus_settings(get, put, patch, admin): - url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'tacacsplus'}) - response = get(url, user=admin, expect=200) - put(url, user=admin, data=response.data, expect=200) - patch(url, user=admin, data={'TACACSPLUS_SECRET': 'mysecret'}, expect=200) - patch(url, user=admin, data={'TACACSPLUS_SECRET': ''}, expect=200) - patch(url, user=admin, data={'TACACSPLUS_HOST': 'localhost'}, expect=400) - patch(url, user=admin, data={'TACACSPLUS_SECRET': 'mysecret'}, expect=200) - patch(url, user=admin, data={'TACACSPLUS_HOST': 'localhost'}, expect=200) - patch(url, user=admin, data={'TACACSPLUS_HOST': '', 'TACACSPLUS_SECRET': ''}, expect=200) - patch(url, user=admin, data={'TACACSPLUS_HOST': 'localhost', 'TACACSPLUS_SECRET': ''}, expect=400) - patch(url, user=admin, data={'TACACSPLUS_HOST': 'localhost', 'TACACSPLUS_SECRET': 'mysecret'}, expect=200) - - @pytest.mark.django_db def test_ui_settings(get, put, patch, delete, admin): url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ui'}) @@ -317,76 +193,3 @@ def test_logging_aggregator_connection_test_valid(put, post, admin): # "Test" the logger url = reverse('api:setting_logging_test') post(url, {}, user=admin, expect=202) - - -@pytest.mark.django_db -@pytest.mark.parametrize('headers', [True, False]) -def test_saml_x509cert_validation(patch, get, admin, headers): - cert = "MIIEogIBAAKCAQEA1T4za6qBbHxFpN5f9eFvA74MFjrsjcp1uvzOaE23AYKMDEJghJ6dqQ7GwHLNIeIeumqDFmODauIzrgSDJTT5+NG30Rr+rRi0zDkrkBAj/AtA+SaVhbzqB6ZSd7LaMly9XAc+82OKlNpuWS9hPmFaSShzDTXRu5RRyvm4NDCAOGDu5hyVR2pV/ffKDNfNkChnqzvRRW9laQcVmliZhlTGn7nPZ+JbjpwEy0nwW+4zoAiEvwnT52N4xTqIcYOnXtGiaf13dh7FkUfYmS0tzF3+h8QRKwtIm4y+sq84R/kr79/0t5aRUpJynNrECajzmArpL4IjXKTPIyUpTKirJgGnCwIDAQABAoIBAC6bbbm2hpsjfkVOpUKkhxMWUqX5MwK6oYjBAIwjkEAwPFPhnh7eXC87H42oidVCCt1LsmMOVQbjcdAzBEb5kTkk/Twi3k8O+1U3maHfJT5NZ2INYNjeNXh+jb/Dw5UGWAzpOIUR2JQ4Oa4cgPCVbppW0O6uOKz6+fWXJv+hKiUoBCC0TiY52iseHJdUOaKNxYRD2IyIzCAxFSd5tZRaARIYDsugXp3E/TdbsVWA7bmjIBOXq+SquTrlB8x7j3B7+Pi09nAJ2U/uV4PHE+/2Fl009ywfmqancvnhwnz+GQ5jjP+gTfghJfbO+Z6M346rS0Vw+osrPgfyudNHlCswHOECgYEA/Cfq25gDP07wo6+wYWbx6LIzj/SSZy/Ux9P8zghQfoZiPoaq7BQBPAzwLNt7JWST8U11LZA8/wo6ch+HSTMk+m5ieVuru2cHxTDqeNlh94eCrNwPJ5ayA5U6LxAuSCTAzp+rv6KQUx1JcKSEHuh+nRYTKvUDE6iA6YtPLO96lLUCgYEA2H5rOPX2M4w1Q9zjol77lplbPRdczXNd0PIzhy8Z2ID65qvmr1nxBG4f2H96ykW8CKLXNvSXreNZ1BhOXc/3Hv+3mm46iitB33gDX4mlV4Jyo/w5IWhUKRyoW6qXquFFsScxRzTrx/9M+aZeRRLdsBk27HavFEg6jrbQ0SleZL8CgYAaM6Op8d/UgkVrHOR9Go9kmK/W85kK8+NuaE7Ksf57R0eKK8AzC9kc/lMuthfTyOG+n0ff1i8gaVWtai1Ko+/hvfqplacAsDIUgYK70AroB8LCZ5ODj5sr2CPVpB7LDFakod7c6O2KVW6+L7oy5AHUHOkc+5y4PDg5DGrLxo68SQKBgAlGoWF3aG0c/MtDk51JZI43U+lyLs++ua5SMlMAeaMFI7rucpvgxqrh7Qthqukvw7a7A22fXUBeFWM5B2KNnpD9c+hyAKAa6l+gzMQzKZpuRGsyS2BbEAAS8kO7M3Rm4o2MmFfstI2FKs8nibJ79HOvIONQ0n+T+K5Utu2/UAQRAoGAFB4fiIyQ0nYzCf18Z4Wvi/qeIOW+UoBonIN3y1h4wruBywINHxFMHx4aVImJ6R09hoJ9D3Mxli3xF/8JIjfTG5fBSGrGnuofl14d/XtRDXbT2uhVXrIkeLL/ojODwwEx0VhxIRUEjPTvEl6AFSRRcBp3KKzQ/cu7ENDY6GTlOUI=" # noqa - if headers: - cert = '-----BEGIN CERTIFICATE-----\n' + cert + '\n-----END CERTIFICATE-----' - url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'saml'}) - resp = patch( - url, - user=admin, - data={ - 'SOCIAL_AUTH_SAML_ENABLED_IDPS': { - "okta": { - "attr_last_name": "LastName", - "attr_username": "login", - "entity_id": "http://www.okta.com/abc123", - "attr_user_permanent_id": "login", - "url": "https://example.okta.com/app/abc123/xyz123/sso/saml", - "attr_email": "Email", - "x509cert": cert, - "attr_first_name": "FirstName", - } - } - }, - ) - assert resp.status_code == 200 - - -@pytest.mark.django_db -def test_github_settings(get, put, patch, delete, admin): - url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'github'}) - get(url, user=admin, expect=200) - delete(url, user=admin, expect=204) - response = get(url, user=admin, expect=200) - data = dict(response.data.items()) - put(url, user=admin, data=data, expect=200) - patch(url, user=admin, data={'SOCIAL_AUTH_GITHUB_KEY': '???'}, expect=200) - response = get(url, user=admin, expect=200) - assert response.data['SOCIAL_AUTH_GITHUB_KEY'] == '???' - data.pop('SOCIAL_AUTH_GITHUB_KEY') - put(url, user=admin, data=data, expect=200) - response = get(url, user=admin, expect=200) - assert response.data['SOCIAL_AUTH_GITHUB_KEY'] == '' - - -@pytest.mark.django_db -def test_github_enterprise_settings(get, put, patch, delete, admin): - url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'github-enterprise'}) - get(url, user=admin, expect=200) - delete(url, user=admin, expect=204) - response = get(url, user=admin, expect=200) - data = dict(response.data.items()) - put(url, user=admin, data=data, expect=200) - patch( - url, - user=admin, - data={ - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL': 'example.com', - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL': 'example.com', - }, - expect=200, - ) - response = get(url, user=admin, expect=200) - assert response.data['SOCIAL_AUTH_GITHUB_ENTERPRISE_URL'] == 'example.com' - assert response.data['SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL'] == 'example.com' - data.pop('SOCIAL_AUTH_GITHUB_ENTERPRISE_URL') - data.pop('SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL') - put(url, user=admin, data=data, expect=200) - response = get(url, user=admin, expect=200) - assert response.data['SOCIAL_AUTH_GITHUB_ENTERPRISE_URL'] == '' - assert response.data['SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL'] == '' diff --git a/awx/main/tests/functional/test_ldap.py b/awx/main/tests/functional/test_ldap.py deleted file mode 100644 index 2467ff52e38f..000000000000 --- a/awx/main/tests/functional/test_ldap.py +++ /dev/null @@ -1,103 +0,0 @@ -import ldap -import ldif -import pytest -import os -from mockldap import MockLdap - -from awx.api.versioning import reverse - - -@pytest.fixture -def ldap_generator(): - def fn(fname, host='localhost'): - fh = open(os.path.join(os.path.dirname(os.path.realpath(__file__)), fname), 'rb') - ctrl = ldif.LDIFRecordList(fh) - ctrl.parse() - - directory = dict(ctrl.all_records) - - mockldap = MockLdap(directory) - - mockldap.start() - mockldap['ldap://{}/'.format(host)] - - conn = ldap.initialize('ldap://{}/'.format(host)) - - return conn - # mockldap.stop() - - return fn - - -@pytest.fixture -def ldap_settings_generator(): - def fn(prefix='', dc='ansible', host='ldap.ansible.com'): - prefix = '_{}'.format(prefix) if prefix else '' - - data = { - 'AUTH_LDAP_SERVER_URI': 'ldap://{}'.format(host), - 'AUTH_LDAP_BIND_DN': 'cn=eng_user1,ou=people,dc={},dc=com'.format(dc), - 'AUTH_LDAP_BIND_PASSWORD': 'password', - "AUTH_LDAP_USER_SEARCH": ["ou=people,dc={},dc=com".format(dc), "SCOPE_SUBTREE", "(cn=%(user)s)"], - "AUTH_LDAP_TEAM_MAP": { - "LDAP Sales": {"organization": "LDAP Organization", "users": "cn=sales,ou=groups,dc={},dc=com".format(dc), "remove": True}, - "LDAP IT": {"organization": "LDAP Organization", "users": "cn=it,ou=groups,dc={},dc=com".format(dc), "remove": True}, - "LDAP Engineering": {"organization": "LDAP Organization", "users": "cn=engineering,ou=groups,dc={},dc=com".format(dc), "remove": True}, - }, - "AUTH_LDAP_REQUIRE_GROUP": None, - "AUTH_LDAP_USER_ATTR_MAP": {"first_name": "givenName", "last_name": "sn", "email": "mail"}, - "AUTH_LDAP_GROUP_SEARCH": ["dc={},dc=com".format(dc), "SCOPE_SUBTREE", "(objectClass=groupOfNames)"], - "AUTH_LDAP_USER_FLAGS_BY_GROUP": {"is_superuser": "cn=superusers,ou=groups,dc={},dc=com".format(dc)}, - "AUTH_LDAP_ORGANIZATION_MAP": { - "LDAP Organization": { - "admins": "cn=engineering_admins,ou=groups,dc={},dc=com".format(dc), - "remove_admins": False, - "users": [ - "cn=engineering,ou=groups,dc={},dc=com".format(dc), - "cn=sales,ou=groups,dc={},dc=com".format(dc), - "cn=it,ou=groups,dc={},dc=com".format(dc), - ], - "remove_users": False, - } - }, - } - - if prefix: - data_new = dict() - for k, v in data.items(): - k_new = k.replace('AUTH_LDAP', 'AUTH_LDAP{}'.format(prefix)) - data_new[k_new] = v - else: - data_new = data - - return data_new - - return fn - - -# Note: mockldap isn't fully featured. Fancy queries aren't fully baked. -# However, objects returned are solid so they should flow through django ldap middleware nicely. -@pytest.mark.skip(reason="Needs Update - CA") -@pytest.mark.django_db -def test_login(ldap_generator, patch, post, admin, ldap_settings_generator): - auth_url = reverse('api:auth_token_view') - ldap_settings_url = reverse('api:setting_singleton_detail', kwargs={'category_slug': 'ldap'}) - - # Generate mock ldap servers and init with ldap data - ldap_generator("../data/ldap_example.ldif", "ldap.example.com") - ldap_generator("../data/ldap_redhat.ldif", "ldap.redhat.com") - ldap_generator("../data/ldap_ansible.ldif", "ldap.ansible.com") - - ldap_settings_example = ldap_settings_generator(dc='example') - ldap_settings_ansible = ldap_settings_generator(prefix='1', dc='ansible') - ldap_settings_redhat = ldap_settings_generator(prefix='2', dc='redhat') - - # eng_user1 exists in ansible and redhat but not example - patch(ldap_settings_url, user=admin, data=ldap_settings_example, expect=200) - - post(auth_url, data={'username': 'eng_user1', 'password': 'password'}, expect=400) - - patch(ldap_settings_url, user=admin, data=ldap_settings_ansible, expect=200) - patch(ldap_settings_url, user=admin, data=ldap_settings_redhat, expect=200) - - post(auth_url, data={'username': 'eng_user1', 'password': 'password'}, expect=200) diff --git a/awx/main/tests/unit/commands/test_dump_auth_config.py b/awx/main/tests/unit/commands/test_dump_auth_config.py deleted file mode 100644 index 48024ff5e425..000000000000 --- a/awx/main/tests/unit/commands/test_dump_auth_config.py +++ /dev/null @@ -1,132 +0,0 @@ -from io import StringIO -import json -from django.core.management import call_command -from django.test import TestCase, override_settings - - -settings_dict = { - "SOCIAL_AUTH_SAML_SP_ENTITY_ID": "SP_ENTITY_ID", - "SOCIAL_AUTH_SAML_SP_PUBLIC_CERT": "SP_PUBLIC_CERT", - "SOCIAL_AUTH_SAML_SP_PRIVATE_KEY": "SP_PRIVATE_KEY", - "SOCIAL_AUTH_SAML_ORG_INFO": "ORG_INFO", - "SOCIAL_AUTH_SAML_TECHNICAL_CONTACT": "TECHNICAL_CONTACT", - "SOCIAL_AUTH_SAML_SUPPORT_CONTACT": "SUPPORT_CONTACT", - "SOCIAL_AUTH_SAML_SP_EXTRA": "SP_EXTRA", - "SOCIAL_AUTH_SAML_SECURITY_CONFIG": "SECURITY_CONFIG", - "SOCIAL_AUTH_SAML_EXTRA_DATA": "EXTRA_DATA", - "SOCIAL_AUTH_SAML_ENABLED_IDPS": { - "Keycloak": { - "attr_last_name": "last_name", - "attr_groups": "groups", - "attr_email": "email", - "attr_user_permanent_id": "name_id", - "attr_username": "username", - "entity_id": "https://example.com/auth/realms/awx", - "url": "https://example.com/auth/realms/awx/protocol/saml", - "x509cert": "-----BEGIN CERTIFICATE-----\nMIIDDjCCAfYCCQCPBeVvpo8+VzANBgkqhkiG9w0BAQsFADBJMQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBkR1cmhhbTEMMAoGA1UECgwDYXd4MQ4w\nDAYDVQQDDAVsb2NhbDAeFw0yNDAxMTgxNDA4MzFaFw0yNTAxMTcxNDA4MzFaMEkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGRHVyaGFtMQwwCgYD\nVQQKDANhd3gxDjAMBgNVBAMMBWxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAzouj93oyFXsHEABdPESh3CYpp5QJJBM4TLYIIolk6PFOFIVwBuFY\nfExi5w7Hh4A42lPM6RkrT+u3h7LV39H9MRUfqygOSmaxICTOI0sU9ROHc44fWWzN\n756OP4B5zSiqG82q8X7nYVkcID+2F/3ekPLMOlWn53OrcdfKKDIcqavoTkQJefc2\nggXU3WgVCxGki/qCm+e5cZ1Cpl/ykSLOT8dWMEzDd12kin66zJ3KYz9F2Q5kQTh4\nKRAChnBBoEqzOfENHEAaHALiXOlVSy61VcLbtvskRMMwBtsydlnd9n/HGnktgrid\n3Ca0z5wBTHWjAOBvCKxKJuDa+jmyHEnpcQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB\nAQBXvmyPWgXhC26cHYJBgQqj57dZ+n7p00kM1J+27oDMjGmbmX+XIKXLWazw/rG3\ngDjw9MXI2tVCrQMX0ohjphaULXhb/VBUPDOiW+k7C6AB3nZySFRflcR3cM4f83zF\nMoBd0549h5Red4p72FeOKNJRTN8YO4ooH9YNh5g0FQkgqn7fV9w2CNlomeKIW9zP\nm8tjFw0cJUk2wEYBVl8O7ko5rgNlzhkLoZkMvJhKa99AQJA6MAdyoLl1lv56Kq4X\njk+mMEiz9SaInp+ILQ1uQxZEwuC7DoGRW76rV4Fnie6+DLft4WKZfX1497mx8NV3\noR0abutJaKnCj07dwRu4/EsK\n-----END CERTIFICATE-----", - "attr_first_name": "first_name", - } - }, - "SOCIAL_AUTH_SAML_CALLBACK_URL": "CALLBACK_URL", - "AUTH_LDAP_1_SERVER_URI": "SERVER_URI", - "AUTH_LDAP_1_BIND_DN": "BIND_DN", - "AUTH_LDAP_1_BIND_PASSWORD": "BIND_PASSWORD", - "AUTH_LDAP_1_GROUP_SEARCH": ["GROUP_SEARCH"], - "AUTH_LDAP_1_GROUP_TYPE": "string object", - "AUTH_LDAP_1_GROUP_TYPE_PARAMS": {"member_attr": "member", "name_attr": "cn"}, - "AUTH_LDAP_1_USER_DN_TEMPLATE": "USER_DN_TEMPLATE", - "AUTH_LDAP_1_USER_SEARCH": ["USER_SEARCH"], - "AUTH_LDAP_1_USER_ATTR_MAP": { - "email": "email", - "last_name": "last_name", - "first_name": "first_name", - }, - "AUTH_LDAP_1_CONNECTION_OPTIONS": {}, - "AUTH_LDAP_1_START_TLS": None, -} - - -@override_settings(**settings_dict) -class TestDumpAuthConfigCommand(TestCase): - def setUp(self): - super().setUp() - self.expected_config = [ - { - "type": "ansible_base.authentication.authenticator_plugins.saml", - "name": "Keycloak", - "enabled": True, - "create_objects": True, - "users_unique": False, - "remove_users": True, - "configuration": { - "SP_ENTITY_ID": "SP_ENTITY_ID", - "SP_PUBLIC_CERT": "SP_PUBLIC_CERT", - "SP_PRIVATE_KEY": "SP_PRIVATE_KEY", - "ORG_INFO": "ORG_INFO", - "TECHNICAL_CONTACT": "TECHNICAL_CONTACT", - "SUPPORT_CONTACT": "SUPPORT_CONTACT", - "SP_EXTRA": "SP_EXTRA", - "SECURITY_CONFIG": "SECURITY_CONFIG", - "EXTRA_DATA": "EXTRA_DATA", - "ENABLED_IDPS": { - "Keycloak": { - "attr_last_name": "last_name", - "attr_groups": "groups", - "attr_email": "email", - "attr_user_permanent_id": "name_id", - "attr_username": "username", - "entity_id": "https://example.com/auth/realms/awx", - "url": "https://example.com/auth/realms/awx/protocol/saml", - "x509cert": "-----BEGIN CERTIFICATE-----\nMIIDDjCCAfYCCQCPBeVvpo8+VzANBgkqhkiG9w0BAQsFADBJMQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBkR1cmhhbTEMMAoGA1UECgwDYXd4MQ4w\nDAYDVQQDDAVsb2NhbDAeFw0yNDAxMTgxNDA4MzFaFw0yNTAxMTcxNDA4MzFaMEkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGRHVyaGFtMQwwCgYD\nVQQKDANhd3gxDjAMBgNVBAMMBWxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAzouj93oyFXsHEABdPESh3CYpp5QJJBM4TLYIIolk6PFOFIVwBuFY\nfExi5w7Hh4A42lPM6RkrT+u3h7LV39H9MRUfqygOSmaxICTOI0sU9ROHc44fWWzN\n756OP4B5zSiqG82q8X7nYVkcID+2F/3ekPLMOlWn53OrcdfKKDIcqavoTkQJefc2\nggXU3WgVCxGki/qCm+e5cZ1Cpl/ykSLOT8dWMEzDd12kin66zJ3KYz9F2Q5kQTh4\nKRAChnBBoEqzOfENHEAaHALiXOlVSy61VcLbtvskRMMwBtsydlnd9n/HGnktgrid\n3Ca0z5wBTHWjAOBvCKxKJuDa+jmyHEnpcQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB\nAQBXvmyPWgXhC26cHYJBgQqj57dZ+n7p00kM1J+27oDMjGmbmX+XIKXLWazw/rG3\ngDjw9MXI2tVCrQMX0ohjphaULXhb/VBUPDOiW+k7C6AB3nZySFRflcR3cM4f83zF\nMoBd0549h5Red4p72FeOKNJRTN8YO4ooH9YNh5g0FQkgqn7fV9w2CNlomeKIW9zP\nm8tjFw0cJUk2wEYBVl8O7ko5rgNlzhkLoZkMvJhKa99AQJA6MAdyoLl1lv56Kq4X\njk+mMEiz9SaInp+ILQ1uQxZEwuC7DoGRW76rV4Fnie6+DLft4WKZfX1497mx8NV3\noR0abutJaKnCj07dwRu4/EsK\n-----END CERTIFICATE-----", - "attr_first_name": "first_name", - } - }, - "CALLBACK_URL": "CALLBACK_URL", - "IDP_URL": "https://example.com/auth/realms/awx/protocol/saml", - "IDP_X509_CERT": "-----BEGIN CERTIFICATE-----\nMIIDDjCCAfYCCQCPBeVvpo8+VzANBgkqhkiG9w0BAQsFADBJMQswCQYDVQQGEwJV\nUzELMAkGA1UECAwCTkMxDzANBgNVBAcMBkR1cmhhbTEMMAoGA1UECgwDYXd4MQ4w\nDAYDVQQDDAVsb2NhbDAeFw0yNDAxMTgxNDA4MzFaFw0yNTAxMTcxNDA4MzFaMEkx\nCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJOQzEPMA0GA1UEBwwGRHVyaGFtMQwwCgYD\nVQQKDANhd3gxDjAMBgNVBAMMBWxvY2FsMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A\nMIIBCgKCAQEAzouj93oyFXsHEABdPESh3CYpp5QJJBM4TLYIIolk6PFOFIVwBuFY\nfExi5w7Hh4A42lPM6RkrT+u3h7LV39H9MRUfqygOSmaxICTOI0sU9ROHc44fWWzN\n756OP4B5zSiqG82q8X7nYVkcID+2F/3ekPLMOlWn53OrcdfKKDIcqavoTkQJefc2\nggXU3WgVCxGki/qCm+e5cZ1Cpl/ykSLOT8dWMEzDd12kin66zJ3KYz9F2Q5kQTh4\nKRAChnBBoEqzOfENHEAaHALiXOlVSy61VcLbtvskRMMwBtsydlnd9n/HGnktgrid\n3Ca0z5wBTHWjAOBvCKxKJuDa+jmyHEnpcQIDAQABMA0GCSqGSIb3DQEBCwUAA4IB\nAQBXvmyPWgXhC26cHYJBgQqj57dZ+n7p00kM1J+27oDMjGmbmX+XIKXLWazw/rG3\ngDjw9MXI2tVCrQMX0ohjphaULXhb/VBUPDOiW+k7C6AB3nZySFRflcR3cM4f83zF\nMoBd0549h5Red4p72FeOKNJRTN8YO4ooH9YNh5g0FQkgqn7fV9w2CNlomeKIW9zP\nm8tjFw0cJUk2wEYBVl8O7ko5rgNlzhkLoZkMvJhKa99AQJA6MAdyoLl1lv56Kq4X\njk+mMEiz9SaInp+ILQ1uQxZEwuC7DoGRW76rV4Fnie6+DLft4WKZfX1497mx8NV3\noR0abutJaKnCj07dwRu4/EsK\n-----END CERTIFICATE-----", - "IDP_ENTITY_ID": "https://example.com/auth/realms/awx", - "IDP_ATTR_EMAIL": "email", - "IDP_GROUPS": "groups", - "IDP_ATTR_USERNAME": "username", - "IDP_ATTR_LAST_NAME": "last_name", - "IDP_ATTR_FIRST_NAME": "first_name", - "IDP_ATTR_USER_PERMANENT_ID": "name_id", - }, - }, - { - "type": "ansible_base.authentication.authenticator_plugins.ldap", - "name": "LDAP_1", - "enabled": True, - "create_objects": True, - "users_unique": False, - "remove_users": True, - "configuration": { - "SERVER_URI": ["SERVER_URI"], - "BIND_DN": "BIND_DN", - "BIND_PASSWORD": "BIND_PASSWORD", - "CONNECTION_OPTIONS": {}, - "GROUP_TYPE": "str", - "GROUP_TYPE_PARAMS": {"member_attr": "member", "name_attr": "cn"}, - "GROUP_SEARCH": ["GROUP_SEARCH"], - "START_TLS": None, - "USER_DN_TEMPLATE": "USER_DN_TEMPLATE", - "USER_ATTR_MAP": {"email": "email", "last_name": "last_name", "first_name": "first_name"}, - "USER_SEARCH": ["USER_SEARCH"], - }, - }, - ] - - def test_json_returned_from_cmd(self): - output = StringIO() - call_command("dump_auth_config", stdout=output) - cmmd_output = json.loads(output.getvalue()) - - # check configured SAML return - assert cmmd_output[0] == self.expected_config[0] - - # check configured LDAP return - assert cmmd_output[2] == self.expected_config[1] - - # check unconfigured LDAP return - assert "LDAP_0_missing_fields" in cmmd_output[1] - assert cmmd_output[1]["LDAP_0_missing_fields"] == ['SERVER_URI', 'GROUP_TYPE', 'GROUP_TYPE_PARAMS', 'USER_DN_TEMPLATE', 'USER_ATTR_MAP'] diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index bf1e6e57270d..44b372a753b1 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -9,8 +9,6 @@ import socket from datetime import timedelta -# python-ldap -import ldap from split_settings.tools import include @@ -316,8 +314,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'], }, @@ -347,14 +343,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', @@ -389,27 +383,7 @@ # 'URL_FORMAT_OVERRIDE': None, } -AUTHENTICATION_BACKENDS = ( - 'awx.sso.backends.LDAPBackend', - 'awx.sso.backends.LDAPBackend1', - 'awx.sso.backends.LDAPBackend2', - 'awx.sso.backends.LDAPBackend3', - 'awx.sso.backends.LDAPBackend4', - 'awx.sso.backends.LDAPBackend5', - 'awx.sso.backends.RADIUSBackend', - 'awx.sso.backends.TACACSPlusBackend', - 'social_core.backends.google.GoogleOAuth2', - 'social_core.backends.github.GithubOAuth2', - 'social_core.backends.github.GithubOrganizationOAuth2', - 'social_core.backends.github.GithubTeamOAuth2', - 'social_core.backends.github_enterprise.GithubEnterpriseOAuth2', - 'social_core.backends.github_enterprise.GithubEnterpriseOrganizationOAuth2', - 'social_core.backends.github_enterprise.GithubEnterpriseTeamOAuth2', - 'social_core.backends.open_id_connect.OpenIdConnectAuth', - 'social_core.backends.azuread.AzureADOAuth2', - 'awx.sso.backends.SAMLAuth', - 'awx.main.backends.AWXModelBackend', -) +AUTHENTICATION_BACKENDS = ('awx.main.backends.AWXModelBackend',) # Django OAuth Toolkit settings @@ -419,31 +393,7 @@ OAUTH2_PROVIDER_ID_TOKEN_MODEL = "oauth2_provider.IDToken" OAUTH2_PROVIDER = {'ACCESS_TOKEN_EXPIRE_SECONDS': 31536000000, 'AUTHORIZATION_CODE_EXPIRE_SECONDS': 600, 'REFRESH_TOKEN_EXPIRE_SECONDS': 2628000} -ALLOW_OAUTH2_FOR_EXTERNAL_USERS = False -# LDAP server (default to None to skip using LDAP authentication). -# Note: This setting may be overridden by database settings. -AUTH_LDAP_SERVER_URI = None - -# Disable LDAP referrals by default (to prevent certain LDAP queries from -# hanging with AD). -# Note: This setting may be overridden by database settings. -AUTH_LDAP_CONNECTION_OPTIONS = {ldap.OPT_REFERRALS: 0, ldap.OPT_NETWORK_TIMEOUT: 30} - -# Radius server settings (default to empty string to skip using Radius auth). -# Note: These settings may be overridden by database settings. -RADIUS_SERVER = '' -RADIUS_PORT = 1812 -RADIUS_SECRET = '' - -# TACACS+ settings (default host to empty string to skip using TACACS+ auth). -# Note: These settings may be overridden by database settings. -TACACSPLUS_HOST = '' -TACACSPLUS_PORT = 49 -TACACSPLUS_SECRET = '' -TACACSPLUS_SESSION_TIMEOUT = 5 -TACACSPLUS_AUTH_PROTOCOL = 'ascii' -TACACSPLUS_REM_ADDR = False # Enable / Disable HTTP Basic Authentication used in the API browser # Note: Session limits are not enforced when using HTTP Basic Authentication. @@ -499,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 = '' @@ -510,90 +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_SAML_PIPELINE = _SOCIAL_AUTH_PIPELINE_BASE + ('awx.sso.saml_pipeline.populate_user', 'awx.sso.saml_pipeline.update_user_flags') -SAML_AUTO_CREATE_OBJECTS = True - -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 - -# Note: These settings may be overridden by database settings. -SOCIAL_AUTH_GOOGLE_OAUTH2_KEY = '' -SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET = '' -SOCIAL_AUTH_GOOGLE_OAUTH2_SCOPE = ['profile'] - -SOCIAL_AUTH_GITHUB_KEY = '' -SOCIAL_AUTH_GITHUB_SECRET = '' -SOCIAL_AUTH_GITHUB_SCOPE = ['user:email', 'read:org'] - -SOCIAL_AUTH_GITHUB_ORG_KEY = '' -SOCIAL_AUTH_GITHUB_ORG_SECRET = '' -SOCIAL_AUTH_GITHUB_ORG_NAME = '' -SOCIAL_AUTH_GITHUB_ORG_SCOPE = ['user:email', 'read:org'] - -SOCIAL_AUTH_GITHUB_TEAM_KEY = '' -SOCIAL_AUTH_GITHUB_TEAM_SECRET = '' -SOCIAL_AUTH_GITHUB_TEAM_ID = '' -SOCIAL_AUTH_GITHUB_TEAM_SCOPE = ['user:email', 'read:org'] - -SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY = '' -SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET = '' -SOCIAL_AUTH_GITHUB_ENTERPRISE_SCOPE = ['user:email', 'read:org'] - -SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY = '' -SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET = '' -SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME = '' -SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SCOPE = ['user:email', 'read:org'] - -SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY = '' -SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET = '' -SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID = '' -SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SCOPE = ['user:email', 'read:org'] - -SOCIAL_AUTH_AZUREAD_OAUTH2_KEY = '' -SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET = '' - -SOCIAL_AUTH_SAML_SP_ENTITY_ID = '' -SOCIAL_AUTH_SAML_SP_PUBLIC_CERT = '' -SOCIAL_AUTH_SAML_SP_PRIVATE_KEY = '' -SOCIAL_AUTH_SAML_ORG_INFO = {} -SOCIAL_AUTH_SAML_TECHNICAL_CONTACT = {} -SOCIAL_AUTH_SAML_SUPPORT_CONTACT = {} -SOCIAL_AUTH_SAML_ENABLED_IDPS = {} - -SOCIAL_AUTH_SAML_ORGANIZATION_ATTR = {} -SOCIAL_AUTH_SAML_TEAM_ATTR = {} -SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR = {} - # Any ANSIBLE_* settings will be passed to the task runner subprocess # environment @@ -928,7 +790,6 @@ 'awx.analytics.broadcast_websocket': {'handlers': ['console', 'file', 'wsrelay', 'external_logger'], 'level': 'INFO', 'propagate': False}, 'awx.analytics.performance': {'handlers': ['console', 'file', 'tower_warnings', 'external_logger'], 'level': 'DEBUG', 'propagate': False}, 'awx.analytics.job_lifecycle': {'handlers': ['console', 'job_lifecycle'], 'level': 'DEBUG', 'propagate': False}, - 'django_auth_ldap': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'DEBUG'}, 'social': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'DEBUG'}, 'system_tracking_migrations': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'DEBUG'}, 'rbac_migrations': {'handlers': ['console', 'file', 'tower_warnings'], 'level': 'DEBUG'}, @@ -1035,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 572afc3ef04b..000000000000 --- a/awx/sso/backends.py +++ /dev/null @@ -1,469 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Python -from collections import OrderedDict -import logging -import uuid - -import ldap - -# Django -from django.dispatch import receiver -from django.contrib.auth.models import User -from django.conf import settings as django_settings -from django.core.signals import setting_changed -from django.utils.encoding import force_str -from django.http import HttpResponse - -# django-auth-ldap -from django_auth_ldap.backend import LDAPSettings as BaseLDAPSettings -from django_auth_ldap.backend import LDAPBackend as BaseLDAPBackend -from django_auth_ldap.backend import populate_user -from django.core.exceptions import ImproperlyConfigured - -# radiusauth -from radiusauth.backends import RADIUSBackend as BaseRADIUSBackend - -# tacacs+ auth -import tacacs_plus - -# social -from social_core.backends.saml import OID_USERID -from social_core.backends.saml import SAMLAuth as BaseSAMLAuth -from social_core.backends.saml import SAMLIdentityProvider as BaseSAMLIdentityProvider - -# Ansible Tower -from awx.sso.models import UserEnterpriseAuth -from awx.sso.common import create_org_and_teams, reconcile_users_org_team_mappings - -logger = logging.getLogger('awx.sso.backends') - - -class LDAPSettings(BaseLDAPSettings): - defaults = dict(list(BaseLDAPSettings.defaults.items()) + list({'ORGANIZATION_MAP': {}, 'TEAM_MAP': {}, 'GROUP_TYPE_PARAMS': {}}.items())) - - def __init__(self, prefix='AUTH_LDAP_', defaults={}): - super(LDAPSettings, self).__init__(prefix, defaults) - - # If a DB-backed setting is specified that wipes out the - # OPT_NETWORK_TIMEOUT, fall back to a sane default - if ldap.OPT_NETWORK_TIMEOUT not in getattr(self, 'CONNECTION_OPTIONS', {}): - options = getattr(self, 'CONNECTION_OPTIONS', {}) - options[ldap.OPT_NETWORK_TIMEOUT] = 30 - self.CONNECTION_OPTIONS = options - - # when specifying `.set_option()` calls for TLS in python-ldap, the - # *order* in which you invoke them *matters*, particularly in Python3, - # where dictionary insertion order is persisted - # - # specifically, it is *critical* that `ldap.OPT_X_TLS_NEWCTX` be set *last* - # this manual sorting puts `OPT_X_TLS_NEWCTX` *after* other TLS-related - # options - # - # see: https://github.com/python-ldap/python-ldap/issues/55 - newctx_option = self.CONNECTION_OPTIONS.pop(ldap.OPT_X_TLS_NEWCTX, None) - self.CONNECTION_OPTIONS = OrderedDict(self.CONNECTION_OPTIONS) - if newctx_option is not None: - self.CONNECTION_OPTIONS[ldap.OPT_X_TLS_NEWCTX] = newctx_option - - -class LDAPBackend(BaseLDAPBackend): - """ - Custom LDAP backend for AWX. - """ - - settings_prefix = 'AUTH_LDAP_' - - def __init__(self, *args, **kwargs): - self._dispatch_uid = uuid.uuid4() - super(LDAPBackend, self).__init__(*args, **kwargs) - setting_changed.connect(self._on_setting_changed, dispatch_uid=self._dispatch_uid) - - def _on_setting_changed(self, sender, **kwargs): - # If any AUTH_LDAP_* setting changes, force settings to be reloaded for - # this backend instance. - if kwargs.get('setting', '').startswith(self.settings_prefix): - self._settings = None - - def _get_settings(self): - if self._settings is None: - self._settings = LDAPSettings(self.settings_prefix) - return self._settings - - def _set_settings(self, settings): - self._settings = settings - - settings = property(_get_settings, _set_settings) - - def authenticate(self, request, username, password): - if self.settings.START_TLS and ldap.OPT_X_TLS_REQUIRE_CERT in self.settings.CONNECTION_OPTIONS: - # with python-ldap, if you want to set connection-specific TLS - # parameters, you must also specify OPT_X_TLS_NEWCTX = 0 - # see: https://stackoverflow.com/a/29722445 - # see: https://stackoverflow.com/a/38136255 - self.settings.CONNECTION_OPTIONS[ldap.OPT_X_TLS_NEWCTX] = 0 - - if not self.settings.SERVER_URI: - return None - try: - user = User.objects.get(username=username) - if user and (not user.profile or not user.profile.ldap_dn): - return None - except User.DoesNotExist: - pass - - try: - for setting_name, type_ in [('GROUP_SEARCH', 'LDAPSearch'), ('GROUP_TYPE', 'LDAPGroupType')]: - if getattr(self.settings, setting_name) is None: - raise ImproperlyConfigured("{} must be an {} instance.".format(setting_name, type_)) - ldap_user = super(LDAPBackend, self).authenticate(request, username, password) - # If we have an LDAP user and that user we found has an ldap_user internal object and that object has a bound connection - # Then we can try and force an unbind to close the sticky connection - if ldap_user and ldap_user.ldap_user and ldap_user.ldap_user._connection_bound: - logger.debug("Forcing LDAP connection to close") - try: - ldap_user.ldap_user._connection.unbind_s() - ldap_user.ldap_user._connection_bound = False - except Exception: - logger.exception(f"Got unexpected LDAP exception when forcing LDAP disconnect for user {ldap_user}, login will still proceed") - return ldap_user - except Exception: - logger.exception("Encountered an error authenticating to LDAP") - return None - - def get_user(self, user_id): - if not self.settings.SERVER_URI: - return None - return super(LDAPBackend, self).get_user(user_id) - - # Disable any LDAP based authorization / permissions checking. - - def has_perm(self, user, perm, obj=None): - return False - - def has_module_perms(self, user, app_label): - return False - - def get_all_permissions(self, user, obj=None): - return set() - - def get_group_permissions(self, user, obj=None): - return set() - - -class LDAPBackend1(LDAPBackend): - settings_prefix = 'AUTH_LDAP_1_' - - -class LDAPBackend2(LDAPBackend): - settings_prefix = 'AUTH_LDAP_2_' - - -class LDAPBackend3(LDAPBackend): - settings_prefix = 'AUTH_LDAP_3_' - - -class LDAPBackend4(LDAPBackend): - settings_prefix = 'AUTH_LDAP_4_' - - -class LDAPBackend5(LDAPBackend): - settings_prefix = 'AUTH_LDAP_5_' - - -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) - - -class RADIUSBackend(BaseRADIUSBackend): - """ - Custom Radius backend to verify license status - """ - - def authenticate(self, request, username, password): - if not django_settings.RADIUS_SERVER: - return None - return super(RADIUSBackend, self).authenticate(request, username, password) - - def get_user(self, user_id): - if not django_settings.RADIUS_SERVER: - return None - user = super(RADIUSBackend, self).get_user(user_id) - if not user.has_usable_password(): - return user - - def get_django_user(self, username, password=None, groups=[], is_staff=False, is_superuser=False): - return _get_or_set_enterprise_user(force_str(username), force_str(password), 'radius') - - -class TACACSPlusBackend(object): - """ - Custom TACACS+ auth backend for AWX - """ - - def authenticate(self, request, username, password): - if not django_settings.TACACSPLUS_HOST: - return None - try: - # Upstream TACACS+ client does not accept non-string, so convert if needed. - tacacs_client = tacacs_plus.TACACSClient( - django_settings.TACACSPLUS_HOST, - django_settings.TACACSPLUS_PORT, - django_settings.TACACSPLUS_SECRET, - timeout=django_settings.TACACSPLUS_SESSION_TIMEOUT, - ) - auth_kwargs = {'authen_type': tacacs_plus.TAC_PLUS_AUTHEN_TYPES[django_settings.TACACSPLUS_AUTH_PROTOCOL]} - if django_settings.TACACSPLUS_AUTH_PROTOCOL: - client_ip = self._get_client_ip(request) - if client_ip: - auth_kwargs['rem_addr'] = client_ip - auth = tacacs_client.authenticate(username, password, **auth_kwargs) - except Exception as e: - logger.exception("TACACS+ Authentication Error: %s" % str(e)) - return None - if auth.valid: - return _get_or_set_enterprise_user(username, password, 'tacacs+') - - def get_user(self, user_id): - if not django_settings.TACACSPLUS_HOST: - return None - try: - return User.objects.get(pk=user_id) - except User.DoesNotExist: - return None - - def _get_client_ip(self, request): - if not request or not hasattr(request, 'META'): - return None - - x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR') - if x_forwarded_for: - ip = x_forwarded_for.split(',')[0] - else: - ip = request.META.get('REMOTE_ADDR') - return ip - - -class TowerSAMLIdentityProvider(BaseSAMLIdentityProvider): - """ - Custom Identity Provider to make attributes to what we expect. - """ - - def get_user_permanent_id(self, attributes): - uid = attributes[self.conf.get('attr_user_permanent_id', OID_USERID)] - if isinstance(uid, str): - return uid - return uid[0] - - def get_attr(self, attributes, conf_key, default_attribute): - """ - Get the attribute 'default_attribute' out of the attributes, - unless self.conf[conf_key] overrides the default by specifying - another attribute to use. - """ - key = self.conf.get(conf_key, default_attribute) - value = attributes[key] if key in attributes else None - # In certain implementations (like https://pagure.io/ipsilon) this value is a string, not a list - if isinstance(value, (list, tuple)): - value = value[0] - if conf_key in ('attr_first_name', 'attr_last_name', 'attr_username', 'attr_email') and value is None: - logger.warning( - "Could not map user detail '%s' from SAML attribute '%s'; update SOCIAL_AUTH_SAML_ENABLED_IDPS['%s']['%s'] with the correct SAML attribute.", - conf_key[5:], - key, - self.name, - conf_key, - ) - return str(value) if value is not None else value - - -class SAMLAuth(BaseSAMLAuth): - """ - Custom SAMLAuth backend to verify license status - """ - - def get_idp(self, idp_name): - idp_config = self.setting('ENABLED_IDPS')[idp_name] - return TowerSAMLIdentityProvider(idp_name, **idp_config) - - def authenticate(self, request, *args, **kwargs): - if not all( - [ - django_settings.SOCIAL_AUTH_SAML_SP_ENTITY_ID, - django_settings.SOCIAL_AUTH_SAML_SP_PUBLIC_CERT, - django_settings.SOCIAL_AUTH_SAML_SP_PRIVATE_KEY, - django_settings.SOCIAL_AUTH_SAML_ORG_INFO, - django_settings.SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, - django_settings.SOCIAL_AUTH_SAML_SUPPORT_CONTACT, - django_settings.SOCIAL_AUTH_SAML_ENABLED_IDPS, - ] - ): - return None - pipeline_result = super(SAMLAuth, self).authenticate(request, *args, **kwargs) - - if isinstance(pipeline_result, HttpResponse): - return pipeline_result - else: - user = pipeline_result - - # Comes from https://github.com/omab/python-social-auth/blob/v0.2.21/social/backends/base.py#L91 - if getattr(user, 'is_new', False): - enterprise_auth = _decorate_enterprise_user(user, 'saml') - logger.debug("Created enterprise user %s from %s backend." % (user.username, enterprise_auth.get_provider_display())) - elif user and not user.is_in_enterprise_category('saml'): - return None - if user: - logger.debug("Enterprise user %s already created in Tower." % user.username) - return user - - def get_user(self, user_id): - if not all( - [ - django_settings.SOCIAL_AUTH_SAML_SP_ENTITY_ID, - django_settings.SOCIAL_AUTH_SAML_SP_PUBLIC_CERT, - django_settings.SOCIAL_AUTH_SAML_SP_PRIVATE_KEY, - django_settings.SOCIAL_AUTH_SAML_ORG_INFO, - django_settings.SOCIAL_AUTH_SAML_TECHNICAL_CONTACT, - django_settings.SOCIAL_AUTH_SAML_SUPPORT_CONTACT, - django_settings.SOCIAL_AUTH_SAML_ENABLED_IDPS, - ] - ): - return None - return super(SAMLAuth, self).get_user(user_id) - - -def _update_m2m_from_groups(ldap_user, opts, remove=True): - """ - Hepler function to evaluate the LDAP team/org options to determine if LDAP user should - be a member of the team/org based on their ldap group dns. - - Returns: - True - User should be added - False - User should be removed - None - Users membership should not be changed - """ - if opts is None: - return None - elif not opts: - pass - elif isinstance(opts, bool) and opts is True: - return True - else: - if isinstance(opts, str): - opts = [opts] - # If any of the users groups matches any of the list options - for group_dn in opts: - if not isinstance(group_dn, str): - continue - if ldap_user._get_groups().is_member_of(group_dn): - return True - if remove: - return False - return None - - -@receiver(populate_user, dispatch_uid='populate-ldap-user') -def on_populate_user(sender, **kwargs): - """ - Handle signal from LDAP backend to populate the user object. Update user - organization/team memberships according to their LDAP groups. - """ - user = kwargs['user'] - ldap_user = kwargs['ldap_user'] - backend = ldap_user.backend - - # Boolean to determine if we should force an user update - # to avoid duplicate SQL update statements - force_user_update = False - - # Prefetch user's groups to prevent LDAP queries for each org/team when - # checking membership. - ldap_user._get_groups().get_group_dns() - - # If the LDAP user has a first or last name > $maxlen chars, truncate it - for field in ('first_name', 'last_name'): - max_len = User._meta.get_field(field).max_length - field_len = len(getattr(user, field)) - if field_len > max_len: - setattr(user, field, getattr(user, field)[:max_len]) - force_user_update = True - logger.warning('LDAP user {} has {} > max {} characters'.format(user.username, field, max_len)) - - org_map = getattr(backend.settings, 'ORGANIZATION_MAP', {}) - team_map_settings = getattr(backend.settings, 'TEAM_MAP', {}) - orgs_list = list(org_map.keys()) - team_map = {} - for team_name, team_opts in team_map_settings.items(): - if not team_opts.get('organization', None): - # You can't save the LDAP config in the UI w/o an org (or '' or null as the org) so if we somehow got this condition its an error - logger.error("Team named {} in LDAP team map settings is invalid due to missing organization".format(team_name)) - continue - team_map[team_name] = team_opts['organization'] - - create_org_and_teams(orgs_list, team_map, 'LDAP') - - # Compute in memory what the state is of the different LDAP orgs - org_roles_and_ldap_attributes = {'admin_role': 'admins', 'auditor_role': 'auditors', 'member_role': 'users'} - desired_org_states = {} - for org_name, org_opts in org_map.items(): - remove = bool(org_opts.get('remove', True)) - desired_org_states[org_name] = {} - for org_role_name in org_roles_and_ldap_attributes.keys(): - ldap_name = org_roles_and_ldap_attributes[org_role_name] - opts = org_opts.get(ldap_name, None) - remove = bool(org_opts.get('remove_{}'.format(ldap_name), remove)) - desired_org_states[org_name][org_role_name] = _update_m2m_from_groups(ldap_user, opts, remove) - - # If everything returned None (because there was no configuration) we can remove this org from our map - # This will prevent us from loading the org in the next query - if all(desired_org_states[org_name][org_role_name] is None for org_role_name in org_roles_and_ldap_attributes.keys()): - del desired_org_states[org_name] - - # Compute in memory what the state is of the different LDAP teams - desired_team_states = {} - for team_name, team_opts in team_map_settings.items(): - if 'organization' not in team_opts: - continue - users_opts = team_opts.get('users', None) - remove = bool(team_opts.get('remove', True)) - state = _update_m2m_from_groups(ldap_user, users_opts, remove) - if state is not None: - organization = team_opts['organization'] - if organization not in desired_team_states: - desired_team_states[organization] = {} - desired_team_states[organization][team_name] = {'member_role': state} - - # Check if user.profile is available, otherwise force user.save() - try: - _ = user.profile - except ValueError: - force_user_update = True - finally: - if force_user_update: - user.save() - - # Update user profile to store LDAP DN. - profile = user.profile - if profile.ldap_dn != ldap_user.dn: - profile.ldap_dn = ldap_user.dn - profile.save() - - reconcile_users_org_team_mappings(user, desired_org_states, desired_team_states, 'LDAP') diff --git a/awx/sso/common.py b/awx/sso/common.py deleted file mode 100644 index 99abc51d5a03..000000000000 --- a/awx/sso/common.py +++ /dev/null @@ -1,213 +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 LDAP 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 - - # Previously this method also checked for active configuration which meant that if a user logged in from LDAP - # and then LDAP was no longer configured it would "convert" the user from an LDAP account_type to none. - # This did have one benefit that if a login type was removed intentionally the user could be given a username password. - # But it had a limitation that the user would have to have an active session (or an admin would have to go set a temp password). - # It also lead to the side affect that if LDAP was ever reconfigured the user would convert back to LDAP but still have a local password. - # That local password could then be used to bypass LDAP authentication. - try: - if user.pk and user.profile.ldap_dn and not user.has_usable_password(): - account_type = "ldap" - except AttributeError: - pass - - 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 - - # Append LDAP, Radius, TACACS+ and SAML options - settings_that_turn_on_remote_auth = [ - 'AUTH_LDAP_SERVER_URI', - 'SOCIAL_AUTH_SAML_ENABLED_IDPS', - 'RADIUS_SERVER', - 'TACACSPLUS_HOST', - ] - # Also include any SOCAIL_AUTH_*KEY (except SAML) - for social_auth_key in dir(settings): - if social_auth_key.startswith('SOCIAL_AUTH_') and social_auth_key.endswith('_KEY') and 'SAML' not in social_auth_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 03640fccd8ae..000000000000 --- a/awx/sso/conf.py +++ /dev/null @@ -1,1667 +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 _ - -# Django REST Framework -from rest_framework import serializers - -# AWX -from awx.conf import register, register_validate, fields -from awx.sso.fields import ( - AuthenticationBackendsField, - LDAPConnectionOptionsField, - LDAPDNField, - LDAPDNWithUserField, - LDAPGroupTypeField, - LDAPGroupTypeParamsField, - LDAPOrganizationMapField, - LDAPSearchField, - LDAPSearchUnionField, - LDAPServerURIField, - LDAPTeamMapField, - LDAPUserAttrMapField, - LDAPUserFlagsField, - SAMLContactField, - SAMLEnabledIdPsField, - SAMLOrgAttrField, - SAMLOrgInfoField, - SAMLSecurityField, - SAMLTeamAttrField, - SAMLUserFlagsAttrField, - SocialOrganizationMapField, - SocialTeamMapField, -) -from awx.main.validators import validate_private_key, validate_certificate -from awx.sso.validators import validate_ldap_bind_dn, validate_tacacsplus_disallow_nonascii # noqa - - -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', - ) - - ############################################################################### - # LDAP AUTHENTICATION SETTINGS - ############################################################################### - - def _register_ldap(append=None): - append_str = '_{}'.format(append) if append else '' - - register( - 'AUTH_LDAP{}_SERVER_URI'.format(append_str), - field_class=LDAPServerURIField, - allow_blank=True, - default='', - label=_('LDAP Server URI'), - help_text=_( - 'URI to connect to LDAP server, such as "ldap://ldap.example.com:389" ' - '(non-SSL) or "ldaps://ldap.example.com:636" (SSL). Multiple LDAP ' - 'servers may be specified by separating with spaces or commas. LDAP ' - 'authentication is disabled if this parameter is empty.' - ), - category=_('LDAP'), - category_slug='ldap', - placeholder='ldaps://ldap.example.com:636', - ) - - register( - 'AUTH_LDAP{}_BIND_DN'.format(append_str), - field_class=fields.CharField, - allow_blank=True, - default='', - validators=[validate_ldap_bind_dn], - label=_('LDAP Bind DN'), - help_text=_( - 'DN (Distinguished Name) of user to bind for all search queries. This' - ' is the system user account we will use to login to query LDAP for other' - ' user information. Refer to the documentation for example syntax.' - ), - category=_('LDAP'), - category_slug='ldap', - ) - - register( - 'AUTH_LDAP{}_BIND_PASSWORD'.format(append_str), - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('LDAP Bind Password'), - help_text=_('Password used to bind LDAP user account.'), - category=_('LDAP'), - category_slug='ldap', - encrypted=True, - ) - - register( - 'AUTH_LDAP{}_START_TLS'.format(append_str), - field_class=fields.BooleanField, - default=False, - label=_('LDAP Start TLS'), - help_text=_('Whether to enable TLS when the LDAP connection is not using SSL.'), - category=_('LDAP'), - category_slug='ldap', - ) - - register( - 'AUTH_LDAP{}_CONNECTION_OPTIONS'.format(append_str), - field_class=LDAPConnectionOptionsField, - default={'OPT_REFERRALS': 0, 'OPT_NETWORK_TIMEOUT': 30}, - label=_('LDAP Connection Options'), - help_text=_( - 'Additional options to set for the LDAP connection. LDAP ' - 'referrals are disabled by default (to prevent certain LDAP ' - 'queries from hanging with AD). Option names should be strings ' - '(e.g. "OPT_REFERRALS"). Refer to ' - 'https://www.python-ldap.org/doc/html/ldap.html#options for ' - 'possible options and values that can be set.' - ), - category=_('LDAP'), - category_slug='ldap', - placeholder=collections.OrderedDict([('OPT_REFERRALS', 0), ('OPT_NETWORK_TIMEOUT', 30)]), - ) - - register( - 'AUTH_LDAP{}_USER_SEARCH'.format(append_str), - field_class=LDAPSearchUnionField, - default=[], - label=_('LDAP User Search'), - help_text=_( - 'LDAP search query to find users. Any user that matches the given ' - 'pattern will be able to login to the service. The user should also be ' - 'mapped into an organization (as defined in the ' - 'AUTH_LDAP_ORGANIZATION_MAP setting). If multiple search queries ' - 'need to be supported use of "LDAPUnion" is possible. See ' - 'the documentation for details.' - ), - category=_('LDAP'), - category_slug='ldap', - placeholder=('OU=Users,DC=example,DC=com', 'SCOPE_SUBTREE', '(sAMAccountName=%(user)s)'), - ) - - register( - 'AUTH_LDAP{}_USER_DN_TEMPLATE'.format(append_str), - field_class=LDAPDNWithUserField, - allow_blank=True, - allow_null=True, - default=None, - label=_('LDAP User DN Template'), - help_text=_( - 'Alternative to user search, if user DNs are all of the same ' - 'format. This approach is more efficient for user lookups than ' - 'searching if it is usable in your organizational environment. If ' - 'this setting has a value it will be used instead of ' - 'AUTH_LDAP_USER_SEARCH.' - ), - category=_('LDAP'), - category_slug='ldap', - placeholder='uid=%(user)s,OU=Users,DC=example,DC=com', - ) - - register( - 'AUTH_LDAP{}_USER_ATTR_MAP'.format(append_str), - field_class=LDAPUserAttrMapField, - default={}, - label=_('LDAP User Attribute Map'), - help_text=_( - 'Mapping of LDAP user schema to API user attributes. The default' - ' setting is valid for ActiveDirectory but users with other LDAP' - ' configurations may need to change the values. Refer to the' - ' documentation for additional details.' - ), - category=_('LDAP'), - category_slug='ldap', - placeholder=collections.OrderedDict([('first_name', 'givenName'), ('last_name', 'sn'), ('email', 'mail')]), - ) - - register( - 'AUTH_LDAP{}_GROUP_SEARCH'.format(append_str), - field_class=LDAPSearchField, - default=[], - label=_('LDAP Group Search'), - help_text=_( - 'Users are mapped to organizations based on their membership in LDAP' - ' groups. This setting defines the LDAP search query to find groups. ' - 'Unlike the user search, group search does not support LDAPSearchUnion.' - ), - category=_('LDAP'), - category_slug='ldap', - placeholder=('DC=example,DC=com', 'SCOPE_SUBTREE', '(objectClass=group)'), - ) - - register( - 'AUTH_LDAP{}_GROUP_TYPE'.format(append_str), - field_class=LDAPGroupTypeField, - label=_('LDAP Group Type'), - help_text=_( - 'The group type may need to be changed based on the type of the ' - 'LDAP server. Values are listed at: ' - 'https://django-auth-ldap.readthedocs.io/en/stable/groups.html#types-of-groups' - ), - category=_('LDAP'), - category_slug='ldap', - default='MemberDNGroupType', - depends_on=['AUTH_LDAP{}_GROUP_TYPE_PARAMS'.format(append_str)], - ) - - register( - 'AUTH_LDAP{}_GROUP_TYPE_PARAMS'.format(append_str), - field_class=LDAPGroupTypeParamsField, - label=_('LDAP Group Type Parameters'), - help_text=_('Key value parameters to send the chosen group type init method.'), - category=_('LDAP'), - category_slug='ldap', - default=collections.OrderedDict([('member_attr', 'member'), ('name_attr', 'cn')]), - placeholder=collections.OrderedDict([('ldap_group_user_attr', 'legacyuid'), ('member_attr', 'member'), ('name_attr', 'cn')]), - depends_on=['AUTH_LDAP{}_GROUP_TYPE'.format(append_str)], - ) - - register( - 'AUTH_LDAP{}_REQUIRE_GROUP'.format(append_str), - field_class=LDAPDNField, - allow_blank=True, - allow_null=True, - default=None, - label=_('LDAP Require Group'), - help_text=_( - 'Group DN required to login. If specified, user must be a member ' - 'of this group to login via LDAP. If not set, everyone in LDAP ' - 'that matches the user search will be able to login to the service. ' - 'Only one require group is supported.' - ), - category=_('LDAP'), - category_slug='ldap', - placeholder='CN=Service Users,OU=Users,DC=example,DC=com', - ) - - register( - 'AUTH_LDAP{}_DENY_GROUP'.format(append_str), - field_class=LDAPDNField, - allow_blank=True, - allow_null=True, - default=None, - label=_('LDAP Deny Group'), - help_text=_( - 'Group DN denied from login. If specified, user will not be allowed to login if a member of this group. Only one deny group is supported.' - ), - category=_('LDAP'), - category_slug='ldap', - placeholder='CN=Disabled Users,OU=Users,DC=example,DC=com', - ) - - register( - 'AUTH_LDAP{}_USER_FLAGS_BY_GROUP'.format(append_str), - field_class=LDAPUserFlagsField, - default={}, - label=_('LDAP User Flags By Group'), - help_text=_( - 'Retrieve users from a given group. At this time, superuser and system' - ' auditors are the only groups supported. Refer to the' - ' documentation for more detail.' - ), - category=_('LDAP'), - category_slug='ldap', - placeholder=collections.OrderedDict( - [('is_superuser', 'CN=Domain Admins,CN=Users,DC=example,DC=com'), ('is_system_auditor', 'CN=Domain Auditors,CN=Users,DC=example,DC=com')] - ), - ) - - register( - 'AUTH_LDAP{}_ORGANIZATION_MAP'.format(append_str), - field_class=LDAPOrganizationMapField, - default={}, - label=_('LDAP Organization Map'), - help_text=_( - 'Mapping between organization admins/users and LDAP groups. This ' - 'controls which users are placed into which organizations ' - 'relative to their LDAP group memberships. Configuration details ' - 'are available in the documentation.' - ), - category=_('LDAP'), - category_slug='ldap', - placeholder=collections.OrderedDict( - [ - ( - 'Test Org', - collections.OrderedDict( - [ - ('admins', 'CN=Domain Admins,CN=Users,DC=example,DC=com'), - ('auditors', 'CN=Domain Auditors,CN=Users,DC=example,DC=com'), - ('users', ['CN=Domain Users,CN=Users,DC=example,DC=com']), - ('remove_users', True), - ('remove_admins', True), - ] - ), - ), - ( - 'Test Org 2', - collections.OrderedDict( - [('admins', 'CN=Administrators,CN=Builtin,DC=example,DC=com'), ('users', True), ('remove_users', True), ('remove_admins', True)] - ), - ), - ] - ), - ) - - register( - 'AUTH_LDAP{}_TEAM_MAP'.format(append_str), - field_class=LDAPTeamMapField, - default={}, - label=_('LDAP Team Map'), - help_text=_('Mapping between team members (users) and LDAP groups. Configuration details are available in the documentation.'), - category=_('LDAP'), - category_slug='ldap', - placeholder=collections.OrderedDict( - [ - ( - 'My Team', - collections.OrderedDict([('organization', 'Test Org'), ('users', ['CN=Domain Users,CN=Users,DC=example,DC=com']), ('remove', True)]), - ), - ( - 'Other Team', - collections.OrderedDict([('organization', 'Test Org 2'), ('users', 'CN=Other Users,CN=Users,DC=example,DC=com'), ('remove', False)]), - ), - ] - ), - ) - - _register_ldap() - _register_ldap('1') - _register_ldap('2') - _register_ldap('3') - _register_ldap('4') - _register_ldap('5') - - ############################################################################### - # RADIUS AUTHENTICATION SETTINGS - ############################################################################### - - register( - 'RADIUS_SERVER', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('RADIUS Server'), - help_text=_('Hostname/IP of RADIUS server. RADIUS authentication is disabled if this setting is empty.'), - category=_('RADIUS'), - category_slug='radius', - placeholder='radius.example.com', - ) - - register( - 'RADIUS_PORT', - field_class=fields.IntegerField, - min_value=1, - max_value=65535, - default=1812, - label=_('RADIUS Port'), - help_text=_('Port of RADIUS server.'), - category=_('RADIUS'), - category_slug='radius', - ) - - register( - 'RADIUS_SECRET', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('RADIUS Secret'), - help_text=_('Shared secret for authenticating to RADIUS server.'), - category=_('RADIUS'), - category_slug='radius', - encrypted=True, - ) - - ############################################################################### - # TACACSPLUS AUTHENTICATION SETTINGS - ############################################################################### - - register( - 'TACACSPLUS_HOST', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('TACACS+ Server'), - help_text=_('Hostname of TACACS+ server.'), - category=_('TACACS+'), - category_slug='tacacsplus', - ) - - register( - 'TACACSPLUS_PORT', - field_class=fields.IntegerField, - min_value=1, - max_value=65535, - default=49, - label=_('TACACS+ Port'), - help_text=_('Port number of TACACS+ server.'), - category=_('TACACS+'), - category_slug='tacacsplus', - ) - - register( - 'TACACSPLUS_SECRET', - field_class=fields.CharField, - allow_blank=True, - default='', - validators=[validate_tacacsplus_disallow_nonascii], - label=_('TACACS+ Secret'), - help_text=_('Shared secret for authenticating to TACACS+ server.'), - category=_('TACACS+'), - category_slug='tacacsplus', - encrypted=True, - ) - - register( - 'TACACSPLUS_SESSION_TIMEOUT', - field_class=fields.IntegerField, - min_value=0, - default=5, - label=_('TACACS+ Auth Session Timeout'), - help_text=_('TACACS+ session timeout value in seconds, 0 disables timeout.'), - category=_('TACACS+'), - category_slug='tacacsplus', - unit=_('seconds'), - ) - - register( - 'TACACSPLUS_AUTH_PROTOCOL', - field_class=fields.ChoiceField, - choices=['ascii', 'pap'], - default='ascii', - label=_('TACACS+ Authentication Protocol'), - help_text=_('Choose the authentication protocol used by TACACS+ client.'), - category=_('TACACS+'), - category_slug='tacacsplus', - ) - - register( - 'TACACSPLUS_REM_ADDR', - field_class=fields.BooleanField, - default=True, - label=_('TACACS+ client address sending enabled'), - help_text=_('Enable the client address sending by TACACS+ client.'), - category=_('TACACS+'), - category_slug='tacacsplus', - ) - - ############################################################################### - # GOOGLE OAUTH2 AUTHENTICATION SETTINGS - ############################################################################### - - register( - 'SOCIAL_AUTH_GOOGLE_OAUTH2_CALLBACK_URL', - field_class=fields.CharField, - read_only=True, - default=SocialAuthCallbackURL('google-oauth2'), - label=_('Google OAuth2 Callback URL'), - help_text=_( - 'Provide this URL as the callback URL for your application as part of your registration process. Refer to the documentation for more detail.' - ), - category=_('Google OAuth2'), - category_slug='google-oauth2', - depends_on=['TOWER_URL_BASE'], - ) - - register( - 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('Google OAuth2 Key'), - help_text=_('The OAuth2 key from your web application.'), - category=_('Google OAuth2'), - category_slug='google-oauth2', - placeholder='528620852399-gm2dt4hrl2tsj67fqamk09k1e0ad6gd8.apps.googleusercontent.com', - ) - - register( - 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('Google OAuth2 Secret'), - help_text=_('The OAuth2 secret from your web application.'), - category=_('Google OAuth2'), - category_slug='google-oauth2', - placeholder='q2fMVCmEregbg-drvebPp8OW', - encrypted=True, - ) - - register( - 'SOCIAL_AUTH_GOOGLE_OAUTH2_WHITELISTED_DOMAINS', - field_class=fields.StringListField, - default=[], - label=_('Google OAuth2 Allowed Domains'), - help_text=_('Update this setting to restrict the domains who are allowed to login using Google OAuth2.'), - category=_('Google OAuth2'), - category_slug='google-oauth2', - placeholder=['example.com'], - ) - - register( - 'SOCIAL_AUTH_GOOGLE_OAUTH2_AUTH_EXTRA_ARGUMENTS', - field_class=fields.DictField, - default={}, - label=_('Google OAuth2 Extra Arguments'), - help_text=_( - 'Extra arguments for Google OAuth2 login. You can restrict it to' - ' only allow a single domain to authenticate, even if the user is' - ' logged in with multple Google accounts. Refer to the' - ' documentation for more detail.' - ), - category=_('Google OAuth2'), - category_slug='google-oauth2', - placeholder={'hd': 'example.com'}, - ) - - register( - 'SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP', - field_class=SocialOrganizationMapField, - allow_null=True, - default=None, - label=_('Google OAuth2 Organization Map'), - help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT, - category=_('Google OAuth2'), - category_slug='google-oauth2', - placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, - ) - - register( - 'SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP', - field_class=SocialTeamMapField, - allow_null=True, - default=None, - label=_('Google OAuth2 Team Map'), - help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT, - category=_('Google OAuth2'), - category_slug='google-oauth2', - placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, - ) - - ############################################################################### - # GITHUB OAUTH2 AUTHENTICATION SETTINGS - ############################################################################### - - register( - 'SOCIAL_AUTH_GITHUB_CALLBACK_URL', - field_class=fields.CharField, - read_only=True, - default=SocialAuthCallbackURL('github'), - label=_('GitHub OAuth2 Callback URL'), - help_text=_( - 'Provide this URL as the callback URL for your application as part of your registration process. Refer to the documentation for more detail.' - ), - category=_('GitHub OAuth2'), - category_slug='github', - depends_on=['TOWER_URL_BASE'], - ) - - register( - 'SOCIAL_AUTH_GITHUB_KEY', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub OAuth2 Key'), - help_text=_('The OAuth2 key (Client ID) from your GitHub developer application.'), - category=_('GitHub OAuth2'), - category_slug='github', - ) - - register( - 'SOCIAL_AUTH_GITHUB_SECRET', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub OAuth2 Secret'), - help_text=_('The OAuth2 secret (Client Secret) from your GitHub developer application.'), - category=_('GitHub OAuth2'), - category_slug='github', - encrypted=True, - ) - - register( - 'SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP', - field_class=SocialOrganizationMapField, - allow_null=True, - default=None, - label=_('GitHub OAuth2 Organization Map'), - help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT, - category=_('GitHub OAuth2'), - category_slug='github', - placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, - ) - - register( - 'SOCIAL_AUTH_GITHUB_TEAM_MAP', - field_class=SocialTeamMapField, - allow_null=True, - default=None, - label=_('GitHub OAuth2 Team Map'), - help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT, - category=_('GitHub OAuth2'), - category_slug='github', - placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, - ) - - ############################################################################### - # GITHUB ORG OAUTH2 AUTHENTICATION SETTINGS - ############################################################################### - - register( - 'SOCIAL_AUTH_GITHUB_ORG_CALLBACK_URL', - field_class=fields.CharField, - read_only=True, - default=SocialAuthCallbackURL('github-org'), - label=_('GitHub Organization OAuth2 Callback URL'), - help_text=_( - 'Provide this URL as the callback URL for your application as part of your registration process. Refer to the documentation for more detail.' - ), - category=_('GitHub Organization OAuth2'), - category_slug='github-org', - depends_on=['TOWER_URL_BASE'], - ) - - register( - 'SOCIAL_AUTH_GITHUB_ORG_KEY', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Organization OAuth2 Key'), - help_text=_('The OAuth2 key (Client ID) from your GitHub organization application.'), - category=_('GitHub Organization OAuth2'), - category_slug='github-org', - ) - - register( - 'SOCIAL_AUTH_GITHUB_ORG_SECRET', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Organization OAuth2 Secret'), - help_text=_('The OAuth2 secret (Client Secret) from your GitHub organization application.'), - category=_('GitHub Organization OAuth2'), - category_slug='github-org', - encrypted=True, - ) - - register( - 'SOCIAL_AUTH_GITHUB_ORG_NAME', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Organization Name'), - help_text=_('The name of your GitHub organization, as used in your organization\'s URL: https://github.com//.'), - category=_('GitHub Organization OAuth2'), - category_slug='github-org', - ) - - register( - 'SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP', - field_class=SocialOrganizationMapField, - allow_null=True, - default=None, - label=_('GitHub Organization OAuth2 Organization Map'), - help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT, - category=_('GitHub Organization OAuth2'), - category_slug='github-org', - placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, - ) - - register( - 'SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP', - field_class=SocialTeamMapField, - allow_null=True, - default=None, - label=_('GitHub Organization OAuth2 Team Map'), - help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT, - category=_('GitHub Organization OAuth2'), - category_slug='github-org', - placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, - ) - - ############################################################################### - # GITHUB TEAM OAUTH2 AUTHENTICATION SETTINGS - ############################################################################### - - register( - 'SOCIAL_AUTH_GITHUB_TEAM_CALLBACK_URL', - field_class=fields.CharField, - read_only=True, - default=SocialAuthCallbackURL('github-team'), - label=_('GitHub Team OAuth2 Callback URL'), - help_text=_( - 'Create an organization-owned application at ' - 'https://github.com/organizations//settings/applications ' - 'and obtain an OAuth2 key (Client ID) and secret (Client Secret). ' - 'Provide this URL as the callback URL for your application.' - ), - category=_('GitHub Team OAuth2'), - category_slug='github-team', - depends_on=['TOWER_URL_BASE'], - ) - - register( - 'SOCIAL_AUTH_GITHUB_TEAM_KEY', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Team OAuth2 Key'), - help_text=_('The OAuth2 key (Client ID) from your GitHub organization application.'), - category=_('GitHub Team OAuth2'), - category_slug='github-team', - ) - - register( - 'SOCIAL_AUTH_GITHUB_TEAM_SECRET', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Team OAuth2 Secret'), - help_text=_('The OAuth2 secret (Client Secret) from your GitHub organization application.'), - category=_('GitHub Team OAuth2'), - category_slug='github-team', - encrypted=True, - ) - - register( - 'SOCIAL_AUTH_GITHUB_TEAM_ID', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Team ID'), - help_text=_('Find the numeric team ID using the Github API: http://fabian-kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/.'), - category=_('GitHub Team OAuth2'), - category_slug='github-team', - ) - - register( - 'SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP', - field_class=SocialOrganizationMapField, - allow_null=True, - default=None, - label=_('GitHub Team OAuth2 Organization Map'), - help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT, - category=_('GitHub Team OAuth2'), - category_slug='github-team', - placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, - ) - - register( - 'SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP', - field_class=SocialTeamMapField, - allow_null=True, - default=None, - label=_('GitHub Team OAuth2 Team Map'), - help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT, - category=_('GitHub Team OAuth2'), - category_slug='github-team', - placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, - ) - - ############################################################################### - # GITHUB ENTERPRISE OAUTH2 AUTHENTICATION SETTINGS - ############################################################################### - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_CALLBACK_URL', - field_class=fields.CharField, - read_only=True, - default=SocialAuthCallbackURL('github-enterprise'), - label=_('GitHub Enterprise OAuth2 Callback URL'), - help_text=_( - 'Provide this URL as the callback URL for your application as part of your registration process. Refer to the documentation for more detail.' - ), - category=_('GitHub Enterprise OAuth2'), - category_slug='github-enterprise', - depends_on=['TOWER_URL_BASE'], - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise URL'), - help_text=_('The URL for your Github Enterprise instance, e.g.: http(s)://hostname/. Refer to Github Enterprise documentation for more details.'), - category=_('GitHub Enterprise OAuth2'), - category_slug='github-enterprise', - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise API URL'), - help_text=_( - 'The API URL for your GitHub Enterprise instance, e.g.: http(s)://hostname/api/v3/. Refer to Github Enterprise documentation for more details.' - ), - category=_('GitHub Enterprise OAuth2'), - category_slug='github-enterprise', - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise OAuth2 Key'), - help_text=_('The OAuth2 key (Client ID) from your GitHub Enterprise developer application.'), - category=_('GitHub Enterprise OAuth2'), - category_slug='github-enterprise', - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise OAuth2 Secret'), - help_text=_('The OAuth2 secret (Client Secret) from your GitHub Enterprise developer application.'), - category=_('GitHub OAuth2'), - category_slug='github-enterprise', - encrypted=True, - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORGANIZATION_MAP', - field_class=SocialOrganizationMapField, - allow_null=True, - default=None, - label=_('GitHub Enterprise OAuth2 Organization Map'), - help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT, - category=_('GitHub Enterprise OAuth2'), - category_slug='github-enterprise', - placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_MAP', - field_class=SocialTeamMapField, - allow_null=True, - default=None, - label=_('GitHub Enterprise OAuth2 Team Map'), - help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT, - category=_('GitHub Enterprise OAuth2'), - category_slug='github-enterprise', - placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, - ) - - ############################################################################### - # GITHUB ENTERPRISE ORG OAUTH2 AUTHENTICATION SETTINGS - ############################################################################### - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_CALLBACK_URL', - field_class=fields.CharField, - read_only=True, - default=SocialAuthCallbackURL('github-enterprise-org'), - label=_('GitHub Enterprise Organization OAuth2 Callback URL'), - help_text=_( - 'Provide this URL as the callback URL for your application as part of your registration process. Refer to the documentation for more detail.' - ), - category=_('GitHub Enterprise Organization OAuth2'), - category_slug='github-enterprise-org', - depends_on=['TOWER_URL_BASE'], - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise Organization URL'), - help_text=_('The URL for your Github Enterprise instance, e.g.: http(s)://hostname/. Refer to Github Enterprise documentation for more details.'), - category=_('GitHub Enterprise OAuth2'), - category_slug='github-enterprise-org', - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise Organization API URL'), - help_text=_( - 'The API URL for your GitHub Enterprise instance, e.g.: http(s)://hostname/api/v3/. Refer to Github Enterprise documentation for more details.' - ), - category=_('GitHub Enterprise OAuth2'), - category_slug='github-enterprise-org', - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise Organization OAuth2 Key'), - help_text=_('The OAuth2 key (Client ID) from your GitHub Enterprise organization application.'), - category=_('GitHub Enterprise Organization OAuth2'), - category_slug='github-enterprise-org', - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise Organization OAuth2 Secret'), - help_text=_('The OAuth2 secret (Client Secret) from your GitHub Enterprise organization application.'), - category=_('GitHub Enterprise Organization OAuth2'), - category_slug='github-enterprise-org', - encrypted=True, - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise Organization Name'), - help_text=_('The name of your GitHub Enterprise organization, as used in your organization\'s URL: https://github.com//.'), - category=_('GitHub Enterprise Organization OAuth2'), - category_slug='github-enterprise-org', - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_ORGANIZATION_MAP', - field_class=SocialOrganizationMapField, - allow_null=True, - default=None, - label=_('GitHub Enterprise Organization OAuth2 Organization Map'), - help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT, - category=_('GitHub Enterprise Organization OAuth2'), - category_slug='github-enterprise-org', - placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_TEAM_MAP', - field_class=SocialTeamMapField, - allow_null=True, - default=None, - label=_('GitHub Enterprise Organization OAuth2 Team Map'), - help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT, - category=_('GitHub Enterprise Organization OAuth2'), - category_slug='github-enterprise-org', - placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, - ) - - ############################################################################### - # GITHUB ENTERPRISE TEAM OAUTH2 AUTHENTICATION SETTINGS - ############################################################################### - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_CALLBACK_URL', - field_class=fields.CharField, - read_only=True, - default=SocialAuthCallbackURL('github-enterprise-team'), - label=_('GitHub Enterprise Team OAuth2 Callback URL'), - help_text=_( - 'Create an organization-owned application at ' - 'https://github.com/organizations//settings/applications ' - 'and obtain an OAuth2 key (Client ID) and secret (Client Secret). ' - 'Provide this URL as the callback URL for your application.' - ), - category=_('GitHub Enterprise Team OAuth2'), - category_slug='github-enterprise-team', - depends_on=['TOWER_URL_BASE'], - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise Team URL'), - help_text=_('The URL for your Github Enterprise instance, e.g.: http(s)://hostname/. Refer to Github Enterprise documentation for more details.'), - category=_('GitHub Enterprise OAuth2'), - category_slug='github-enterprise-team', - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise Team API URL'), - help_text=_( - 'The API URL for your GitHub Enterprise instance, e.g.: http(s)://hostname/api/v3/. Refer to Github Enterprise documentation for more details.' - ), - category=_('GitHub Enterprise OAuth2'), - category_slug='github-enterprise-team', - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise Team OAuth2 Key'), - help_text=_('The OAuth2 key (Client ID) from your GitHub Enterprise organization application.'), - category=_('GitHub Enterprise Team OAuth2'), - category_slug='github-enterprise-team', - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise Team OAuth2 Secret'), - help_text=_('The OAuth2 secret (Client Secret) from your GitHub Enterprise organization application.'), - category=_('GitHub Enterprise Team OAuth2'), - category_slug='github-enterprise-team', - encrypted=True, - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('GitHub Enterprise Team ID'), - help_text=_('Find the numeric team ID using the Github Enterprise API: http://fabian-kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/.'), - category=_('GitHub Enterprise Team OAuth2'), - category_slug='github-enterprise-team', - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ORGANIZATION_MAP', - field_class=SocialOrganizationMapField, - allow_null=True, - default=None, - label=_('GitHub Enterprise Team OAuth2 Organization Map'), - help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT, - category=_('GitHub Enterprise Team OAuth2'), - category_slug='github-enterprise-team', - placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, - ) - - register( - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_TEAM_MAP', - field_class=SocialTeamMapField, - allow_null=True, - default=None, - label=_('GitHub Enterprise Team OAuth2 Team Map'), - help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT, - category=_('GitHub Enterprise Team OAuth2'), - category_slug='github-enterprise-team', - placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, - ) - - ############################################################################### - # MICROSOFT AZURE ACTIVE DIRECTORY SETTINGS - ############################################################################### - - register( - 'SOCIAL_AUTH_AZUREAD_OAUTH2_CALLBACK_URL', - field_class=fields.CharField, - read_only=True, - default=SocialAuthCallbackURL('azuread-oauth2'), - label=_('Azure AD OAuth2 Callback URL'), - help_text=_( - 'Provide this URL as the callback URL for your application as part of your registration process. Refer to the documentation for more detail. ' - ), - category=_('Azure AD OAuth2'), - category_slug='azuread-oauth2', - depends_on=['TOWER_URL_BASE'], - ) - - register( - 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('Azure AD OAuth2 Key'), - help_text=_('The OAuth2 key (Client ID) from your Azure AD application.'), - category=_('Azure AD OAuth2'), - category_slug='azuread-oauth2', - ) - - register( - 'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('Azure AD OAuth2 Secret'), - help_text=_('The OAuth2 secret (Client Secret) from your Azure AD application.'), - category=_('Azure AD OAuth2'), - category_slug='azuread-oauth2', - encrypted=True, - ) - - register( - 'SOCIAL_AUTH_AZUREAD_OAUTH2_ORGANIZATION_MAP', - field_class=SocialOrganizationMapField, - allow_null=True, - default=None, - label=_('Azure AD OAuth2 Organization Map'), - help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT, - category=_('Azure AD OAuth2'), - category_slug='azuread-oauth2', - placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, - ) - - register( - 'SOCIAL_AUTH_AZUREAD_OAUTH2_TEAM_MAP', - field_class=SocialTeamMapField, - allow_null=True, - default=None, - label=_('Azure AD OAuth2 Team Map'), - help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT, - category=_('Azure AD OAuth2'), - category_slug='azuread-oauth2', - placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, - ) - - ############################################################################### - # Generic OIDC AUTHENTICATION SETTINGS - ############################################################################### - - register( - 'SOCIAL_AUTH_OIDC_KEY', - field_class=fields.CharField, - allow_null=False, - default=None, - label=_('OIDC Key'), - help_text='The OIDC key (Client ID) from your IDP.', - category=_('Generic OIDC'), - category_slug='oidc', - ) - - register( - 'SOCIAL_AUTH_OIDC_SECRET', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('OIDC Secret'), - help_text=_('The OIDC secret (Client Secret) from your IDP.'), - category=_('Generic OIDC'), - category_slug='oidc', - encrypted=True, - ) - - register( - 'SOCIAL_AUTH_OIDC_OIDC_ENDPOINT', - field_class=fields.CharField, - allow_blank=True, - default='', - label=_('OIDC Provider URL'), - help_text=_('The URL for your OIDC provider including the path up to /.well-known/openid-configuration'), - category=_('Generic OIDC'), - category_slug='oidc', - ) - - register( - 'SOCIAL_AUTH_OIDC_VERIFY_SSL', - field_class=fields.BooleanField, - default=True, - label=_('Verify OIDC Provider Certificate'), - help_text=_('Verify the OIDC provider ssl certificate.'), - category=_('Generic OIDC'), - category_slug='oidc', - ) - - ############################################################################### - # SAML AUTHENTICATION SETTINGS - ############################################################################### - - def get_saml_metadata_url(): - return urlparse.urljoin(settings.TOWER_URL_BASE, reverse('sso:saml_metadata')) - - def get_saml_entity_id(): - return settings.TOWER_URL_BASE - - register( - 'SAML_AUTO_CREATE_OBJECTS', - field_class=fields.BooleanField, - default=True, - label=_('Automatically Create Organizations and Teams on SAML Login'), - help_text=_('When enabled (the default), mapped Organizations and Teams will be created automatically on successful SAML login.'), - category=_('SAML'), - category_slug='saml', - ) - - register( - 'SOCIAL_AUTH_SAML_CALLBACK_URL', - field_class=fields.CharField, - read_only=True, - default=SocialAuthCallbackURL('saml'), - label=_('SAML Assertion Consumer Service (ACS) URL'), - help_text=_( - 'Register the service as a service provider (SP) with each identity ' - 'provider (IdP) you have configured. Provide your SP Entity ID ' - 'and this ACS URL for your application.' - ), - category=_('SAML'), - category_slug='saml', - depends_on=['TOWER_URL_BASE'], - ) - - register( - 'SOCIAL_AUTH_SAML_METADATA_URL', - field_class=fields.CharField, - read_only=True, - default=get_saml_metadata_url, - label=_('SAML Service Provider Metadata URL'), - help_text=_('If your identity provider (IdP) allows uploading an XML metadata file, you can download one from this URL.'), - category=_('SAML'), - category_slug='saml', - ) - - register( - 'SOCIAL_AUTH_SAML_SP_ENTITY_ID', - field_class=fields.CharField, - allow_blank=True, - default=get_saml_entity_id, - label=_('SAML Service Provider Entity ID'), - help_text=_( - 'The application-defined unique identifier used as the ' - 'audience of the SAML service provider (SP) configuration. ' - 'This is usually the URL for the service.' - ), - category=_('SAML'), - category_slug='saml', - depends_on=['TOWER_URL_BASE'], - ) - - register( - 'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT', - field_class=fields.CharField, - allow_blank=True, - validators=[validate_certificate], - label=_('SAML Service Provider Public Certificate'), - help_text=_('Create a keypair to use as a service provider (SP) and include the certificate content here.'), - category=_('SAML'), - category_slug='saml', - ) - - register( - 'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY', - field_class=fields.CharField, - allow_blank=True, - validators=[validate_private_key], - label=_('SAML Service Provider Private Key'), - help_text=_('Create a keypair to use as a service provider (SP) and include the private key content here.'), - category=_('SAML'), - category_slug='saml', - encrypted=True, - ) - - register( - 'SOCIAL_AUTH_SAML_ORG_INFO', - field_class=SAMLOrgInfoField, - label=_('SAML Service Provider Organization Info'), - help_text=_('Provide the URL, display name, and the name of your app. Refer to the documentation for example syntax.'), - category=_('SAML'), - category_slug='saml', - placeholder=collections.OrderedDict( - [('en-US', collections.OrderedDict([('name', 'example'), ('displayname', 'Example'), ('url', 'http://www.example.com')]))] - ), - ) - - register( - 'SOCIAL_AUTH_SAML_TECHNICAL_CONTACT', - field_class=SAMLContactField, - allow_blank=True, - label=_('SAML Service Provider Technical Contact'), - help_text=_('Provide the name and email address of the technical contact for your service provider. Refer to the documentation for example syntax.'), - category=_('SAML'), - category_slug='saml', - placeholder=collections.OrderedDict([('givenName', 'Technical Contact'), ('emailAddress', 'techsup@example.com')]), - ) - - register( - 'SOCIAL_AUTH_SAML_SUPPORT_CONTACT', - field_class=SAMLContactField, - allow_blank=True, - label=_('SAML Service Provider Support Contact'), - help_text=_('Provide the name and email address of the support contact for your service provider. Refer to the documentation for example syntax.'), - category=_('SAML'), - category_slug='saml', - placeholder=collections.OrderedDict([('givenName', 'Support Contact'), ('emailAddress', 'support@example.com')]), - ) - - register( - 'SOCIAL_AUTH_SAML_ENABLED_IDPS', - field_class=SAMLEnabledIdPsField, - default={}, - label=_('SAML Enabled Identity Providers'), - help_text=_( - 'Configure the Entity ID, SSO URL and certificate for each identity' - ' provider (IdP) in use. Multiple SAML IdPs are supported. Some IdPs' - ' may provide user data using attribute names that differ from the' - ' default OIDs. Attribute names may be overridden for each IdP. Refer' - ' to the Ansible documentation for additional details and syntax.' - ), - category=_('SAML'), - category_slug='saml', - placeholder=collections.OrderedDict( - [ - ( - 'Okta', - collections.OrderedDict( - [ - ('entity_id', 'http://www.okta.com/HHniyLkaxk9e76wD0Thh'), - ('url', 'https://dev-123456.oktapreview.com/app/ansibletower/HHniyLkaxk9e76wD0Thh/sso/saml'), - ('x509cert', 'MIIDpDCCAoygAwIBAgIGAVVZ4rPzMA0GCSqGSIb3...'), - ('attr_user_permanent_id', 'username'), - ('attr_first_name', 'first_name'), - ('attr_last_name', 'last_name'), - ('attr_username', 'username'), - ('attr_email', 'email'), - ] - ), - ), - ( - 'OneLogin', - collections.OrderedDict( - [ - ('entity_id', 'https://app.onelogin.com/saml/metadata/123456'), - ('url', 'https://example.onelogin.com/trust/saml2/http-post/sso/123456'), - ('x509cert', 'MIIEJjCCAw6gAwIBAgIUfuSD54OPSBhndDHh3gZo...'), - ('attr_user_permanent_id', 'name_id'), - ('attr_first_name', 'User.FirstName'), - ('attr_last_name', 'User.LastName'), - ('attr_username', 'User.email'), - ('attr_email', 'User.email'), - ] - ), - ), - ] - ), - ) - - register( - 'SOCIAL_AUTH_SAML_SECURITY_CONFIG', - field_class=SAMLSecurityField, - allow_null=True, - default={'requestedAuthnContext': False}, - label=_('SAML Security Config'), - help_text=_( - 'A dict of key value pairs that are passed to the underlying python-saml security setting https://github.com/onelogin/python-saml#settings' - ), - category=_('SAML'), - category_slug='saml', - placeholder=collections.OrderedDict( - [ - ("nameIdEncrypted", False), - ("authnRequestsSigned", False), - ("logoutRequestSigned", False), - ("logoutResponseSigned", False), - ("signMetadata", False), - ("wantMessagesSigned", False), - ("wantAssertionsSigned", False), - ("wantAssertionsEncrypted", False), - ("wantNameId", True), - ("wantNameIdEncrypted", False), - ("wantAttributeStatement", True), - ("requestedAuthnContext", True), - ("requestedAuthnContextComparison", "exact"), - ("metadataValidUntil", "2015-06-26T20:00:00Z"), - ("metadataCacheDuration", "PT518400S"), - ("signatureAlgorithm", "http://www.w3.org/2000/09/xmldsig#rsa-sha1"), - ("digestAlgorithm", "http://www.w3.org/2000/09/xmldsig#sha1"), - ] - ), - ) - - register( - 'SOCIAL_AUTH_SAML_SP_EXTRA', - field_class=fields.DictField, - allow_null=True, - default=None, - label=_('SAML Service Provider extra configuration data'), - help_text=_('A dict of key value pairs to be passed to the underlying python-saml Service Provider configuration setting.'), - category=_('SAML'), - category_slug='saml', - placeholder=collections.OrderedDict(), - ) - - register( - 'SOCIAL_AUTH_SAML_EXTRA_DATA', - field_class=fields.ListTuplesField, - allow_null=True, - default=None, - label=_('SAML IDP to extra_data attribute mapping'), - help_text=_('A list of tuples that maps IDP attributes to extra_attributes.' ' Each attribute will be a list of values, even if only 1 value.'), - category=_('SAML'), - category_slug='saml', - placeholder=[('attribute_name', 'extra_data_name_for_attribute'), ('department', 'department'), ('manager_full_name', 'manager_full_name')], - ) - - register( - 'SOCIAL_AUTH_SAML_ORGANIZATION_MAP', - field_class=SocialOrganizationMapField, - allow_null=True, - default=None, - label=_('SAML Organization Map'), - help_text=SOCIAL_AUTH_ORGANIZATION_MAP_HELP_TEXT, - category=_('SAML'), - category_slug='saml', - placeholder=SOCIAL_AUTH_ORGANIZATION_MAP_PLACEHOLDER, - ) - - register( - 'SOCIAL_AUTH_SAML_TEAM_MAP', - field_class=SocialTeamMapField, - allow_null=True, - default=None, - label=_('SAML Team Map'), - help_text=SOCIAL_AUTH_TEAM_MAP_HELP_TEXT, - category=_('SAML'), - category_slug='saml', - placeholder=SOCIAL_AUTH_TEAM_MAP_PLACEHOLDER, - ) - - register( - 'SOCIAL_AUTH_SAML_ORGANIZATION_ATTR', - field_class=SAMLOrgAttrField, - allow_null=True, - default=None, - label=_('SAML Organization Attribute Mapping'), - help_text=_('Used to translate user organization membership.'), - category=_('SAML'), - category_slug='saml', - placeholder=collections.OrderedDict( - [ - ('saml_attr', 'organization'), - ('saml_admin_attr', 'organization_admin'), - ('saml_auditor_attr', 'organization_auditor'), - ('remove', True), - ('remove_admins', True), - ('remove_auditors', True), - ] - ), - ) - - register( - 'SOCIAL_AUTH_SAML_TEAM_ATTR', - field_class=SAMLTeamAttrField, - allow_null=True, - default=None, - label=_('SAML Team Attribute Mapping'), - help_text=_('Used to translate user team membership.'), - category=_('SAML'), - category_slug='saml', - placeholder=collections.OrderedDict( - [ - ('saml_attr', 'team'), - ('remove', True), - ( - 'team_org_map', - [ - collections.OrderedDict([('team', 'Marketing'), ('organization', 'Red Hat')]), - collections.OrderedDict([('team', 'Human Resources'), ('organization', 'Red Hat')]), - collections.OrderedDict([('team', 'Engineering'), ('organization', 'Red Hat')]), - collections.OrderedDict([('team', 'Engineering'), ('organization', 'Ansible')]), - collections.OrderedDict([('team', 'Quality Engineering'), ('organization', 'Ansible')]), - collections.OrderedDict([('team', 'Sales'), ('organization', 'Ansible')]), - ], - ), - ] - ), - ) - - register( - 'SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR', - field_class=SAMLUserFlagsAttrField, - allow_null=True, - default=None, - label=_('SAML User Flags Attribute Mapping'), - help_text=_('Used to map super users and system auditors from SAML.'), - category=_('SAML'), - category_slug='saml', - placeholder=[ - ('is_superuser_attr', 'saml_attr'), - ('is_superuser_value', ['value']), - ('is_superuser_role', ['saml_role']), - ('remove_superusers', True), - ('is_system_auditor_attr', 'saml_attr'), - ('is_system_auditor_value', ['value']), - ('is_system_auditor_role', ['saml_role']), - ('remove_system_auditors', True), - ], - ) - - 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', - ) - - def tacacs_validate(serializer, attrs): - if not serializer.instance or not hasattr(serializer.instance, 'TACACSPLUS_HOST') or not hasattr(serializer.instance, 'TACACSPLUS_SECRET'): - return attrs - errors = [] - host = serializer.instance.TACACSPLUS_HOST - if 'TACACSPLUS_HOST' in attrs: - host = attrs['TACACSPLUS_HOST'] - secret = serializer.instance.TACACSPLUS_SECRET - if 'TACACSPLUS_SECRET' in attrs: - secret = attrs['TACACSPLUS_SECRET'] - if host and not secret: - errors.append('TACACSPLUS_SECRET is required when TACACSPLUS_HOST is provided.') - if errors: - raise serializers.ValidationError(_('\n'.join(errors))) - return attrs - - register_validate('tacacsplus', tacacs_validate) diff --git a/awx/sso/fields.py b/awx/sso/fields.py deleted file mode 100644 index a81cb1cf34d4..000000000000 --- a/awx/sso/fields.py +++ /dev/null @@ -1,725 +0,0 @@ -import collections -import copy -import inspect -import json -import re - -import six - -# Python LDAP -import ldap -import awx - -# Django -from django.utils.translation import gettext_lazy as _ - -# Django Auth LDAP -import django_auth_ldap.config -from django_auth_ldap.config import LDAPSearch, LDAPSearchUnion - -from rest_framework.exceptions import ValidationError -from rest_framework.fields import empty, Field, SkipField - -# This must be imported so get_subclasses picks it up -from awx.sso.ldap_group_types import PosixUIDGroupType # noqa - -# AWX -from awx.conf import fields -from awx.main.validators import validate_certificate -from awx.sso.validators import ( # noqa - validate_ldap_dn, - validate_ldap_bind_dn, - validate_ldap_dn_with_user, - validate_ldap_filter, - validate_ldap_filter_with_user, - validate_tacacsplus_disallow_nonascii, -) - - -def get_subclasses(cls): - for subclass in cls.__subclasses__(): - for subsubclass in get_subclasses(subclass): - yield subsubclass - yield subclass - - -def find_class_in_modules(class_name): - """ - Used to find ldap subclasses by string - """ - module_search_space = [django_auth_ldap.config, awx.sso.ldap_group_types] - for m in module_search_space: - cls = getattr(m, class_name, None) - if cls: - return cls - return None - - -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( - [ - ('awx.sso.backends.LDAPBackend', ['AUTH_LDAP_SERVER_URI']), - ('awx.sso.backends.LDAPBackend1', ['AUTH_LDAP_1_SERVER_URI']), - ('awx.sso.backends.LDAPBackend2', ['AUTH_LDAP_2_SERVER_URI']), - ('awx.sso.backends.LDAPBackend3', ['AUTH_LDAP_3_SERVER_URI']), - ('awx.sso.backends.LDAPBackend4', ['AUTH_LDAP_4_SERVER_URI']), - ('awx.sso.backends.LDAPBackend5', ['AUTH_LDAP_5_SERVER_URI']), - ('awx.sso.backends.RADIUSBackend', ['RADIUS_SERVER']), - ('social_core.backends.google.GoogleOAuth2', ['SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET']), - ('social_core.backends.github.GithubOAuth2', ['SOCIAL_AUTH_GITHUB_KEY', 'SOCIAL_AUTH_GITHUB_SECRET']), - ('social_core.backends.open_id_connect.OpenIdConnectAuth', ['SOCIAL_AUTH_OIDC_KEY', 'SOCIAL_AUTH_OIDC_SECRET', 'SOCIAL_AUTH_OIDC_OIDC_ENDPOINT']), - ( - 'social_core.backends.github.GithubOrganizationOAuth2', - ['SOCIAL_AUTH_GITHUB_ORG_KEY', 'SOCIAL_AUTH_GITHUB_ORG_SECRET', 'SOCIAL_AUTH_GITHUB_ORG_NAME'], - ), - ('social_core.backends.github.GithubTeamOAuth2', ['SOCIAL_AUTH_GITHUB_TEAM_KEY', 'SOCIAL_AUTH_GITHUB_TEAM_SECRET', 'SOCIAL_AUTH_GITHUB_TEAM_ID']), - ( - 'social_core.backends.github_enterprise.GithubEnterpriseOAuth2', - [ - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_URL', - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_API_URL', - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY', - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_SECRET', - ], - ), - ( - 'social_core.backends.github_enterprise.GithubEnterpriseOrganizationOAuth2', - [ - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_URL', - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_API_URL', - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY', - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_SECRET', - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_NAME', - ], - ), - ( - 'social_core.backends.github_enterprise.GithubEnterpriseTeamOAuth2', - [ - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_URL', - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_API_URL', - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY', - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_SECRET', - 'SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_ID', - ], - ), - ('social_core.backends.azuread.AzureADOAuth2', ['SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', 'SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET']), - ( - 'awx.sso.backends.SAMLAuth', - [ - 'SOCIAL_AUTH_SAML_SP_ENTITY_ID', - 'SOCIAL_AUTH_SAML_SP_PUBLIC_CERT', - 'SOCIAL_AUTH_SAML_SP_PRIVATE_KEY', - 'SOCIAL_AUTH_SAML_ORG_INFO', - 'SOCIAL_AUTH_SAML_TECHNICAL_CONTACT', - 'SOCIAL_AUTH_SAML_SUPPORT_CONTACT', - 'SOCIAL_AUTH_SAML_ENABLED_IDPS', - ], - ), - ('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 LDAPServerURIField(fields.URLField): - def __init__(self, **kwargs): - kwargs.setdefault('schemes', ('ldap', 'ldaps')) - kwargs.setdefault('allow_plain_hostname', True) - super(LDAPServerURIField, self).__init__(**kwargs) - - def run_validators(self, value): - for url in filter(None, re.split(r'[, ]', (value or ''))): - super(LDAPServerURIField, self).run_validators(url) - return value - - -class LDAPConnectionOptionsField(fields.DictField): - default_error_messages = {'invalid_options': _('Invalid connection option(s): {invalid_options}.')} - - def to_representation(self, value): - value = value or {} - opt_names = ldap.OPT_NAMES_DICT - # Convert integer options to their named constants. - repr_value = {} - for opt, opt_value in value.items(): - if opt in opt_names: - repr_value[opt_names[opt]] = opt_value - return repr_value - - def to_internal_value(self, data): - data = super(LDAPConnectionOptionsField, self).to_internal_value(data) - valid_options = dict([(v, k) for k, v in ldap.OPT_NAMES_DICT.items()]) - invalid_options = set(data.keys()) - set(valid_options.keys()) - if invalid_options: - invalid_options = sorted(list(invalid_options)) - options_display = json.dumps(invalid_options).lstrip('[').rstrip(']') - self.fail('invalid_options', invalid_options=options_display) - # Convert named options to their integer constants. - internal_data = {} - for opt_name, opt_value in data.items(): - internal_data[valid_options[opt_name]] = opt_value - return internal_data - - -class LDAPDNField(fields.CharField): - def __init__(self, **kwargs): - super(LDAPDNField, self).__init__(**kwargs) - self.validators.append(validate_ldap_dn) - - def run_validation(self, data=empty): - value = super(LDAPDNField, self).run_validation(data) - # django-auth-ldap expects DN fields (like AUTH_LDAP_REQUIRE_GROUP) - # to be either a valid string or ``None`` (not an empty string) - return None if value == '' else value - - -class LDAPDNListField(fields.StringListField): - def __init__(self, **kwargs): - super(LDAPDNListField, self).__init__(**kwargs) - self.validators.append(lambda dn: list(map(validate_ldap_dn, dn))) - - def run_validation(self, data=empty): - if not isinstance(data, (list, tuple)): - data = [data] - return super(LDAPDNListField, self).run_validation(data) - - -class LDAPDNWithUserField(fields.CharField): - def __init__(self, **kwargs): - super(LDAPDNWithUserField, self).__init__(**kwargs) - self.validators.append(validate_ldap_dn_with_user) - - def run_validation(self, data=empty): - value = super(LDAPDNWithUserField, self).run_validation(data) - # django-auth-ldap expects DN fields (like AUTH_LDAP_USER_DN_TEMPLATE) - # to be either a valid string or ``None`` (not an empty string) - return None if value == '' else value - - -class LDAPFilterField(fields.CharField): - def __init__(self, **kwargs): - super(LDAPFilterField, self).__init__(**kwargs) - self.validators.append(validate_ldap_filter) - - -class LDAPFilterWithUserField(fields.CharField): - def __init__(self, **kwargs): - super(LDAPFilterWithUserField, self).__init__(**kwargs) - self.validators.append(validate_ldap_filter_with_user) - - -class LDAPScopeField(fields.ChoiceField): - def __init__(self, choices=None, **kwargs): - choices = choices or [('SCOPE_BASE', _('Base')), ('SCOPE_ONELEVEL', _('One Level')), ('SCOPE_SUBTREE', _('Subtree'))] - super(LDAPScopeField, self).__init__(choices, **kwargs) - - def to_representation(self, value): - for choice in self.choices.keys(): - if value == getattr(ldap, choice): - return choice - return super(LDAPScopeField, self).to_representation(value) - - def to_internal_value(self, data): - value = super(LDAPScopeField, self).to_internal_value(data) - return getattr(ldap, value) - - -class LDAPSearchField(fields.ListField): - default_error_messages = { - 'invalid_length': _('Expected a list of three items but got {length} instead.'), - 'type_error': _('Expected an instance of LDAPSearch but got {input_type} instead.'), - } - ldap_filter_field_class = LDAPFilterField - - def to_representation(self, value): - if not value: - return [] - if not isinstance(value, LDAPSearch): - self.fail('type_error', input_type=type(value)) - return [ - LDAPDNField().to_representation(value.base_dn), - LDAPScopeField().to_representation(value.scope), - self.ldap_filter_field_class().to_representation(value.filterstr), - ] - - def to_internal_value(self, data): - data = super(LDAPSearchField, self).to_internal_value(data) - if len(data) == 0: - return None - if len(data) != 3: - self.fail('invalid_length', length=len(data)) - return LDAPSearch( - LDAPDNField().run_validation(data[0]), LDAPScopeField().run_validation(data[1]), self.ldap_filter_field_class().run_validation(data[2]) - ) - - -class LDAPSearchWithUserField(LDAPSearchField): - ldap_filter_field_class = LDAPFilterWithUserField - - -class LDAPSearchUnionField(fields.ListField): - default_error_messages = {'type_error': _('Expected an instance of LDAPSearch or LDAPSearchUnion but got {input_type} instead.')} - ldap_search_field_class = LDAPSearchWithUserField - - def to_representation(self, value): - if not value: - return [] - elif isinstance(value, LDAPSearchUnion): - return [self.ldap_search_field_class().to_representation(s) for s in value.searches] - elif isinstance(value, LDAPSearch): - return self.ldap_search_field_class().to_representation(value) - else: - self.fail('type_error', input_type=type(value)) - - def to_internal_value(self, data): - data = super(LDAPSearchUnionField, self).to_internal_value(data) - if len(data) == 0: - return None - if len(data) == 3 and isinstance(data[0], str): - return self.ldap_search_field_class().run_validation(data) - else: - search_args = [] - for i in range(len(data)): - if not isinstance(data[i], list): - raise ValidationError('In order to ultilize LDAP Union, input element No. %d should be a search query array.' % (i + 1)) - try: - search_args.append(self.ldap_search_field_class().run_validation(data[i])) - except Exception as e: - if hasattr(e, 'detail') and isinstance(e.detail, list): - e.detail.insert(0, "Error parsing LDAP Union element No. %d:" % (i + 1)) - raise e - return LDAPSearchUnion(*search_args) - - -class LDAPUserAttrMapField(fields.DictField): - default_error_messages = {'invalid_attrs': _('Invalid user attribute(s): {invalid_attrs}.')} - valid_user_attrs = {'first_name', 'last_name', 'email'} - child = fields.CharField() - - def to_internal_value(self, data): - data = super(LDAPUserAttrMapField, self).to_internal_value(data) - invalid_attrs = set(data.keys()) - self.valid_user_attrs - if invalid_attrs: - invalid_attrs = sorted(list(invalid_attrs)) - attrs_display = json.dumps(invalid_attrs).lstrip('[').rstrip(']') - self.fail('invalid_attrs', invalid_attrs=attrs_display) - return data - - -class LDAPGroupTypeField(fields.ChoiceField, DependsOnMixin): - default_error_messages = { - 'type_error': _('Expected an instance of LDAPGroupType but got {input_type} instead.'), - 'missing_parameters': _('Missing required parameters in {dependency}.'), - 'invalid_parameters': _('Invalid group_type parameters. Expected instance of dict but got {parameters_type} instead.'), - } - - def __init__(self, choices=None, **kwargs): - group_types = get_subclasses(django_auth_ldap.config.LDAPGroupType) - choices = choices or [(x.__name__, x.__name__) for x in group_types] - super(LDAPGroupTypeField, self).__init__(choices, **kwargs) - - def to_representation(self, value): - if not value: - return 'MemberDNGroupType' - if not isinstance(value, django_auth_ldap.config.LDAPGroupType): - self.fail('type_error', input_type=type(value)) - return value.__class__.__name__ - - def to_internal_value(self, data): - data = super(LDAPGroupTypeField, self).to_internal_value(data) - if not data: - return None - - cls = find_class_in_modules(data) - if not cls: - return None - - # Per-group type parameter validation and handling here - - # Backwords compatability. Before AUTH_LDAP_GROUP_TYPE_PARAMS existed - # MemberDNGroupType was the only group type, of the underlying lib, that - # took a parameter. - params = self.get_depends_on() or {} - params_sanitized = dict() - - cls_args = inspect.getfullargspec(cls.__init__).args[1:] - - if cls_args: - if not isinstance(params, dict): - self.fail('invalid_parameters', parameters_type=type(params)) - - for attr in cls_args: - if attr in params: - params_sanitized[attr] = params[attr] - - try: - return cls(**params_sanitized) - except TypeError: - self.fail('missing_parameters', dependency=list(self.depends_on)[0]) - - -class LDAPGroupTypeParamsField(fields.DictField, DependsOnMixin): - default_error_messages = {'invalid_keys': _('Invalid key(s): {invalid_keys}.')} - - def to_internal_value(self, value): - value = super(LDAPGroupTypeParamsField, self).to_internal_value(value) - if not value: - return value - group_type_str = self.get_depends_on() - group_type_str = group_type_str or '' - - group_type_cls = find_class_in_modules(group_type_str) - if not group_type_cls: - # Fail safe - return {} - - invalid_keys = set(value.keys()) - set(inspect.getfullargspec(group_type_cls.__init__).args[1:]) - if invalid_keys: - invalid_keys = sorted(list(invalid_keys)) - keys_display = json.dumps(invalid_keys).lstrip('[').rstrip(']') - self.fail('invalid_keys', invalid_keys=keys_display) - return value - - -class LDAPUserFlagsField(fields.DictField): - default_error_messages = {'invalid_flag': _('Invalid user flag: "{invalid_flag}".')} - valid_user_flags = {'is_superuser', 'is_system_auditor'} - child = LDAPDNListField() - - def to_internal_value(self, data): - data = super(LDAPUserFlagsField, self).to_internal_value(data) - invalid_flags = set(data.keys()) - self.valid_user_flags - if invalid_flags: - self.fail('invalid_flag', invalid_flag=list(invalid_flags)[0]) - return data - - -class LDAPDNMapField(fields.StringListBooleanField): - child = LDAPDNField() - - -class LDAPSingleOrganizationMapField(HybridDictField): - admins = LDAPDNMapField(allow_null=True, required=False) - users = LDAPDNMapField(allow_null=True, required=False) - auditors = LDAPDNMapField(allow_null=True, required=False) - remove_admins = fields.BooleanField(required=False) - remove_users = fields.BooleanField(required=False) - remove_auditors = fields.BooleanField(required=False) - - child = _Forbidden() - - -class LDAPOrganizationMapField(fields.DictField): - child = LDAPSingleOrganizationMapField() - - -class LDAPSingleTeamMapField(HybridDictField): - organization = fields.CharField() - users = LDAPDNMapField(allow_null=True, required=False) - remove = fields.BooleanField(required=False) - - child = _Forbidden() - - -class LDAPTeamMapField(fields.DictField): - child = LDAPSingleTeamMapField() - - -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() - - -class SAMLOrgInfoValueField(HybridDictField): - name = fields.CharField() - displayname = fields.CharField() - url = fields.URLField() - - -class SAMLOrgInfoField(fields.DictField): - default_error_messages = {'invalid_lang_code': _('Invalid language code(s) for org info: {invalid_lang_codes}.')} - child = SAMLOrgInfoValueField() - - def to_internal_value(self, data): - data = super(SAMLOrgInfoField, self).to_internal_value(data) - invalid_keys = set() - for key in data.keys(): - if not re.match(r'^[a-z]{2}(?:-[a-z]{2})??$', key, re.I): - invalid_keys.add(key) - if invalid_keys: - invalid_keys = sorted(list(invalid_keys)) - keys_display = json.dumps(invalid_keys).lstrip('[').rstrip(']') - self.fail('invalid_lang_code', invalid_lang_codes=keys_display) - return data - - -class SAMLContactField(HybridDictField): - givenName = fields.CharField() - emailAddress = fields.EmailField() - - -class SAMLIdPField(HybridDictField): - entity_id = fields.CharField() - url = fields.URLField() - x509cert = fields.CharField(validators=[validate_certificate]) - attr_user_permanent_id = fields.CharField(required=False) - attr_first_name = fields.CharField(required=False) - attr_last_name = fields.CharField(required=False) - attr_username = fields.CharField(required=False) - attr_email = fields.CharField(required=False) - - -class SAMLEnabledIdPsField(fields.DictField): - child = SAMLIdPField() - - -class SAMLSecurityField(HybridDictField): - nameIdEncrypted = fields.BooleanField(required=False) - authnRequestsSigned = fields.BooleanField(required=False) - logoutRequestSigned = fields.BooleanField(required=False) - logoutResponseSigned = fields.BooleanField(required=False) - signMetadata = fields.BooleanField(required=False) - wantMessagesSigned = fields.BooleanField(required=False) - wantAssertionsSigned = fields.BooleanField(required=False) - wantAssertionsEncrypted = fields.BooleanField(required=False) - wantNameId = fields.BooleanField(required=False) - wantNameIdEncrypted = fields.BooleanField(required=False) - wantAttributeStatement = fields.BooleanField(required=False) - requestedAuthnContext = fields.StringListBooleanField(required=False) - requestedAuthnContextComparison = fields.CharField(required=False) - metadataValidUntil = fields.CharField(allow_null=True, required=False) - metadataCacheDuration = fields.CharField(allow_null=True, required=False) - signatureAlgorithm = fields.CharField(allow_null=True, required=False) - digestAlgorithm = fields.CharField(allow_null=True, required=False) - - -class SAMLOrgAttrField(HybridDictField): - remove = fields.BooleanField(required=False) - saml_attr = fields.CharField(required=False, allow_null=True) - remove_admins = fields.BooleanField(required=False) - saml_admin_attr = fields.CharField(required=False, allow_null=True) - remove_auditors = fields.BooleanField(required=False) - saml_auditor_attr = fields.CharField(required=False, allow_null=True) - - child = _Forbidden() - - -class SAMLTeamAttrTeamOrgMapField(HybridDictField): - team = fields.CharField(required=True, allow_null=False) - team_alias = fields.CharField(required=False, allow_null=True) - organization = fields.CharField(required=True, allow_null=False) - - child = _Forbidden() - - -class SAMLTeamAttrField(HybridDictField): - team_org_map = fields.ListField(required=False, child=SAMLTeamAttrTeamOrgMapField(), allow_null=True) - remove = fields.BooleanField(required=False) - saml_attr = fields.CharField(required=False, allow_null=True) - - child = _Forbidden() - - -class SAMLUserFlagsAttrField(HybridDictField): - is_superuser_attr = fields.CharField(required=False, allow_null=True) - is_superuser_value = fields.StringListField(required=False, allow_null=True) - is_superuser_role = fields.StringListField(required=False, allow_null=True) - remove_superusers = fields.BooleanField(required=False, allow_null=True) - is_system_auditor_attr = fields.CharField(required=False, allow_null=True) - is_system_auditor_value = fields.StringListField(required=False, allow_null=True) - is_system_auditor_role = fields.StringListField(required=False, allow_null=True) - remove_system_auditors = fields.BooleanField(required=False, allow_null=True) - - child = _Forbidden() diff --git a/awx/sso/ldap_group_types.py b/awx/sso/ldap_group_types.py deleted file mode 100644 index 2a5434c15440..000000000000 --- a/awx/sso/ldap_group_types.py +++ /dev/null @@ -1,73 +0,0 @@ -# Copyright (c) 2018 Ansible by Red Hat -# All Rights Reserved. - -# Python -import ldap - -# Django -from django.utils.encoding import force_str - -# 3rd party -from django_auth_ldap.config import LDAPGroupType - - -class PosixUIDGroupType(LDAPGroupType): - def __init__(self, name_attr='cn', ldap_group_user_attr='uid'): - self.ldap_group_user_attr = ldap_group_user_attr - super(PosixUIDGroupType, self).__init__(name_attr) - - """ - An LDAPGroupType subclass that handles non-standard DS. - """ - - def user_groups(self, ldap_user, group_search): - """ - Searches for any group that is either the user's primary or contains the - user as a member. - """ - groups = [] - - try: - user_uid = ldap_user.attrs[self.ldap_group_user_attr][0] - - if 'gidNumber' in ldap_user.attrs: - user_gid = ldap_user.attrs['gidNumber'][0] - filterstr = u'(|(gidNumber=%s)(memberUid=%s))' % ( - self.ldap.filter.escape_filter_chars(user_gid), - self.ldap.filter.escape_filter_chars(user_uid), - ) - else: - filterstr = u'(memberUid=%s)' % (self.ldap.filter.escape_filter_chars(user_uid),) - - search = group_search.search_with_additional_term_string(filterstr) - search.attrlist = [str(self.name_attr)] - groups = search.execute(ldap_user.connection) - except (KeyError, IndexError): - pass - - return groups - - def is_member(self, ldap_user, group_dn): - """ - Returns True if the group is the user's primary group or if the user is - listed in the group's memberUid attribute. - """ - is_member = False - try: - user_uid = ldap_user.attrs[self.ldap_group_user_attr][0] - - try: - is_member = ldap_user.connection.compare_s(force_str(group_dn), 'memberUid', force_str(user_uid)) - except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE): - is_member = False - - if not is_member: - try: - user_gid = ldap_user.attrs['gidNumber'][0] - is_member = ldap_user.connection.compare_s(force_str(group_dn), 'gidNumber', force_str(user_gid)) - except (ldap.UNDEFINED_TYPE, ldap.NO_SUCH_ATTRIBUTE): - is_member = False - except (KeyError, IndexError): - is_member = False - - return is_member 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 bacc25e3c00f..000000000000 --- a/awx/sso/migrations/0003_convert_saml_string_to_list.py +++ /dev/null @@ -1,58 +0,0 @@ -from django.db import migrations, connection -import json - -_values_to_change = ['is_superuser_value', 'is_superuser_role', 'is_system_auditor_value', 'is_system_auditor_role'] - - -def _get_setting(): - with connection.cursor() as cursor: - cursor.execute('SELECT value FROM conf_setting WHERE key= %s', ['SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR']) - row = cursor.fetchone() - if row == None: - return {} - existing_setting = row[0] - - try: - existing_json = json.loads(existing_setting) - except json.decoder.JSONDecodeError as e: - print("Failed to decode existing json setting:") - print(existing_setting) - raise e - - return existing_json - - -def _set_setting(value): - with connection.cursor() as cursor: - cursor.execute('UPDATE conf_setting SET value = %s WHERE key = %s', [json.dumps(value), 'SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR']) - - -def forwards(app, schema_editor): - # The Operation should use schema_editor to apply any changes it - # wants to make to the database. - existing_json = _get_setting() - for key in _values_to_change: - if existing_json.get(key, None) and isinstance(existing_json.get(key), str): - existing_json[key] = [existing_json.get(key)] - _set_setting(existing_json) - - -def backwards(app, schema_editor): - existing_json = _get_setting() - for key in _values_to_change: - if existing_json.get(key, None) and not isinstance(existing_json.get(key), str): - try: - existing_json[key] = existing_json.get(key).pop() - except IndexError: - existing_json[key] = "" - _set_setting(existing_json) - - -class Migration(migrations.Migration): - dependencies = [ - ('sso', '0002_expand_provider_options'), - ] - - operations = [ - migrations.RunPython(forwards, backwards), - ] 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 28eb23857f4b..000000000000 --- a/awx/sso/models.py +++ /dev/null @@ -1,19 +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 _ - - -class UserEnterpriseAuth(models.Model): - """Enterprise Auth association model""" - - PROVIDER_CHOICES = (('radius', _('RADIUS')), ('tacacs+', _('TACACS+')), ('saml', _('SAML'))) - - 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/saml_pipeline.py b/awx/sso/saml_pipeline.py deleted file mode 100644 index e0244a9ce344..000000000000 --- a/awx/sso/saml_pipeline.py +++ /dev/null @@ -1,291 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Python -import re -import logging - -# Django -from django.conf import settings - -from awx.main.models import Team -from awx.sso.common import create_org_and_teams, reconcile_users_org_team_mappings, get_orgs_by_ids - -logger = logging.getLogger('awx.sso.saml_pipeline') - - -def populate_user(backend, details, user=None, *args, **kwargs): - if not user: - return - - # Build the in-memory settings for how this user should be modeled - desired_org_state = {} - desired_team_state = {} - orgs_to_create = [] - teams_to_create = {} - _update_user_orgs_by_saml_attr(backend, desired_org_state, orgs_to_create, **kwargs) - _update_user_teams_by_saml_attr(desired_team_state, teams_to_create, **kwargs) - _update_user_orgs(backend, desired_org_state, orgs_to_create, user) - _update_user_teams(backend, desired_team_state, teams_to_create, user) - - # If the SAML adapter is allowed to create objects, lets do that first - create_org_and_teams(orgs_to_create, teams_to_create, 'SAML', settings.SAML_AUTO_CREATE_OBJECTS) - - # Finally reconcile the user - reconcile_users_org_team_mappings(user, desired_org_state, desired_team_state, 'SAML') - - -def _update_m2m_from_expression(user, expr, remove=True): - """ - Helper function to update m2m relationship based on user matching one or - more expressions. - """ - should_add = False - if expr is None or 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: - return True - elif remove: - return False - else: - return None - - -def _update_user_orgs(backend, desired_org_state, orgs_to_create, user=None): - """ - Update organization memberships for the given user based on mapping rules - defined in settings. - """ - 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 - if organization_name not in orgs_to_create: - orgs_to_create.append(organization_name) - - remove = bool(org_opts.get('remove', True)) - - if organization_name not in desired_org_state: - desired_org_state[organization_name] = {} - - for role_name, user_type in (('admin_role', 'admins'), ('member_role', 'users'), ('auditor_role', 'auditors')): - is_member_expression = org_opts.get(user_type, None) - remove_members = bool(org_opts.get('remove_{}'.format(user_type), remove)) - has_role = _update_m2m_from_expression(user, is_member_expression, remove_members) - desired_org_state[organization_name][role_name] = desired_org_state[organization_name].get(role_name, False) or has_role - - -def _update_user_teams(backend, desired_team_state, teams_to_create, user=None): - """ - Update team memberships for the given user based on mapping rules defined - in settings. - """ - - 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 - teams_to_create[team_name] = team_opts['organization'] - users_expr = team_opts.get('users', None) - remove = bool(team_opts.get('remove', True)) - add_or_remove = _update_m2m_from_expression(user, users_expr, remove) - if add_or_remove is not None: - org_name = team_opts['organization'] - if org_name not in desired_team_state: - desired_team_state[org_name] = {} - desired_team_state[org_name][team_name] = {'member_role': add_or_remove} - - -def _update_user_orgs_by_saml_attr(backend, desired_org_state, orgs_to_create, **kwargs): - org_map = settings.SOCIAL_AUTH_SAML_ORGANIZATION_ATTR - roles_and_flags = ( - ('member_role', 'remove', 'saml_attr'), - ('admin_role', 'remove_admins', 'saml_admin_attr'), - ('auditor_role', 'remove_auditors', 'saml_auditor_attr'), - ) - - # If the remove_flag was present we need to load all of the orgs and remove the user from the role - all_orgs = None - for role, remove_flag, _ in roles_and_flags: - remove = bool(org_map.get(remove_flag, True)) - if remove: - # Only get the all orgs once, and only if needed - if all_orgs is None: - all_orgs = get_orgs_by_ids() - for org_name in all_orgs.keys(): - if org_name not in desired_org_state: - desired_org_state[org_name] = {} - desired_org_state[org_name][role] = False - - # Now we can add the user as a member/admin/auditor for any orgs they have specified - for role, _, attr_flag in roles_and_flags: - if org_map.get(attr_flag) is None: - continue - saml_attr_values = kwargs.get('response', {}).get('attributes', {}).get(org_map.get(attr_flag), []) - for org_name in saml_attr_values: - try: - organization_alias = backend.setting('ORGANIZATION_MAP').get(org_name).get('organization_alias') - if organization_alias is not None: - organization_name = organization_alias - else: - organization_name = org_name - except Exception: - organization_name = org_name - if organization_name not in orgs_to_create: - orgs_to_create.append(organization_name) - if organization_name not in desired_org_state: - desired_org_state[organization_name] = {} - desired_org_state[organization_name][role] = True - - -def _update_user_teams_by_saml_attr(desired_team_state, teams_to_create, **kwargs): - # - # Map users into organizations based on SOCIAL_AUTH_SAML_TEAM_ATTR setting - # - team_map = settings.SOCIAL_AUTH_SAML_TEAM_ATTR - if team_map.get('saml_attr') is None: - return - - all_teams = None - # The role and flag is hard coded here but intended to be flexible in case we ever wanted to add another team type - for role, remove_flag in [('member_role', 'remove')]: - remove = bool(team_map.get(remove_flag, True)) - if remove: - # Only get the all orgs once, and only if needed - if all_teams is None: - all_teams = Team.objects.all().values_list('name', 'organization__name') - for team_name, organization_name in all_teams: - if organization_name not in desired_team_state: - desired_team_state[organization_name] = {} - desired_team_state[organization_name][team_name] = {role: False} - - saml_team_names = set(kwargs.get('response', {}).get('attributes', {}).get(team_map['saml_attr'], [])) - - for team_name_map in team_map.get('team_org_map', []): - team_name = team_name_map.get('team', None) - team_alias = team_name_map.get('team_alias', None) - organization_name = team_name_map.get('organization', None) - if team_name in saml_team_names: - if not organization_name: - # Settings field validation should prevent this. - logger.error("organization name invalid for team {}".format(team_name)) - continue - - if team_alias: - team_name = team_alias - - teams_to_create[team_name] = organization_name - user_is_member_of_team = True - else: - user_is_member_of_team = False - - if organization_name not in desired_team_state: - desired_team_state[organization_name] = {} - desired_team_state[organization_name][team_name] = {'member_role': user_is_member_of_team} - - -def _get_matches(list1, list2): - # Because we are just doing an intersection here we don't really care which list is in which parameter - - # A SAML provider could return either a string or a list of items so we need to coerce the SAML value into a list (if needed) - if not isinstance(list1, (list, tuple)): - list1 = [list1] - - # In addition, we used to allow strings in the SAML config instead of Lists. The migration should take case of that but just in case, we will convert our list too - if not isinstance(list2, (list, tuple)): - list2 = [list2] - - return set(list1).intersection(set(list2)) - - -def _check_flag(user, flag, attributes, user_flags_settings): - ''' - Helper function to set the is_superuser is_system_auditor flags for the SAML adapter - Returns the new flag and whether or not it changed the flag - ''' - new_flag = False - is_role_key = "is_%s_role" % (flag) - is_attr_key = "is_%s_attr" % (flag) - is_value_key = "is_%s_value" % (flag) - remove_setting = "remove_%ss" % (flag) - - # Check to see if we are respecting a role and, if so, does our user have that role? - required_roles = user_flags_settings.get(is_role_key, None) - if required_roles: - matching_roles = _get_matches(required_roles, attributes.get('Role', [])) - - # We do a 2 layer check here so that we don't spit out the else message if there is no role defined - if matching_roles: - logger.debug("User %s has %s role(s) %s" % (user.username, flag, ', '.join(matching_roles))) - new_flag = True - else: - logger.debug("User %s is missing the %s role(s) %s" % (user.username, flag, ', '.join(required_roles))) - - # Next, check to see if we are respecting an attribute; this will take priority over the role if its defined - attr_setting = user_flags_settings.get(is_attr_key, None) - if attr_setting and attributes.get(attr_setting, None): - # Do we have a required value for the attribute - required_value = user_flags_settings.get(is_value_key, None) - if required_value: - # If so, check and see if the value of the attr matches the required value - saml_user_attribute_value = attributes.get(attr_setting, None) - matching_values = _get_matches(required_value, saml_user_attribute_value) - - if matching_values: - logger.debug("Giving %s %s from attribute %s with matching values %s" % (user.username, flag, attr_setting, ', '.join(matching_values))) - new_flag = True - # if they don't match make sure that new_flag is false - else: - logger.debug( - "Refusing %s for %s because attr %s (%s) did not match value(s) %s" - % (flag, user.username, attr_setting, ", ".join(saml_user_attribute_value), ', '.join(required_value)) - ) - new_flag = False - # If there was no required value then we can just allow them in because of the attribute - else: - logger.debug("Giving %s %s from attribute %s" % (user.username, flag, attr_setting)) - new_flag = True - - # Get the users old flag - old_value = getattr(user, "is_%s" % (flag)) - - # If we are not removing the flag and they were a system admin and now we don't want them to be just return - remove_flag = user_flags_settings.get(remove_setting, True) - if not remove_flag and (old_value and not new_flag): - logger.debug("Remove flag %s preventing removal of %s for %s" % (remove_flag, flag, user.username)) - return old_value, False - - # If the user was flagged and we are going to make them not flagged make sure there is a message - if old_value and not new_flag: - logger.debug("Revoking %s from %s" % (flag, user.username)) - - return new_flag, old_value != new_flag - - -def update_user_flags(backend, details, user=None, *args, **kwargs): - user_flags_settings = settings.SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR - - attributes = kwargs.get('response', {}).get('attributes', {}) - logger.debug("User attributes for %s: %s" % (user.username, attributes)) - - user.is_superuser, superuser_changed = _check_flag(user, 'superuser', attributes, user_flags_settings) - user.is_system_auditor, auditor_changed = _check_flag(user, 'system_auditor', attributes, user_flags_settings) - - if superuser_changed or auditor_changed: - user.save() 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/conftest.py b/awx/sso/tests/conftest.py deleted file mode 100644 index f94b1c528f6d..000000000000 --- a/awx/sso/tests/conftest.py +++ /dev/null @@ -1,34 +0,0 @@ -import pytest - -from django.contrib.auth.models import User - -from awx.sso.backends import TACACSPlusBackend -from awx.sso.models import UserEnterpriseAuth - - -@pytest.fixture -def tacacsplus_backend(): - return TACACSPlusBackend() - - -@pytest.fixture -def existing_normal_user(): - try: - user = User.objects.get(username="alice") - except User.DoesNotExist: - user = User(username="alice", password="password") - user.save() - return user - - -@pytest.fixture -def existing_tacacsplus_user(): - try: - user = User.objects.get(username="foo") - except User.DoesNotExist: - user = User(username="foo") - user.set_unusable_password() - user.save() - enterprise_auth = UserEnterpriseAuth(user=user, provider='tacacs+') - enterprise_auth.save() - return user 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_backends.py b/awx/sso/tests/functional/test_backends.py deleted file mode 100644 index a0d2c31da3e2..000000000000 --- a/awx/sso/tests/functional/test_backends.py +++ /dev/null @@ -1,115 +0,0 @@ -import pytest -from awx.sso.backends import _update_m2m_from_groups - - -class MockLDAPGroups(object): - def is_member_of(self, group_dn): - return bool(group_dn) - - -class MockLDAPUser(object): - def _get_groups(self): - return MockLDAPGroups() - - -@pytest.mark.parametrize( - "setting, expected_result", - [ - (True, True), - ('something', True), - (False, False), - ('', False), - ], -) -def test_mock_objects(setting, expected_result): - ldap_user = MockLDAPUser() - assert ldap_user._get_groups().is_member_of(setting) == expected_result - - -@pytest.mark.parametrize( - "opts, remove, expected_result", - [ - # In these case we will pass no opts so we should get None as a return in all cases - ( - None, - False, - None, - ), - ( - None, - True, - None, - ), - # Next lets test with empty opts ([]) This should return False if remove is True and None otherwise - ( - [], - True, - False, - ), - ( - [], - False, - None, - ), - # Next opts is True, this will always return True - ( - True, - True, - True, - ), - ( - True, - False, - True, - ), - # If we get only a non-string as an option we hit a continue and will either return None or False depending on the remove flag - ( - [32], - False, - None, - ), - ( - [32], - True, - False, - ), - # Finally we need to test whether or not a user should be allowed in or not. - # We use a mock class for ldap_user that simply returns true/false based on the otps - ( - ['true'], - False, - True, - ), - # In this test we are going to pass a string to test the part of the code that coverts strings into array, this should give us True - ( - 'something', - True, - True, - ), - ( - [''], - False, - None, - ), - ( - False, - True, - False, - ), - # Empty strings are considered opts == None and will result in None or False based on the remove flag - ( - '', - True, - False, - ), - ( - '', - False, - None, - ), - ], -) -@pytest.mark.django_db -def test__update_m2m_from_groups(opts, remove, expected_result): - ldap_user = MockLDAPUser() - assert expected_result == _update_m2m_from_groups(ldap_user, opts, remove) diff --git a/awx/sso/tests/functional/test_common.py b/awx/sso/tests/functional/test_common.py deleted file mode 100644 index f2b3e5781d90..000000000000 --- a/awx/sso/tests/functional/test_common.py +++ /dev/null @@ -1,377 +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_ldap, enable_social, enable_enterprise, expected_results", - [ - (False, False, False, None), - (True, False, False, 'ldap'), - (True, True, False, 'social'), - (True, True, True, 'enterprise'), - (False, True, True, 'enterprise'), - (False, False, True, 'enterprise'), - (False, True, False, 'social'), - ], - ) - def test_get_external_account(self, enable_ldap, enable_social, 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_ldap: - user.profile.ldap_dn = 'test.dn' - if enable_social: - from social_django.models import UserSocialAuth - - social_auth, _ = UserSocialAuth.objects.get_or_create( - uid='667ec049-cdf3-45d0-a4dc-0465f7505954', - provider='oidc', - extra_data={}, - user_id=user.id, - ) - user.social_auth.set([social_auth]) - if enable_enterprise: - from awx.sso.models import UserEnterpriseAuth - - enterprise_auth = UserEnterpriseAuth(user=user, provider='tacacs+') - 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), - # Set the hard coded settings - ('AUTH_LDAP_SERVER_URI', True), - ('SOCIAL_AUTH_SAML_ENABLED_IDPS', True), - ('RADIUS_SERVER', True), - ('TACACSPLUS_HOST', True), - # Set some SOCIAL_SOCIAL_AUTH_OIDC_KEYAUTH_*_KEY settings - ('SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', True), - ('SOCIAL_AUTH_GITHUB_ENTERPRISE_KEY', True), - ('SOCIAL_AUTH_GITHUB_ENTERPRISE_ORG_KEY', True), - ('SOCIAL_AUTH_GITHUB_ENTERPRISE_TEAM_KEY', True), - ('SOCIAL_AUTH_GITHUB_KEY', True), - ('SOCIAL_AUTH_GITHUB_ORG_KEY', True), - ('SOCIAL_AUTH_GITHUB_TEAM_KEY', True), - ('SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', True), - ('SOCIAL_AUTH_OIDC_KEY', True), - # Try a hypothetical future one - ('SOCIAL_AUTH_GIBBERISH_KEY', True), - # Do a SAML one - ('SOCIAL_AUTH_SAML_SP_PRIVATE_KEY', False), - ], - ) - 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), - ('AUTH_LDAP_SERVER_URI', True, 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', True, True), - ('JUNK_SETTING', True, 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', True, True), - ('AUTH_LDAP_SERVER_URI', False, 'SOCIAL_AUTH_AZUREAD_OAUTH2_KEY', False, 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_get_or_set_enterprise_user.py b/awx/sso/tests/functional/test_get_or_set_enterprise_user.py deleted file mode 100644 index 3f37b41df319..000000000000 --- a/awx/sso/tests/functional/test_get_or_set_enterprise_user.py +++ /dev/null @@ -1,37 +0,0 @@ -# Python -import pytest -from unittest import mock - -# AWX -from awx.sso.backends import _get_or_set_enterprise_user - - -@pytest.mark.django_db -def test_fetch_user_if_exist(existing_tacacsplus_user): - with mock.patch('awx.sso.backends.logger') as mocked_logger: - new_user = _get_or_set_enterprise_user("foo", "password", "tacacs+") - mocked_logger.debug.assert_not_called() - mocked_logger.warning.assert_not_called() - assert new_user == existing_tacacsplus_user - - -@pytest.mark.django_db -def test_create_user_if_not_exist(existing_tacacsplus_user): - with mock.patch('awx.sso.backends.logger') as mocked_logger: - new_user = _get_or_set_enterprise_user("bar", "password", "tacacs+") - mocked_logger.debug.assert_called_once_with(u'Created enterprise user bar via TACACS+ backend.') - assert new_user != existing_tacacsplus_user - - -@pytest.mark.django_db -def test_created_user_has_no_usable_password(): - new_user = _get_or_set_enterprise_user("bar", "password", "tacacs+") - assert not new_user.has_usable_password() - - -@pytest.mark.django_db -def test_non_enterprise_user_does_not_get_pass(existing_normal_user): - with mock.patch('awx.sso.backends.logger') as mocked_logger: - new_user = _get_or_set_enterprise_user("alice", "password", "tacacs+") - mocked_logger.warning.assert_called_once_with(u'Enterprise user alice already defined in Tower.') - assert new_user is None diff --git a/awx/sso/tests/functional/test_ldap.py b/awx/sso/tests/functional/test_ldap.py deleted file mode 100644 index 881ab29e2b4f..000000000000 --- a/awx/sso/tests/functional/test_ldap.py +++ /dev/null @@ -1,19 +0,0 @@ -from django.test.utils import override_settings -import ldap -import pytest - -from awx.sso.backends import LDAPSettings - - -@override_settings(AUTH_LDAP_CONNECTION_OPTIONS={ldap.OPT_NETWORK_TIMEOUT: 60}) -@pytest.mark.django_db -def test_ldap_with_custom_timeout(): - settings = LDAPSettings() - assert settings.CONNECTION_OPTIONS == {ldap.OPT_NETWORK_TIMEOUT: 60} - - -@override_settings(AUTH_LDAP_CONNECTION_OPTIONS={ldap.OPT_REFERRALS: 0}) -@pytest.mark.django_db -def test_ldap_with_missing_timeout(): - settings = LDAPSettings() - assert settings.CONNECTION_OPTIONS == {ldap.OPT_REFERRALS: 0, ldap.OPT_NETWORK_TIMEOUT: 30} diff --git a/awx/sso/tests/functional/test_saml_pipeline.py b/awx/sso/tests/functional/test_saml_pipeline.py deleted file mode 100644 index 5204dc30ae46..000000000000 --- a/awx/sso/tests/functional/test_saml_pipeline.py +++ /dev/null @@ -1,711 +0,0 @@ -import pytest -import re - -from django.test.utils import override_settings -from awx.main.models import User, Organization, Team -from awx.sso.saml_pipeline import ( - _update_m2m_from_expression, - _update_user_orgs, - _update_user_teams, - _update_user_orgs_by_saml_attr, - _update_user_teams_by_saml_attr, - _check_flag, -) - -# from unittest import mock -# from django.utils.timezone import now -# , Credential, CredentialType - - -@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 TestSAMLPopulateUser: - # The main populate_user does not need to be tested since its just a conglomeration of other functions that we test - # This test is here in case someone alters the code in the future in a way that does require testing - def test_populate_user(self): - assert True - - -@pytest.mark.django_db -class TestSAMLSimpleMaps: - # This tests __update_user_orgs and __update_user_teams - @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() - - def test__update_user_orgs(self, 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('.*') - - desired_org_state = {} - orgs_to_create = [] - _update_user_orgs(backend, desired_org_state, orgs_to_create, u1) - _update_user_orgs(backend, desired_org_state, orgs_to_create, u2) - _update_user_orgs(backend, desired_org_state, orgs_to_create, u3) - - assert desired_org_state == {'Default': {'member_role': True, 'admin_role': True, 'auditor_role': False}} - assert orgs_to_create == ['Default'] - - # 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 - desired_org_state = {} - orgs_to_create = [] - _update_user_orgs(backend, desired_org_state, orgs_to_create, u1) - assert desired_org_state == {'Default': {'member_role': False, 'admin_role': False, 'auditor_role': False}} - assert orgs_to_create == ['Default'] - - # Test remove feature disabled - backend.setting('ORGANIZATION_MAP')['Default']['remove_admins'] = False - backend.setting('ORGANIZATION_MAP')['Default']['remove_users'] = False - desired_org_state = {} - orgs_to_create = [] - _update_user_orgs(backend, desired_org_state, orgs_to_create, u2) - - assert desired_org_state == {'Default': {'member_role': None, 'admin_role': None, 'auditor_role': False}} - assert orgs_to_create == ['Default'] - - # Test organization alias feature - backend.setting('ORGANIZATION_MAP')['Default']['organization_alias'] = 'Default_Alias' - orgs_to_create = [] - _update_user_orgs(backend, {}, orgs_to_create, u1) - assert orgs_to_create == ['Default_Alias'] - - 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('.*') - - desired_team_state = {} - teams_to_create = {} - _update_user_teams(backend, desired_team_state, teams_to_create, u1) - assert teams_to_create == {'Red': 'Default', 'Blue': 'Default'} - assert desired_team_state == {'Default': {'Blue': {'member_role': True}, 'Red': {'member_role': True}}} - - # 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'] = '' - - desired_team_state = {} - teams_to_create = {} - _update_user_teams(backend, desired_team_state, teams_to_create, u1) - assert teams_to_create == {'Red': 'Default', 'Blue': 'Default'} - assert desired_team_state == {'Default': {'Blue': {'member_role': False}, 'Red': {'member_role': False}}} - - # Test remove feature disabled - backend.setting('TEAM_MAP')['Blue']['remove'] = False - backend.setting('TEAM_MAP')['Red']['remove'] = False - - desired_team_state = {} - teams_to_create = {} - _update_user_teams(backend, desired_team_state, teams_to_create, u2) - assert teams_to_create == {'Red': 'Default', 'Blue': 'Default'} - # If we don't care about team memberships we just don't add them to the hash so this would be an empty hash - assert desired_team_state == {} - - -@pytest.mark.django_db -class TestSAMLM2M: - @pytest.mark.parametrize( - "expression, remove, expected_return", - [ - # No expression with no remove - (None, False, None), - ("", False, None), - # No expression with remove - (None, True, False), - # True expression with and without remove - (True, False, True), - (True, True, True), - # Single string matching the user name - ("user1", False, True), - # Single string matching the user email - ("user1@foo.com", False, True), - # Single string not matching username or email, no remove - ("user27", False, None), - # Single string not matching username or email, with remove - ("user27", True, False), - # Same tests with arrays instead of strings - (["user1"], False, True), - (["user1@foo.com"], False, True), - (["user27"], False, None), - (["user27"], True, False), - # Arrays with nothing matching - (["user27", "user28"], False, None), - (["user27", "user28"], True, False), - # Arrays with all matches - (["user1", "user1@foo.com"], False, True), - # Arrays with some match, some not - (["user1", "user28", "user27"], False, True), - # - # Note: For RE's, usually settings takes care of the compilation for us, so we have to do it manually for testing. - # we also need to remove any / or flags for the compile to happen - # - # Matching username regex non-array - (re.compile("^user.*"), False, True), - (re.compile("^user.*"), True, True), - # Matching email regex non-array - (re.compile(".*@foo.com$"), False, True), - (re.compile(".*@foo.com$"), True, True), - # Non-array not matching username or email - (re.compile("^$"), False, None), - (re.compile("^$"), True, False), - # All re tests just in array form - ([re.compile("^user.*")], False, True), - ([re.compile("^user.*")], True, True), - ([re.compile(".*@foo.com$")], False, True), - ([re.compile(".*@foo.com$")], True, True), - ([re.compile("^$")], False, None), - ([re.compile("^$")], True, False), - # An re with username matching but not email - ([re.compile("^user.*"), re.compile(".*@bar.com$")], False, True), - # An re with email matching but not username - ([re.compile("^user27$"), re.compile(".*@foo.com$")], False, True), - # An re array with no matching - ([re.compile("^user27$"), re.compile(".*@bar.com$")], False, None), - ([re.compile("^user27$"), re.compile(".*@bar.com$")], True, False), - # - # A mix of re and strings - # - # String matches, re does not - (["user1", re.compile(".*@bar.com$")], False, True), - # String does not match, re does - (["user27", re.compile(".*@foo.com$")], False, True), - # Nothing matches - (["user27", re.compile(".*@bar.com$")], False, None), - (["user27", re.compile(".*@bar.com$")], True, False), - ], - ) - def test__update_m2m_from_expression(self, expression, remove, expected_return): - user = User.objects.create(username='user1', last_name='foo', first_name='bar', email='user1@foo.com') - return_val = _update_m2m_from_expression(user, expression, remove) - assert return_val == expected_return - - -@pytest.mark.django_db -class TestSAMLAttrMaps: - @pytest.fixture - def backend(self): - class Backend: - s = { - 'ORGANIZATION_MAP': { - 'Default1': { - 'remove': True, - 'admins': 'foobar', - 'remove_admins': True, - 'users': 'foo', - 'remove_users': True, - 'organization_alias': 'o1_alias', - } - } - } - - def setting(self, key): - return self.s[key] - - return Backend() - - @pytest.mark.parametrize( - "setting, expected_state, expected_orgs_to_create, kwargs_member_of_mods", - [ - ( - # Default test, make sure that our roles get applied and removed as specified (with an alias) - { - 'saml_attr': 'memberOf', - 'saml_admin_attr': 'admins', - 'saml_auditor_attr': 'auditors', - 'remove': True, - 'remove_admins': True, - }, - { - 'Default2': {'member_role': True}, - 'Default3': {'admin_role': True}, - 'Default4': {'auditor_role': True}, - 'o1_alias': {'member_role': True}, - 'Rando1': {'admin_role': False, 'auditor_role': False, 'member_role': False}, - }, - [ - 'o1_alias', - 'Default2', - 'Default3', - 'Default4', - ], - None, - ), - ( - # Similar test, we are just going to override the values "coming from the IdP" to limit the teams - { - 'saml_attr': 'memberOf', - 'saml_admin_attr': 'admins', - 'saml_auditor_attr': 'auditors', - 'remove': True, - 'remove_admins': True, - }, - { - 'Default3': {'admin_role': True, 'member_role': True}, - 'Default4': {'auditor_role': True}, - 'Rando1': {'admin_role': False, 'auditor_role': False, 'member_role': False}, - }, - [ - 'Default3', - 'Default4', - ], - ['Default3'], - ), - ( - # Test to make sure the remove logic is working - { - 'saml_attr': 'memberOf', - 'saml_admin_attr': 'admins', - 'saml_auditor_attr': 'auditors', - 'remove': False, - 'remove_admins': False, - 'remove_auditors': False, - }, - { - 'Default2': {'member_role': True}, - 'Default3': {'admin_role': True}, - 'Default4': {'auditor_role': True}, - 'o1_alias': {'member_role': True}, - }, - [ - 'o1_alias', - 'Default2', - 'Default3', - 'Default4', - ], - ['Default1', 'Default2'], - ), - ], - ) - def test__update_user_orgs_by_saml_attr(self, backend, setting, expected_state, expected_orgs_to_create, kwargs_member_of_mods): - kwargs = { - 'username': u'cmeyers@redhat.com', - 'uid': 'idp:cmeyers@redhat.com', - 'request': {u'SAMLResponse': [], u'RelayState': [u'idp']}, - 'is_new': False, - 'response': { - 'session_index': '_0728f0e0-b766-0135-75fa-02842b07c044', - 'idp_name': u'idp', - 'attributes': { - 'memberOf': ['Default1', 'Default2'], - 'admins': ['Default3'], - 'auditors': ['Default4'], - 'groups': ['Blue', 'Red'], - 'User.email': ['cmeyers@redhat.com'], - 'User.LastName': ['Meyers'], - 'name_id': 'cmeyers@redhat.com', - 'User.FirstName': ['Chris'], - 'PersonImmutableID': [], - }, - }, - 'social': None, - 'strategy': None, - 'new_association': False, - } - if kwargs_member_of_mods: - kwargs['response']['attributes']['memberOf'] = kwargs_member_of_mods - - # Create a random organization in the database for testing - Organization.objects.create(name='Rando1') - - with override_settings(SOCIAL_AUTH_SAML_ORGANIZATION_ATTR=setting): - desired_org_state = {} - orgs_to_create = [] - _update_user_orgs_by_saml_attr(backend, desired_org_state, orgs_to_create, **kwargs) - assert desired_org_state == expected_state - assert orgs_to_create == expected_orgs_to_create - - @pytest.mark.parametrize( - "setting, expected_team_state, expected_teams_to_create, kwargs_group_override", - [ - ( - { - 'saml_attr': 'groups', - 'remove': False, - 'team_org_map': [ - {'team': 'Blue', 'organization': 'Default1'}, - {'team': 'Blue', 'organization': 'Default2'}, - {'team': 'Blue', 'organization': 'Default3'}, - {'team': 'Red', 'organization': 'Default1'}, - {'team': 'Green', 'organization': 'Default1'}, - {'team': 'Green', 'organization': 'Default3'}, - {'team': 'Yellow', 'team_alias': 'Yellow_Alias', 'organization': 'Default4', 'organization_alias': 'Default4_Alias'}, - ], - }, - { - 'Default1': { - 'Blue': {'member_role': True}, - 'Green': {'member_role': False}, - 'Red': {'member_role': True}, - }, - 'Default2': { - 'Blue': {'member_role': True}, - }, - 'Default3': { - 'Blue': {'member_role': True}, - 'Green': {'member_role': False}, - }, - 'Default4': { - 'Yellow': {'member_role': False}, - }, - }, - { - 'Blue': 'Default3', - 'Red': 'Default1', - }, - None, - ), - ( - { - 'saml_attr': 'groups', - 'remove': False, - 'team_org_map': [ - {'team': 'Blue', 'organization': 'Default1'}, - {'team': 'Blue', 'organization': 'Default2'}, - {'team': 'Blue', 'organization': 'Default3'}, - {'team': 'Red', 'organization': 'Default1'}, - {'team': 'Green', 'organization': 'Default1'}, - {'team': 'Green', 'organization': 'Default3'}, - {'team': 'Yellow', 'team_alias': 'Yellow_Alias', 'organization': 'Default4', 'organization_alias': 'Default4_Alias'}, - ], - }, - { - 'Default1': { - 'Blue': {'member_role': True}, - 'Green': {'member_role': True}, - 'Red': {'member_role': True}, - }, - 'Default2': { - 'Blue': {'member_role': True}, - }, - 'Default3': { - 'Blue': {'member_role': True}, - 'Green': {'member_role': True}, - }, - 'Default4': { - 'Yellow': {'member_role': False}, - }, - }, - { - 'Blue': 'Default3', - 'Red': 'Default1', - 'Green': 'Default3', - }, - ['Blue', 'Red', 'Green'], - ), - ( - { - 'saml_attr': 'groups', - 'remove': True, - 'team_org_map': [ - {'team': 'Blue', 'organization': 'Default1'}, - {'team': 'Blue', 'organization': 'Default2'}, - {'team': 'Blue', 'organization': 'Default3'}, - {'team': 'Red', 'organization': 'Default1'}, - {'team': 'Green', 'organization': 'Default1'}, - {'team': 'Green', 'organization': 'Default3'}, - {'team': 'Yellow', 'team_alias': 'Yellow_Alias', 'organization': 'Default4', 'organization_alias': 'Default4_Alias'}, - ], - }, - { - 'Default1': { - 'Blue': {'member_role': False}, - 'Green': {'member_role': True}, - 'Red': {'member_role': False}, - }, - 'Default2': { - 'Blue': {'member_role': False}, - }, - 'Default3': { - 'Blue': {'member_role': False}, - 'Green': {'member_role': True}, - }, - 'Default4': { - 'Yellow': {'member_role': False}, - }, - 'Rando1': { - 'Rando1': {'member_role': False}, - }, - }, - { - 'Green': 'Default3', - }, - ['Green'], - ), - ], - ) - def test__update_user_teams_by_saml_attr(self, setting, expected_team_state, expected_teams_to_create, kwargs_group_override): - kwargs = { - 'username': u'cmeyers@redhat.com', - 'uid': 'idp:cmeyers@redhat.com', - 'request': {u'SAMLResponse': [], u'RelayState': [u'idp']}, - 'is_new': False, - 'response': { - 'session_index': '_0728f0e0-b766-0135-75fa-02842b07c044', - 'idp_name': u'idp', - 'attributes': { - 'memberOf': ['Default1', 'Default2'], - 'admins': ['Default3'], - 'auditors': ['Default4'], - 'groups': ['Blue', 'Red'], - 'User.email': ['cmeyers@redhat.com'], - 'User.LastName': ['Meyers'], - 'name_id': 'cmeyers@redhat.com', - 'User.FirstName': ['Chris'], - 'PersonImmutableID': [], - }, - }, - 'social': None, - 'strategy': None, - 'new_association': False, - } - if kwargs_group_override: - kwargs['response']['attributes']['groups'] = kwargs_group_override - - o = Organization.objects.create(name='Rando1') - Team.objects.create(name='Rando1', organization_id=o.id) - - with override_settings(SOCIAL_AUTH_SAML_TEAM_ATTR=setting): - desired_team_state = {} - teams_to_create = {} - _update_user_teams_by_saml_attr(desired_team_state, teams_to_create, **kwargs) - assert desired_team_state == expected_team_state - assert teams_to_create == expected_teams_to_create - - -@pytest.mark.django_db -class TestSAMLUserFlags: - @pytest.mark.parametrize( - "user_flags_settings, expected, is_superuser", - [ - # In this case we will pass no user flags so new_flag should be false and changed will def be false - ( - {}, - (False, False), - False, - ), - # NOTE: The first handful of tests test role/value as string instead of lists. - # This was from the initial implementation of these fields but the code should be able to handle this - # There are a couple tests at the end of this which will validate arrays in these values. - # - # In this case we will give the user a group to make them an admin - ( - {'is_superuser_role': 'test-role-1'}, - (True, True), - False, - ), - # In this case we will give the user a flag that will make then an admin - ( - {'is_superuser_attr': 'is_superuser'}, - (True, True), - False, - ), - # In this case we will give the user a flag but the wrong value - ( - {'is_superuser_attr': 'is_superuser', 'is_superuser_value': 'junk'}, - (False, False), - False, - ), - # In this case we will give the user a flag and the right value - ( - {'is_superuser_attr': 'is_superuser', 'is_superuser_value': 'true'}, - (True, True), - False, - ), - # In this case we will give the user a proper role and an is_superuser_attr role that they don't have, this should make them an admin - ( - {'is_superuser_role': 'test-role-1', 'is_superuser_attr': 'gibberish', 'is_superuser_value': 'true'}, - (True, True), - False, - ), - # In this case we will give the user a proper role and an is_superuser_attr role that they have, this should make them an admin - ( - {'is_superuser_role': 'test-role-1', 'is_superuser_attr': 'test-role-1'}, - (True, True), - False, - ), - # In this case we will give the user a proper role and an is_superuser_attr role that they have but a bad value, this should make them an admin - ( - {'is_superuser_role': 'test-role-1', 'is_superuser_attr': 'is_superuser', 'is_superuser_value': 'junk'}, - (False, False), - False, - ), - # In this case we will give the user everything - ( - {'is_superuser_role': 'test-role-1', 'is_superuser_attr': 'is_superuser', 'is_superuser_value': 'true'}, - (True, True), - False, - ), - # In this test case we will validate that a single attribute (instead of a list) still works - ( - {'is_superuser_attr': 'name_id', 'is_superuser_value': 'test_id'}, - (True, True), - False, - ), - # This will be a negative test for a single attribute - ( - {'is_superuser_attr': 'name_id', 'is_superuser_value': 'junk'}, - (False, False), - False, - ), - # The user is already a superuser so we should remove them - ( - {'is_superuser_attr': 'name_id', 'is_superuser_value': 'junk', 'remove_superusers': True}, - (False, True), - True, - ), - # The user is already a superuser but we don't have a remove field - ( - {'is_superuser_attr': 'name_id', 'is_superuser_value': 'junk', 'remove_superusers': False}, - (True, False), - True, - ), - # Positive test for multiple values for is_superuser_value - ( - {'is_superuser_attr': 'is_superuser', 'is_superuser_value': ['junk', 'junk2', 'else', 'junk']}, - (True, True), - False, - ), - # Negative test for multiple values for is_superuser_value - ( - {'is_superuser_attr': 'is_superuser', 'is_superuser_value': ['junk', 'junk2', 'junk']}, - (False, True), - True, - ), - # Positive test for multiple values of is_superuser_role - ( - {'is_superuser_role': ['junk', 'junk2', 'something', 'junk']}, - (True, True), - False, - ), - # Negative test for multiple values of is_superuser_role - ( - {'is_superuser_role': ['junk', 'junk2', 'junk']}, - (False, True), - True, - ), - ], - ) - def test__check_flag(self, user_flags_settings, expected, is_superuser): - user = User() - user.username = 'John' - user.is_superuser = is_superuser - - attributes = { - 'email': ['noone@nowhere.com'], - 'last_name': ['Westcott'], - 'is_superuser': ['something', 'else', 'true'], - 'username': ['test_id'], - 'first_name': ['John'], - 'Role': ['test-role-1', 'something', 'different'], - 'name_id': 'test_id', - } - - assert expected == _check_flag(user, 'superuser', attributes, user_flags_settings) - - -@pytest.mark.django_db -def test__update_user_orgs_org_map_and_saml_attr(): - """ - This combines the action of two other tests where an org membership is defined both by - the ORGANIZATION_MAP and the SOCIAL_AUTH_SAML_ORGANIZATION_ATTR at the same time - """ - - # This data will make the user a member - class BackendClass: - s = { - 'ORGANIZATION_MAP': { - 'Default1': { - 'remove': True, - 'remove_admins': True, - 'users': 'foobar', - 'remove_users': True, - 'organization_alias': 'o1_alias', - } - } - } - - def setting(self, key): - return self.s[key] - - backend = BackendClass() - - setting = { - 'saml_attr': 'memberOf', - 'saml_admin_attr': 'admins', - 'saml_auditor_attr': 'auditors', - 'remove': True, - 'remove_admins': True, - } - - # This data from the server will make the user an admin of the organization - kwargs = { - 'username': 'foobar', - 'uid': 'idp:cmeyers@redhat.com', - 'request': {u'SAMLResponse': [], u'RelayState': [u'idp']}, - 'is_new': False, - 'response': { - 'session_index': '_0728f0e0-b766-0135-75fa-02842b07c044', - 'idp_name': u'idp', - 'attributes': { - 'admins': ['Default1'], - }, - }, - 'social': None, - 'strategy': None, - 'new_association': False, - } - - this_user = User.objects.create(username='foobar') - - with override_settings(SOCIAL_AUTH_SAML_ORGANIZATION_ATTR=setting): - desired_org_state = {} - orgs_to_create = [] - - # this should add user as an admin of the org - _update_user_orgs_by_saml_attr(backend, desired_org_state, orgs_to_create, **kwargs) - assert desired_org_state['o1_alias']['admin_role'] is True - - assert set(orgs_to_create) == set(['o1_alias']) - - # this should add user as a member of the org without reverting the admin status - _update_user_orgs(backend, desired_org_state, orgs_to_create, this_user) - assert desired_org_state['o1_alias']['member_role'] is True - assert desired_org_state['o1_alias']['admin_role'] is True - - assert set(orgs_to_create) == set(['o1_alias']) 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 35ab58d07fab..000000000000 --- a/awx/sso/tests/unit/test_fields.py +++ /dev/null @@ -1,235 +0,0 @@ -import pytest -from unittest import mock - -from rest_framework.exceptions import ValidationError - -from awx.sso.fields import SAMLOrgAttrField, SAMLTeamAttrField, SAMLUserFlagsAttrField, LDAPGroupTypeParamsField, LDAPServerURIField - - -class TestSAMLOrgAttrField: - @pytest.mark.parametrize( - "data, expected", - [ - ({}, {}), - ({'remove': True, 'saml_attr': 'foobar'}, {'remove': True, 'saml_attr': 'foobar'}), - ({'remove': True, 'saml_attr': 1234}, {'remove': True, 'saml_attr': '1234'}), - ({'remove': True, 'saml_attr': 3.14}, {'remove': True, 'saml_attr': '3.14'}), - ({'saml_attr': 'foobar'}, {'saml_attr': 'foobar'}), - ({'remove': True}, {'remove': True}), - ({'remove': True, 'saml_admin_attr': 'foobar'}, {'remove': True, 'saml_admin_attr': 'foobar'}), - ({'saml_admin_attr': 'foobar'}, {'saml_admin_attr': 'foobar'}), - ({'remove_admins': True, 'saml_admin_attr': 'foobar'}, {'remove_admins': True, 'saml_admin_attr': 'foobar'}), - ( - {'remove': True, 'saml_attr': 'foo', 'remove_admins': True, 'saml_admin_attr': 'bar'}, - {'remove': True, 'saml_attr': 'foo', 'remove_admins': True, 'saml_admin_attr': 'bar'}, - ), - ], - ) - def test_internal_value_valid(self, data, expected): - field = SAMLOrgAttrField() - res = field.to_internal_value(data) - assert res == expected - - @pytest.mark.parametrize( - "data, expected", - [ - ({'remove': 'blah', 'saml_attr': 'foobar'}, {'remove': ['Must be a valid boolean.']}), - ({'remove': True, 'saml_attr': False}, {'saml_attr': ['Not a valid string.']}), - ( - {'remove': True, 'saml_attr': False, 'foo': 'bar', 'gig': 'ity'}, - {'saml_attr': ['Not a valid string.'], 'foo': ['Invalid field.'], 'gig': ['Invalid field.']}, - ), - ({'remove_admins': True, 'saml_admin_attr': False}, {'saml_admin_attr': ['Not a valid string.']}), - ({'remove_admins': 'blah', 'saml_admin_attr': 'foobar'}, {'remove_admins': ['Must be a valid boolean.']}), - ], - ) - def test_internal_value_invalid(self, data, expected): - field = SAMLOrgAttrField() - with pytest.raises(ValidationError) as e: - field.to_internal_value(data) - assert e.value.detail == expected - - -class TestSAMLTeamAttrField: - @pytest.mark.parametrize( - "data", - [ - {}, - {'remove': True, 'saml_attr': 'foobar', 'team_org_map': []}, - {'remove': True, 'saml_attr': 'foobar', 'team_org_map': [{'team': 'Engineering', 'organization': 'Ansible'}]}, - { - 'remove': True, - 'saml_attr': 'foobar', - 'team_org_map': [ - {'team': 'Engineering', 'organization': 'Ansible'}, - {'team': 'Engineering', 'organization': 'Ansible2'}, - {'team': 'Engineering2', 'organization': 'Ansible'}, - ], - }, - { - 'remove': True, - 'saml_attr': 'foobar', - 'team_org_map': [ - {'team': 'Engineering', 'organization': 'Ansible'}, - {'team': 'Engineering', 'organization': 'Ansible2'}, - {'team': 'Engineering2', 'organization': 'Ansible'}, - ], - }, - { - 'remove': True, - 'saml_attr': 'foobar', - 'team_org_map': [ - {'team': 'Engineering', 'team_alias': 'Engineering Team', 'organization': 'Ansible'}, - {'team': 'Engineering', 'organization': 'Ansible2'}, - {'team': 'Engineering2', 'organization': 'Ansible'}, - ], - }, - ], - ) - def test_internal_value_valid(self, data): - field = SAMLTeamAttrField() - res = field.to_internal_value(data) - assert res == data - - @pytest.mark.parametrize( - "data, expected", - [ - ( - {'remove': True, 'saml_attr': 'foobar', 'team_org_map': [{'team': 'foobar', 'not_a_valid_key': 'blah', 'organization': 'Ansible'}]}, - {'team_org_map': {0: {'not_a_valid_key': ['Invalid field.']}}}, - ), - ( - {'remove': False, 'saml_attr': 'foobar', 'team_org_map': [{'organization': 'Ansible'}]}, - {'team_org_map': {0: {'team': ['This field is required.']}}}, - ), - ( - {'remove': False, 'saml_attr': 'foobar', 'team_org_map': [{}]}, - {'team_org_map': {0: {'organization': ['This field is required.'], 'team': ['This field is required.']}}}, - ), - ], - ) - def test_internal_value_invalid(self, data, expected): - field = SAMLTeamAttrField() - with pytest.raises(ValidationError) as e: - field.to_internal_value(data) - assert e.value.detail == expected - - -class TestSAMLUserFlagsAttrField: - @pytest.mark.parametrize( - "data", - [ - {}, - {'is_superuser_attr': 'something'}, - {'is_superuser_value': ['value']}, - {'is_superuser_role': ['my_peeps']}, - {'remove_superusers': False}, - {'is_system_auditor_attr': 'something_else'}, - {'is_system_auditor_value': ['value2']}, - {'is_system_auditor_role': ['other_peeps']}, - {'remove_system_auditors': False}, - ], - ) - def test_internal_value_valid(self, data): - field = SAMLUserFlagsAttrField() - res = field.to_internal_value(data) - assert res == data - - @pytest.mark.parametrize( - "data, expected", - [ - ( - { - 'junk': 'something', - 'is_superuser_value': 'value', - 'is_superuser_role': 'my_peeps', - 'is_system_auditor_attr': 'else', - 'is_system_auditor_value': 'value2', - 'is_system_auditor_role': 'other_peeps', - }, - { - 'junk': ['Invalid field.'], - 'is_superuser_role': ['Expected a list of items but got type "str".'], - 'is_superuser_value': ['Expected a list of items but got type "str".'], - 'is_system_auditor_role': ['Expected a list of items but got type "str".'], - 'is_system_auditor_value': ['Expected a list of items but got type "str".'], - }, - ), - ( - { - 'junk': 'something', - }, - { - 'junk': ['Invalid field.'], - }, - ), - ( - { - 'junk': 'something', - 'junk2': 'else', - }, - { - 'junk': ['Invalid field.'], - 'junk2': ['Invalid field.'], - }, - ), - # make sure we can't pass a string to the boolean fields - ( - { - 'remove_superusers': 'test', - 'remove_system_auditors': 'test', - }, - { - "remove_superusers": ["Must be a valid boolean."], - "remove_system_auditors": ["Must be a valid boolean."], - }, - ), - ], - ) - def test_internal_value_invalid(self, data, expected): - field = SAMLUserFlagsAttrField() - with pytest.raises(ValidationError) as e: - field.to_internal_value(data) - print(e.value.detail) - assert e.value.detail == expected - - -class TestLDAPGroupTypeParamsField: - @pytest.mark.parametrize( - "group_type, data, expected", - [ - ('LDAPGroupType', {'name_attr': 'user', 'bob': ['a', 'b'], 'scooter': 'hello'}, ['Invalid key(s): "bob", "scooter".']), - ('MemberDNGroupType', {'name_attr': 'user', 'member_attr': 'west', 'bob': ['a', 'b'], 'scooter': 'hello'}, ['Invalid key(s): "bob", "scooter".']), - ( - 'PosixUIDGroupType', - {'name_attr': 'user', 'member_attr': 'west', 'ldap_group_user_attr': 'legacyThing', 'bob': ['a', 'b'], 'scooter': 'hello'}, - ['Invalid key(s): "bob", "member_attr", "scooter".'], - ), - ], - ) - def test_internal_value_invalid(self, group_type, data, expected): - field = LDAPGroupTypeParamsField() - field.get_depends_on = mock.MagicMock(return_value=group_type) - - with pytest.raises(ValidationError) as e: - field.to_internal_value(data) - assert e.value.detail == expected - - -class TestLDAPServerURIField: - @pytest.mark.parametrize( - "ldap_uri, exception, expected", - [ - (r'ldap://servername.com:444', None, r'ldap://servername.com:444'), - (r'ldap://servername.so3:444', None, r'ldap://servername.so3:444'), - (r'ldaps://servername3.s300:344', None, r'ldaps://servername3.s300:344'), - (r'ldap://servername.-so3:444', ValidationError, None), - ], - ) - def test_run_validators_valid(self, ldap_uri, exception, expected): - field = LDAPServerURIField() - if exception is None: - assert field.run_validators(ldap_uri) == expected - else: - with pytest.raises(exception): - field.run_validators(ldap_uri) diff --git a/awx/sso/tests/unit/test_ldap.py b/awx/sso/tests/unit/test_ldap.py deleted file mode 100644 index aa54aaa49dbe..000000000000 --- a/awx/sso/tests/unit/test_ldap.py +++ /dev/null @@ -1,25 +0,0 @@ -import ldap - -from awx.sso.backends import LDAPSettings -from awx.sso.validators import validate_ldap_filter -from django.core.cache import cache - - -def test_ldap_default_settings(mocker): - from_db = mocker.Mock(**{'order_by.return_value': []}) - mocker.patch('awx.conf.models.Setting.objects.filter', return_value=from_db) - settings = LDAPSettings() - assert settings.ORGANIZATION_MAP == {} - assert settings.TEAM_MAP == {} - - -def test_ldap_default_network_timeout(mocker): - cache.clear() # clearing cache avoids picking up stray default for OPT_REFERRALS - from_db = mocker.Mock(**{'order_by.return_value': []}) - mocker.patch('awx.conf.models.Setting.objects.filter', return_value=from_db) - settings = LDAPSettings() - assert settings.CONNECTION_OPTIONS[ldap.OPT_NETWORK_TIMEOUT] == 30 - - -def test_ldap_filter_validator(): - validate_ldap_filter('(test-uid=%(user)s)', with_user=True) diff --git a/awx/sso/tests/unit/test_pipelines.py b/awx/sso/tests/unit/test_pipelines.py deleted file mode 100644 index 94a1111187b8..000000000000 --- a/awx/sso/tests/unit/test_pipelines.py +++ /dev/null @@ -1,12 +0,0 @@ -import pytest - - -@pytest.mark.parametrize( - "lib", - [ - ("saml_pipeline"), - ("social_pipeline"), - ], -) -def test_module_loads(lib): - module = __import__("awx.sso." + lib) # noqa diff --git a/awx/sso/tests/unit/test_tacacsplus.py b/awx/sso/tests/unit/test_tacacsplus.py deleted file mode 100644 index 49315a96432c..000000000000 --- a/awx/sso/tests/unit/test_tacacsplus.py +++ /dev/null @@ -1,116 +0,0 @@ -from unittest import mock -import pytest - - -def test_empty_host_fails_auth(tacacsplus_backend): - with mock.patch('awx.sso.backends.django_settings') as settings: - settings.TACACSPLUS_HOST = '' - ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass") - assert ret_user is None - - -def test_client_raises_exception(tacacsplus_backend): - client = mock.MagicMock() - client.authenticate.side_effect = Exception("foo") - with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('awx.sso.backends.logger') as logger, mock.patch( - 'tacacs_plus.TACACSClient', return_value=client - ): - settings.TACACSPLUS_HOST = 'localhost' - settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' - ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass") - assert ret_user is None - logger.exception.assert_called_once_with("TACACS+ Authentication Error: foo") - - -def test_client_return_invalid_fails_auth(tacacsplus_backend): - auth = mock.MagicMock() - auth.valid = False - client = mock.MagicMock() - client.authenticate.return_value = auth - with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client): - settings.TACACSPLUS_HOST = 'localhost' - settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' - ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass") - assert ret_user is None - - -def test_client_return_valid_passes_auth(tacacsplus_backend): - auth = mock.MagicMock() - auth.valid = True - client = mock.MagicMock() - client.authenticate.return_value = auth - user = mock.MagicMock() - user.has_usable_password = mock.MagicMock(return_value=False) - with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client), mock.patch( - 'awx.sso.backends._get_or_set_enterprise_user', return_value=user - ): - settings.TACACSPLUS_HOST = 'localhost' - settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' - ret_user = tacacsplus_backend.authenticate(None, u"user", u"pass") - assert ret_user == user - - -@pytest.mark.parametrize( - "client_ip_header,client_ip_header_value,expected_client_ip", - [('HTTP_X_FORWARDED_FOR', '12.34.56.78, 23.45.67.89', '12.34.56.78'), ('REMOTE_ADDR', '12.34.56.78', '12.34.56.78')], -) -def test_remote_addr_is_passed_to_client_if_available_and_setting_enabled(tacacsplus_backend, client_ip_header, client_ip_header_value, expected_client_ip): - auth = mock.MagicMock() - auth.valid = True - client = mock.MagicMock() - client.authenticate.return_value = auth - user = mock.MagicMock() - user.has_usable_password = mock.MagicMock(return_value=False) - request = mock.MagicMock() - request.META = { - client_ip_header: client_ip_header_value, - } - with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client), mock.patch( - 'awx.sso.backends._get_or_set_enterprise_user', return_value=user - ): - settings.TACACSPLUS_HOST = 'localhost' - settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' - settings.TACACSPLUS_REM_ADDR = True - tacacsplus_backend.authenticate(request, u"user", u"pass") - - client.authenticate.assert_called_once_with('user', 'pass', authen_type=1, rem_addr=expected_client_ip) - - -def test_remote_addr_is_completely_ignored_in_client_call_if_setting_is_disabled(tacacsplus_backend): - auth = mock.MagicMock() - auth.valid = True - client = mock.MagicMock() - client.authenticate.return_value = auth - user = mock.MagicMock() - user.has_usable_password = mock.MagicMock(return_value=False) - request = mock.MagicMock() - request.META = {} - with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client), mock.patch( - 'awx.sso.backends._get_or_set_enterprise_user', return_value=user - ): - settings.TACACSPLUS_HOST = 'localhost' - settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' - settings.TACACSPLUS_REM_ADDR = False - tacacsplus_backend.authenticate(request, u"user", u"pass") - - client.authenticate.assert_called_once_with('user', 'pass', authen_type=1) - - -def test_remote_addr_is_completely_ignored_in_client_call_if_unavailable_and_setting_enabled(tacacsplus_backend): - auth = mock.MagicMock() - auth.valid = True - client = mock.MagicMock() - client.authenticate.return_value = auth - user = mock.MagicMock() - user.has_usable_password = mock.MagicMock(return_value=False) - request = mock.MagicMock() - request.META = {} - with mock.patch('awx.sso.backends.django_settings') as settings, mock.patch('tacacs_plus.TACACSClient', return_value=client), mock.patch( - 'awx.sso.backends._get_or_set_enterprise_user', return_value=user - ): - settings.TACACSPLUS_HOST = 'localhost' - settings.TACACSPLUS_AUTH_PROTOCOL = 'ascii' - settings.TACACSPLUS_REM_ADDR = True - tacacsplus_backend.authenticate(request, u"user", u"pass") - - client.authenticate.assert_called_once_with('user', 'pass', authen_type=1) diff --git a/awx/sso/urls.py b/awx/sso/urls.py deleted file mode 100644 index 93da0996c970..000000000000 --- a/awx/sso/urls.py +++ /dev/null @@ -1,15 +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, saml_metadata - - -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'), - re_path(r'^metadata/saml/$', saml_metadata, name='saml_metadata'), -] diff --git a/awx/sso/validators.py b/awx/sso/validators.py deleted file mode 100644 index 478b86b36fc9..000000000000 --- a/awx/sso/validators.py +++ /dev/null @@ -1,74 +0,0 @@ -# Python -import re - -# Python-LDAP -import ldap - -# Django -from django.core.exceptions import ValidationError -from django.utils.translation import gettext_lazy as _ - -__all__ = [ - 'validate_ldap_dn', - 'validate_ldap_dn_with_user', - 'validate_ldap_bind_dn', - 'validate_ldap_filter', - 'validate_ldap_filter_with_user', - 'validate_tacacsplus_disallow_nonascii', -] - - -def validate_ldap_dn(value, with_user=False): - if with_user: - if '%(user)s' not in value: - raise ValidationError(_('DN must include "%%(user)s" placeholder for username: %s') % value) - dn_value = value.replace('%(user)s', 'USER') - else: - dn_value = value - try: - ldap.dn.str2dn(dn_value.encode('utf-8')) - except ldap.DECODING_ERROR: - raise ValidationError(_('Invalid DN: %s') % value) - - -def validate_ldap_dn_with_user(value): - validate_ldap_dn(value, with_user=True) - - -def validate_ldap_bind_dn(value): - if not re.match(r'^[A-Za-z][A-Za-z0-9._-]*?\\[A-Za-z0-9 ._-]+?$', value.strip()) and not re.match( - r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$', value.strip() - ): - validate_ldap_dn(value) - - -def validate_ldap_filter(value, with_user=False): - value = value.strip() - if not value: - return - if with_user: - if '%(user)s' not in value: - raise ValidationError(_('DN must include "%%(user)s" placeholder for username: %s') % value) - dn_value = value.replace('%(user)s', 'USER') - else: - dn_value = value - if re.match(r'^\([A-Za-z0-9-]+?=[^()]+?\)$', dn_value): - return - elif re.match(r'^\([&|!]\(.*?\)\)$', dn_value): - try: - map(validate_ldap_filter, ['(%s)' % x for x in dn_value[3:-2].split(')(')]) - return - except ValidationError: - pass - raise ValidationError(_('Invalid filter: %s') % value) - - -def validate_ldap_filter_with_user(value): - validate_ldap_filter(value, with_user=True) - - -def validate_tacacsplus_disallow_nonascii(value): - try: - value.encode('ascii') - except (UnicodeEncodeError, UnicodeDecodeError): - raise ValidationError(_('TACACS+ secret does not allow non-ascii characters')) diff --git a/awx/sso/views.py b/awx/sso/views.py deleted file mode 100644 index b6fd724df7dd..000000000000 --- a/awx/sso/views.py +++ /dev/null @@ -1,68 +0,0 @@ -# Copyright (c) 2015 Ansible, Inc. -# All Rights Reserved. - -# Python -import urllib.parse -import logging - -# Django -from django.urls import reverse -from django.http import HttpResponse -from django.views.generic import View -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() - - -class MetadataView(View): - def get(self, request, *args, **kwargs): - from social_django.utils import load_backend, load_strategy - - complete_url = reverse('social:complete', args=('saml',)) - try: - saml_backend = load_backend(load_strategy(request), 'saml', redirect_uri=complete_url) - metadata, errors = saml_backend.generate_metadata_xml() - except Exception as e: - logger.exception('unable to generate SAML metadata') - errors = e - if not errors: - return HttpResponse(content=metadata, content_type='text/xml') - else: - return HttpResponse(content=str(errors), content_type='text/plain') - - -saml_metadata = MetadataView.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/awx_collection/plugins/modules/settings.py b/awx_collection/plugins/modules/settings.py index c911f77fcc6b..7314257463b2 100644 --- a/awx_collection/plugins/modules/settings.py +++ b/awx_collection/plugins/modules/settings.py @@ -52,21 +52,6 @@ name: "AWX_ISOLATION_SHOW_PATHS" value: "'/var/lib/awx/projects/', '/tmp'" register: testing_settings - -- name: Set the LDAP Auth Bind Password - settings: - name: "AUTH_LDAP_BIND_PASSWORD" - value: "Password" - no_log: true - -- name: Set all the LDAP Auth Bind Params - settings: - settings: - AUTH_LDAP_BIND_PASSWORD: "password" - AUTH_LDAP_USER_ATTR_MAP: - email: "mail" - first_name: "givenName" - last_name: "surname" ''' from ..module_utils.controller_api import ControllerAPIModule diff --git a/awx_collection/test/awx/test_settings.py b/awx_collection/test/awx/test_settings.py index 69e823b3b9cf..2e0de9d2e04d 100644 --- a/awx_collection/test/awx/test_settings.py +++ b/awx_collection/test/awx/test_settings.py @@ -7,36 +7,6 @@ from awx.conf.models import Setting -@pytest.mark.django_db -def test_setting_flat_value(run_module, admin_user): - the_value = 'CN=service_account,OU=ServiceAccounts,DC=domain,DC=company,DC=org' - result = run_module('settings', dict(name='AUTH_LDAP_BIND_DN', value=the_value), admin_user) - assert not result.get('failed', False), result.get('msg', result) - assert result.get('changed'), result - - assert Setting.objects.get(key='AUTH_LDAP_BIND_DN').value == the_value - - -@pytest.mark.django_db -def test_setting_dict_value(run_module, admin_user): - the_value = {'email': 'mail', 'first_name': 'givenName', 'last_name': 'surname'} - result = run_module('settings', dict(name='AUTH_LDAP_USER_ATTR_MAP', value=the_value), admin_user) - assert not result.get('failed', False), result.get('msg', result) - assert result.get('changed'), result - - assert Setting.objects.get(key='AUTH_LDAP_USER_ATTR_MAP').value == the_value - - -@pytest.mark.django_db -def test_setting_nested_type(run_module, admin_user): - the_value = {'email': 'mail', 'first_name': 'givenName', 'last_name': 'surname'} - result = run_module('settings', dict(settings={'AUTH_LDAP_USER_ATTR_MAP': the_value}), admin_user) - assert not result.get('failed', False), result.get('msg', result) - assert result.get('changed'), result - - assert Setting.objects.get(key='AUTH_LDAP_USER_ATTR_MAP').value == the_value - - @pytest.mark.django_db def test_setting_bool_value(run_module, admin_user): for the_value in (True, False): diff --git a/awxkit/awxkit/api/pages/settings.py b/awxkit/awxkit/api/pages/settings.py index 12fa7e2910ca..b4ced68ade18 100644 --- a/awxkit/awxkit/api/pages/settings.py +++ b/awxkit/awxkit/api/pages/settings.py @@ -13,16 +13,8 @@ class Setting(base.Base): resources.settings_all, resources.settings_authentication, resources.settings_changed, - resources.settings_github, - resources.settings_github_org, - resources.settings_github_team, - resources.settings_google_oauth2, resources.settings_jobs, - resources.settings_ldap, - resources.settings_radius, - resources.settings_saml, resources.settings_system, - resources.settings_tacacsplus, resources.settings_ui, resources.settings_user, resources.settings_user_defaults, diff --git a/awxkit/awxkit/api/resources.py b/awxkit/awxkit/api/resources.py index 4ffb70a9b563..57bf845f865c 100644 --- a/awxkit/awxkit/api/resources.py +++ b/awxkit/awxkit/api/resources.py @@ -208,20 +208,11 @@ class Resources(object): _settings = 'settings/' _settings_all = 'settings/all/' _settings_authentication = 'settings/authentication/' - _settings_azuread_oauth2 = 'settings/azuread-oauth2/' _settings_changed = 'settings/changed/' - _settings_github = 'settings/github/' - _settings_github_org = 'settings/github-org/' - _settings_github_team = 'settings/github-team/' - _settings_google_oauth2 = 'settings/google-oauth2/' _settings_jobs = 'settings/jobs/' - _settings_ldap = 'settings/ldap/' _settings_logging = 'settings/logging/' _settings_named_url = 'settings/named-url/' - _settings_radius = 'settings/radius/' - _settings_saml = 'settings/saml/' _settings_system = 'settings/system/' - _settings_tacacsplus = 'settings/tacacsplus/' _settings_ui = 'settings/ui/' _settings_user = 'settings/user/' _settings_user_defaults = 'settings/user-defaults/' diff --git a/docs/auth/README.md b/docs/auth/README.md deleted file mode 100644 index eb23268747a3..000000000000 --- a/docs/auth/README.md +++ /dev/null @@ -1,27 +0,0 @@ -This folder describes third-party authentications supported by AWX. These authentications can be configured and enabled inside AWX. - -When a user wants to log into AWX, she can explicitly choose some of the supported authentications to log in instead of AWX's own authentication using username and password. Here is a list of such authentications: -* Google OAuth2 -* Github OAuth2 -* Github Organization OAuth2 -* Github Team OAuth2 -* Github Enterprise OAuth2 -* Github Enterprise Organization OAuth2 -* Github Enterprise Team OAuth2 -* Microsoft Azure Active Directory (AD) OAuth2 - -On the other hand, the other authentication methods use the same types of login info (username and password), but authenticate using external auth systems rather than AWX's own database. If some of these methods are enabled, AWX will try authenticating using the enabled methods *before AWX's own authentication method*. The order of precedence is: -* LDAP -* RADIUS -* TACACS+ -* SAML - -AWX will try authenticating against each enabled authentication method *in the specified order*, meaning if the same username and password is valid in multiple enabled auth methods (*e.g.*, both LDAP and TACACS+), AWX will only use the first positive match (in the above example, log a user in via LDAP and skip TACACS+). - -## Notes: -SAML users, RADIUS users and TACACS+ users are categorized as 'Enterprise' users. The following rules apply to Enterprise users: - - * Enterprise users can only be created via the first successful login attempt from remote authentication backend. - * Enterprise users cannot be created/authenticated if non-enterprise users with the same name has already been created in AWX. - * AWX passwords of Enterprise users should always be empty and cannot be set by any user if there are enterprise backends enabled. - * If enterprise backends are disabled, an Enterprise user can be converted to a normal AWX user by setting password field. But this operation is irreversible (the converted AWX user can no longer be treated as Enterprise user). diff --git a/docs/auth/ldap.md b/docs/auth/ldap.md deleted file mode 100644 index d212fa7ca853..000000000000 --- a/docs/auth/ldap.md +++ /dev/null @@ -1,68 +0,0 @@ -# LDAP -The Lightweight Directory Access Protocol (LDAP) is an open, vendor-neutral, industry-standard application protocol for accessing and maintaining distributed directory information services over an Internet Protocol (IP) network. Directory services play an important role in developing intranet and Internet applications by allowing the sharing of information about users, systems, networks, services, and applications throughout the network. - - -# Configure LDAP Authentication - -Please see the [AWX documentation](https://ansible.readthedocs.io/projects/awx/en/latest/administration/ldap_auth.html) for basic LDAP configuration. - -LDAP Authentication provides duplicate sets of configuration fields for authentication with up to six different LDAP servers. -The default set of configuration fields take the form `AUTH_LDAP_`. Configuration fields for additional LDAP servers are numbered `AUTH_LDAP__`. - - -## Test Environment Setup - -Please see `README.md` of this repository: https://github.com/ansible/deploy_ldap - - -# Basic Setup for FreeIPA - -LDAP Server URI (append if you have multiple LDAPs) -`ldaps://{{serverip1}}:636` - -LDAP BIND DN (How to create a bind account in [FreeIPA](https://www.freeipa.org/page/Creating_a_binddn_for_Foreman) -`uid=awx-bind,cn=sysaccounts,cn=etc,dc=example,dc=com` - -LDAP BIND PASSWORD -`{{yourbindaccountpassword}}` - -LDAP USER DN TEMPLATE -`uid=%(user)s,cn=users,cn=accounts,dc=example,dc=com` - -LDAP GROUP TYPE -`NestedMemberDNGroupType` - -LDAP GROUP SEARCH -``` -[ -"cn=groups,cn=accounts,dc=example,dc=com", -"SCOPE_SUBTREE", -"(objectClass=groupOfNames)" -] -``` - -LDAP USER ATTRIBUTE MAP -``` -{ -"first_name": "givenName", -"last_name": "sn", -"email": "mail" -} -``` - -LDAP USER FLAGS BY GROUP -``` -{ -"is_superuser": "cn={{superusergroupname}},cn=groups,cn=accounts,dc=example,dc=com" -} -``` - -LDAP ORGANIZATION MAP -``` -{ -"{{yourorganizationname}}": { -"admins": "cn={{admingroupname}},cn=groups,cn=accounts,dc=example,dc=com", -"remove_admins": false -} -} -``` diff --git a/docs/auth/oauth.md b/docs/auth/oauth.md deleted file mode 100644 index 6496adc81d15..000000000000 --- a/docs/auth/oauth.md +++ /dev/null @@ -1,403 +0,0 @@ -## Introduction -OAuth2 is the AWX means of token-based authentication. Users -will be able to manage OAuth2 tokens as well as applications, a server-side representation of API -clients used to generate tokens. With OAuth2, a user can authenticate by passing a token as part of -the HTTP authentication header. The token can be scoped to have more restrictive permissions on top of -the base RBAC permissions of the user. Refer to [RFC 6749](https://tools.ietf.org/html/rfc6749) for -more details of OAuth2 specification. - -## Basic Usage - -To get started using OAuth2 tokens for accessing the browsable API using OAuth2, this document will walk through the steps of acquiring a token and using it. - -1. Make an application with `authorization_grant_type` set to 'password'. HTTP POST the following to the `/api/v2/applications/` endpoint (supplying your own `organization-id`): -``` -{ - "name": "Admin Internal Application", - "description": "For use by secure services & clients. ", - "client_type": "confidential", - "redirect_uris": "", - "authorization_grant_type": "password", - "skip_authorization": false, - "organization": -} -``` -2. Make a token with a POST to the `/api/v2/tokens/` endpoint: -``` -{ - "description": "My Access Token", - "application": , - "scope": "write" -} -``` -This will return a `` that you can use to authenticate with for future requests (this will not be shown again) - -3. Use token to access a resource. We will use `curl` to demonstrate this: -``` -curl -H "Authorization: Bearer " -X GET https:///api/v2/users/ -``` -> The `-k` flag may be needed if you have not set up a CA yet and are using SSL. - -This token can be revoked by making a DELETE on the detail page for that token. All you need is that token's id. For example: -``` -curl -ku : -X DELETE https:///api/v2/tokens// -``` - -Similarly, using a token: -``` -curl -H "Authorization: Bearer " -X DELETE https:///api/v2/tokens// -k -``` - - -## More Information - -#### Managing OAuth2 Applications and Tokens - -Applications and tokens can be managed as a top-level resource at `/api/v2/applications` and -`/api/v2/tokens`. These resources can also be accessed respective to the user at -`/api/v2/users/N/`. Applications can be created by making a POST to either `api/v2/applications` -or `/api/v2/users/N/applications`. - -Each OAuth2 application represents a specific API client on the server side. For an API client to use the API via an application token, -it must first have an application and issue an access token. - -Individual applications will be accessible via their primary keys: -`/api/v2/applications//`. Here is a typical application: -``` -{ - "id": 1, - "type": "o_auth2_application", - "url": "/api/v2/applications/1/", - "related": { - "user": "/api/v2/users/1/", - "tokens": "/api/v2/applications/1/tokens/", - "activity_stream": "/api/v2/applications/1/activity_stream/" - }, - "summary_fields": { - "user": { - "id": 1, - "username": "root", - "first_name": "", - "last_name": "" - }, - "tokens": { - "count": 1, - "results": [ - { - "scope": "read", - "token": "*************", - "id": 2 - } - ] - } - }, - "created": "2018-02-20T23:06:43.215315Z", - "modified": "2018-02-20T23:06:43.215375Z", - "name": "Default application for root", - "user": 1, - "client_id": "BIyE720WAjr14nNxGXrBbsRsG0FkjgeL8cxNmIWP", - "client_secret": "OdO6TMNAYxUVv4HLitLOnRdAvtClEV8l99zlb8EJEZjlzVNaVVlWiKXicznLDeANwu5qRgeQRvD3AnuisQGCPXXRCx79W1ARQ5cSmc9mrU1JbqW7nX3IZYhLIFgsDH8u", - "client_type": "confidential", - "redirect_uris": "", - "authorization_grant_type": "password", - "skip_authorization": false -}, -``` -In the above example, `user` is the primary key of the user associated to this application and `name` is - a human-readable identifier for the application. The other fields, like `client_id` and -`redirect_uris`, are mainly used for OAuth2 authorization, which will be covered later in the 'Using -OAuth2 Token System' section. - -Fields `client_id` and `client_secret` are immutable identifiers of applications, and will be -generated during creation; Fields `user` and `authorization_grant_type`, on the other hand, are -*immutable on update*, meaning they are required fields on creation, but will become read-only after -that. - -**On RBAC side:** -- System admins will be able to see and manipulate all applications in the system; -- Organization admins will be able to see and manipulate all applications belonging to Organization - members; -- Other normal users will only be able to see, update and delete their own applications, but - cannot create any new applications. - -Tokens, on the other hand, are resources used to actually authenticate incoming requests and mask the -permissions of the underlying user. Tokens can be created by POSTing to `/api/v2/tokens/` -endpoint by providing `application` and `scope` fields to point to related application and specify -token scope; or POSTing to `/api/v2/applications//tokens/` by providing only `scope`, while -the parent application will be automatically linked. - -Individual tokens will be accessible via their primary keys at -`/api/v2/tokens//`. Here is a typical token: -``` -{ - "id": 4, - "type": "o_auth2_access_token", - "url": "/api/v2/tokens/4/", - "related": { - "user": "/api/v2/users/1/", - "application": "/api/v2/applications/1/", - "activity_stream": "/api/v2/tokens/4/activity_stream/" - }, - "summary_fields": { - "application": { - "id": 1, - "name": "Default application for root", - "client_id": "mcU5J5uGQcEQMgAZyr5JUnM3BqBJpgbgL9fLOVch" - }, - "user": { - "id": 1, - "username": "root", - "first_name": "", - "last_name": "" - } - }, - "created": "2018-02-23T14:39:32.618932Z", - "modified": "2018-02-23T14:39:32.643626Z", - "description": "App Token Test", - "user": 1, - "token": "*************", - "refresh_token": "*************", - "application": 1, - "expires": "2018-02-24T00:39:32.618279Z", - "scope": "read" -}, -``` -For an OAuth2 token, the only fully mutable fields are `scope` and `description`. The `application` -field is *immutable on update*, and all other fields are totally immutable, and will be auto-populated -during creation. -* `user` - this field corresponds to the user the token is created for -* `expires` will be generated according to the configuration setting `OAUTH2_PROVIDER` -* `token` and `refresh_token` will be auto-generated to be non-clashing random strings. - -Both application tokens and personal access tokens will be shown at the `/api/v2/tokens/` -endpoint. Personal access tokens can be identified by the `application` field being `null`. - -**On RBAC side:** -- A user will be able to create a token if they are able to see the related application; -- The System Administrator is able to see and manipulate every token in the system; -- Organization admins will be able to see and manipulate all tokens belonging to Organization - members; - System Auditors can see all tokens and applications -- Other normal users will only be able to see and manipulate their own tokens. -> Note: Users can only see the token or refresh-token _value_ at the time of creation ONLY. - -#### Using OAuth2 Token System for Personal Access Tokens (PAT) -The most common usage of OAuth2 is authenticating users. The `token` field of a token is used -as part of the HTTP authentication header, in the format `Authorization: Bearer `. This _Bearer_ -token can be obtained by doing a curl to the `/api/o/token/` endpoint. For example: -``` -curl -ku : -H "Content-Type: application/json" -X POST \ --d '{"description":"Tower CLI", "application":null, "scope":"write"}' \ -https:///api/v2/users/1/personal_tokens/ | python -m json.tool -``` -Here is an example of using that PAT to access an API endpoint using `curl`: -``` -curl -H "Authorization: Bearer kqHqxfpHGRRBXLNCOXxT5Zt3tpJogn" http:///api/v2/credentials/ -``` - -According to OAuth2 specification, users should be able to acquire, revoke and refresh an access -token. In AWX the equivalent, and easiest, way of doing that is creating a token, deleting -a token, and deleting a token quickly followed by creating a new one. - -The specification also provides standard ways of doing this. RFC 6749 elaborates -on those topics, but in summary, an OAuth2 token is officially acquired via authorization using -authorization information provided by applications (special application fields mentioned above). -There are dedicated endpoints for authorization and acquiring tokens. The `token` endpoint -is also responsible for token refresh, and token revoke can be done by the dedicated token revoke endpoint. - -In AWX, our OAuth2 system is built on top of -[Django Oauth Toolkit](https://django-oauth-toolkit.readthedocs.io/en/latest/), which provides full -support on standard authorization, token revoke and refresh. AWX implements them and puts related -endpoints under `/api/o/` endpoint. Detailed examples on the most typical usage of those endpoints -are available as description text of `/api/o/`. See below for information on Application Access Token usage. -> Note: The `/api/o/` endpoints can only be used for application tokens, and are not valid for personal access tokens. - - -#### Token Scope Mask Over RBAC System - -The scope of an OAuth2 token is a space-separated string composed of keywords like 'read' and 'write'. -These keywords are configurable and used to specify permission level of the authenticated API client. -For the initial OAuth2 implementation, we use the most simple scope configuration, where the only -valid scope keywords are 'read' and 'write'. - -Read and write scopes provide a mask layer over the RBAC permission system of AWX. In specific, a -'write' scope gives the authenticated user the full permissions the RBAC system provides, while 'read' -scope gives the authenticated user only read permissions the RBAC system provides. - -For example, if a user has admin permission to a job template, he/she can both see and modify, launch -and delete the job template if authenticated via session or basic auth. On the other hand, if the user -is authenticated using OAuth2 token, and the related token scope is 'read', the user can only see but -not manipulate or launch the job template, despite being an admin. If the token scope is -'write' or 'read write', she can take full advantage of the job template as its admin. Note that 'write' -implies 'read' as well. - - -## Application Functions - -This page lists OAuth2 utility endpoints used for authorization, token refresh and revoke. -Note endpoints other than `/api/o/authorize/` are not meant to be used in browsers and do not -support HTTP GET. The endpoints here strictly follow -[RFC specs for OAuth2](https://tools.ietf.org/html/rfc6749), so please use that for detailed -reference. Below are some examples to demonstrate the typical usage of these endpoints in -AWX context (note that the AWX net location defaults to `http://localhost:8013` in these examples). - - -#### Application Using `authorization code` Grant Type - -This application grant type is intended to be used when the application is executing on the server. To create -an application named `AuthCodeApp` with the `authorization-code` grant type, -make a POST to the `/api/v2/applications/` endpoint: -```text -{ - "name": "AuthCodeApp", - "user": 1, - "client_type": "confidential", - "redirect_uris": "http:///api/v2", - "authorization_grant_type": "authorization-code", - "skip_authorization": false -} -``` -You can test the authorization flow out with this new application by copying the `client_id` and URI link into the -homepage [here](http://django-oauth-toolkit.herokuapp.com/consumer/) and click submit. This is just a simple test -application `Django-oauth-toolkit` provides. - -From the client app, the user makes a GET to the Authorize endpoint with the `response_type`, -`client_id`, `redirect_uris`, and `scope`. AWX will respond with the authorization `code` and `state` -to the `redirect_uri` specified in the application. The client application will then make a POST to the -`api/o/token/` endpoint on AWX with the `code`, `client_id`, `client_secret`, `grant_type`, and `redirect_uri`. -AWX will respond with the `access_token`, `token_type`, `refresh_token`, and `expires_in`. For more -information on testing this flow, refer to [django-oauth-toolkit](http://django-oauth-toolkit.readthedocs.io/en/latest/tutorial/tutorial_01.html#test-your-authorization-server). - - -#### Application Using `password` Grant Type - -This is also called the `resource owner credentials grant`. This is for use by users who have -native access to the web app. This should be used when the client is the Resource owner. Suppose -we have an application `Default Application` with grant type `password`: -```text -{ - "id": 6, - "type": "application", - ... - "name": "Default Application", - "user": 1, - "client_id": "gwSPoasWSdNkMDtBN3Hu2WYQpPWCO9SwUEsKK22l", - "client_secret": "fI6ZpfocHYBGfm1tP92r0yIgCyfRdDQt0Tos9L8a4fNsJjQQMwp9569eIaUBsaVDgt2eiwOGe0bg5m5vCSstClZmtdy359RVx2rQK5YlIWyPlrolpt2LEpVeKXWaiybo", - "client_type": "confidential", - "redirect_uris": "", - "authorization_grant_type": "password", - "skip_authorization": false -} -``` - -Login is not required for `password` grant type, so we can simply use `curl` to acquire a personal access token -via `/api/o/token/`: -```bash -curl -X POST \ - -d "grant_type=password&username=&password=&scope=read" \ - -u "gwSPoasWSdNkMDtBN3Hu2WYQpPWCO9SwUEsKK22l:fI6ZpfocHYBGfm1tP92r0yIgCyfRdDQt0Tos9L8a4fNsJjQQMwp9569e -IaUBsaVDgt2eiwOGe0bg5m5vCSstClZmtdy359RVx2rQK5YlIWyPlrolpt2LEpVeKXWaiybo" \ - http:///api/o/token/ -i -``` -In the above POST request, parameters `username` and `password` are the username and password of the related -AWX user of the underlying application, and the authentication information is of format -`:`, where `client_id` and `client_secret` are the corresponding fields of -underlying application. - -Upon success, the access token, refresh token and other information are given in the response body in JSON -format: -```text -HTTP/1.1 200 OK -Server: nginx/1.12.2 -Date: Tue, 05 Dec 2017 16:48:09 GMT -Content-Type: application/json -Content-Length: 163 -Connection: keep-alive -Content-Language: en -Vary: Accept-Language, Cookie -Pragma: no-cache -Cache-Control: no-store -Strict-Transport-Security: max-age=15768000 - -{"access_token": "9epHOqHhnXUcgYK8QanOmUQPSgX92g", "token_type": "Bearer", "expires_in": 315360000000, "refresh_token": "jMRX6QvzOTf046KHee3TU5mT3nyXsz", "scope": "read"} -``` - - -## Token Functions - -#### Refresh an Existing Access Token - -Suppose we have an existing access token with refresh token provided: -```text -{ - "id": 35, - "type": "access_token", - ... - "user": 1, - "token": "omMFLk7UKpB36WN2Qma9H3gbwEBSOc", - "refresh_token": "AL0NK9TTpv0qp54dGbC4VUZtsZ9r8z", - "application": 6, - "expires": "2017-12-06T03:46:17.087022Z", - "scope": "read write" -} -``` -The `/api/o/token/` endpoint is used for refreshing the access token: -```bash -curl -X POST \ - -d "grant_type=refresh_token&refresh_token=AL0NK9TTpv0qp54dGbC4VUZtsZ9r8z" \ - -u "gwSPoasWSdNkMDtBN3Hu2WYQpPWCO9SwUEsKK22l:fI6ZpfocHYBGfm1tP92r0yIgCyfRdDQt0Tos9L8a4fNsJjQQMwp9569eIaUBsaVDgt2eiwOGe0bg5m5vCSstClZmtdy359RVx2rQK5YlIWyPlrolpt2LEpVeKXWaiybo" \ - http:///api/o/token/ -i -``` -In the above POST request, `refresh_token` is provided by `refresh_token` field of the access token -above. The authentication information is of format `:`, where `client_id` -and `client_secret` are the corresponding fields of underlying related application of the access token. - -Upon success, the new (refreshed) access token with the same scope information as the previous one is -given in the response body in JSON format: -```text -HTTP/1.1 200 OK -Server: nginx/1.12.2 -Date: Tue, 05 Dec 2017 17:54:06 GMT -Content-Type: application/json -Content-Length: 169 -Connection: keep-alive -Content-Language: en -Vary: Accept-Language, Cookie -Pragma: no-cache -Cache-Control: no-store -Strict-Transport-Security: max-age=15768000 - -{"access_token": "NDInWxGJI4iZgqpsreujjbvzCfJqgR", "token_type": "Bearer", "expires_in": 315360000000, "refresh_token": "DqOrmz8bx3srlHkZNKmDpqA86bnQkT", "scope": "read write"} -``` -Internally, the refresh operation deletes the existing token and a new token is created immediately -after, with information like scope and related application identical to the original one. We can -verify by checking the new token is present and the old token is deleted at the `/api/v2/tokens/` endpoint. - - -#### Revoke an Access Token - -##### Alternatively Revoke Using the /api/o/revoke-token/ Endpoint - -Revoking an access token by this method is the same as deleting the token resource object, but it allows you to delete a token by providing its token value, and the associated `client_id` (and `client_secret` if the application is `confidential`). For example: -```bash -curl -X POST -d "token=rQONsve372fQwuc2pn76k3IHDCYpi7" \ - -u "gwSPoasWSdNkMDtBN3Hu2WYQpPWCO9SwUEsKK22l:fI6ZpfocHYBGfm1tP92r0yIgCyfRdDQt0Tos9L8a4fNsJjQQMwp9569eIaUBsaVDgt2eiwOGe0bg5m5vCSstClZmtdy359RVx2rQK5YlIWyPlrolpt2LEpVeKXWaiybo" \ - http:///api/o/revoke_token/ -i -``` -`200 OK` means a successful delete. - -We can verify the effect by checking if the token is no longer present -at `/api/v2/tokens/`. - - -## Acceptance Criteria - -* All CRUD operations for OAuth2 applications and tokens should function as described. -* RBAC rules applied to OAuth2 applications and tokens should behave as described. -* A default application should be auto-created for each new user. -* Incoming requests using unexpired OAuth2 token correctly in authentication header should be able - to successfully authenticate themselves. -* Token scope mask over RBAC should work as described. -* AWX configuration setting `OAUTH2_PROVIDER` should be configurable and function as described. -* `/api/o/` endpoint should work as expected. In specific, all examples given in the description - help text should be working (a user following the steps should get expected result). diff --git a/docs/auth/saml.md b/docs/auth/saml.md deleted file mode 100644 index 8b9425027cbd..000000000000 --- a/docs/auth/saml.md +++ /dev/null @@ -1,146 +0,0 @@ -# SAML -Security Assertion Markup Language, or SAML, is an open standard for exchanging authentication and/or authorization data between an identity provider (*i.e.*, LDAP) and a service provider (*i.e.*, AWX). More concretely, AWX can be configured to talk with SAML in order to authenticate (create/login/logout) users of AWX. User Team and Organization membership can be embedded in the SAML response to AWX. - - -# Configure SAML Authentication -Please see the [AWX documentation](https://ansible.readthedocs.io/projects/awx/en/latest/administration/ent_auth.html#saml-settings) for basic SAML configuration. Note that AWX's SAML implementation relies on `python-social-auth` which uses `python-saml`. AWX exposes three fields which are directly passed to the lower libraries: -* `SOCIAL_AUTH_SAML_SP_EXTRA` is passed to the `python-saml` library configuration's `sp` setting. -* `SOCIAL_AUTH_SAML_SECURITY_CONFIG` is passed to the `python-saml` library configuration's `security` setting. -* `SOCIAL_AUTH_SAML_EXTRA_DATA` - -See https://python-social-auth.readthedocs.io/en/latest/backends/saml.html#advanced-settings for more information. - - -# Configure SAML for Team and Organization Membership -AWX can be configured to look for particular attributes that contain AWX Team and Organization membership to associate with users when they log in to AWX. The attribute names are defined in AWX settings. Specifically, the authentication settings tab and SAML sub category fields *SAML Team Attribute Mapping* and *SAML Organization Attribute Mapping*. The meaning and usefulness of these settings is best communicated through example. - -### Example SAML Organization Attribute Mapping - -Below is an example SAML attribute that embeds user organization membership in the attribute *member-of*. -``` - - - Engineering - IT - HR - Sales - - - IT - HR - - -``` -Below, the corresponding AWX configuration: -``` -{ - "saml_attr": "member-of", - "saml_admin_attr": "administrator-of", - "remove": true, - 'remove_admins': true -} -``` -**saml_attr:** The SAML attribute name where the organization array can be found. - -**remove:** Set this to `true` to remove a user from all organizations before adding the user to the list of Organizations. Set it to `false` to keep the user in whatever Organization(s) they are in while adding the user to the Organization(s) in the SAML attribute. - -**saml_admin_attr:** The SAML attribute name where the organization administrators' array can be found. - -**remove_admins:** Set this to `true` to remove a user from all organizations that they are administrators of before adding the user to the list of Organizations admins. Set it to `false` to keep the user in whatever Organization(s) they are in as admin while adding the user as an Organization administrator in the SAML attribute. - -### Example SAML Team Attribute Mapping -Below is another example of a SAML attribute that contains a Team membership in a list: -``` - - - member - staff - - -``` - -``` -{ - "saml_attr": "eduPersonAffiliation", - "remove": true, - "team_org_map": [ - { - "team": "member", - "organization": "Default1" - }, - { - "team": "staff", - "organization": "Default2" - } - ] -} -``` -**saml_attr:** The SAML attribute name where the team array can be found. - -**remove:** Set this to `true` to remove user from all Teams before adding the user to the list of Teams. Set this to `false` to keep the user in whatever Team(s) they are in while adding the user to the Team(s) in the SAML attribute. - -**team_org_map:** An array of dictionaries of the form `{ "team": "", "organization": "" }` which defines mapping from AWX Team -> AWX Organization. This is needed because the same named Team can exist in multiple Organizations in Tower. The organization to which a team listed in a SAML attribute belongs to would be ambiguous without this mapping. - - -### Example SAML User Flags Attribute Mapping -SAML User flags can be set for users with global "System Administrator" (superuser) or "System Auditor" (system_auditor) permissions. - -Below is an example of a SAML attribute that contains admin attributes: -``` - - - Auditor - - - IT-Superadmin - - -``` - -These properties can be defined either by a role or an attribute with the following configuration options: -``` -{ - "is_superuser_role": ["awx_admins"], - "is_superuser_attr": "is_superuser", - "is_superuser_value": ["IT-Superadmin"], - "is_system_auditor_role": ["awx_auditors"], - "is_system_auditor_attr": "is_system_auditor", - "is_system_auditor_value": ["Auditor"] -} -``` - -**is_superuser_role:** Specifies a SAML role which will grant a user the superuser flag. - -**is_superuser_attr:** Specifies a SAML attribute which will grant a user the superuser flag. - -**is_superuser_value:** Specifies a specific value required for ``is_superuser_attr`` that is required for the user to be a superuser. - -**is_system_auditor_role:** Specifies a SAML role which will grant a user the system auditor flag. - -**is_system_auditor_attr:** Specifies a SAML attribute which will grant a user the system auditor flag. - -**is_system_auditor_value:** Specifies a specific value required for ``is_system_auditor_attr`` that is required for the user to be a system auditor. - - -If `role` and `attr` are both specified for either superuser or system_auditor the settings for `attr` will take precedence over a `role`. The following table describes how the logic works. -| Has Role | Has Attr | Has Attr Value | Is Flagged | -|----------|----------|----------------|------------| -| No | No | N/A | No | -| Yes | No | N/A | Yes | -| No | Yes | Yes | Yes | -| No | Yes | No | No | -| No | Yes | Unset | Yes | -| Yes | Yes | Yes | Yes | -| Yes | Yes | No | No | -| Yes | Yes | Unset | Yes | - - -### SAML Debugging -You can enable logging messages for the SAML adapter the same way you can enable logging for LDAP. On the logging settings page change the log level to `Debug`. diff --git a/docs/auth/session.md b/docs/auth/session.md deleted file mode 100644 index 1f0f68914a56..000000000000 --- a/docs/auth/session.md +++ /dev/null @@ -1,91 +0,0 @@ -## Introduction - -Session-based authentication is the main authentication method, and auth tokens have been replaced by OAuth 2 tokens. - -Session authentication is a safer way of utilizing HTTP(S) cookies. Theoretically, the user can provide authentication information, like username and password, as part of the -`Cookie` header, but this method is vulnerable to cookie hijacks, where crackers can see and steal user -information from the cookie payload. - -Session authentication, on the other hand, sets a single `awx_sessionid` cookie. The `awx_sessionid` -is _a random string which will be mapped to user authentication information by the server_. Crackers who -hijack cookies will only get the `awx_sessionid` itself, which does not imply any critical user info, is valid only for -a limited time, and can be revoked at any time. - -> Note: The CSRF token will by default allow HTTP. To increase security, the `CSRF_COOKIE_SECURE` setting should -> be set to True. - -## Usage - -In session authentication, users log in using the `/api/login/` endpoint. A GET to `/api/login/` displays the -login page of API browser: - -![Example session log in page](../img/auth_session_1.png?raw=true) - -Users should enter correct username and password before clicking on the 'LOG IN' button, which fires a POST -to `/api/login/` to actually log the user in. The return code of a successful login is 302, meaning upon -successful login, the browser will be redirected; the redirected destination is determined by the `next` form -item described below. - -It should be noted that the POST body of `/api/login/` is _not_ in JSON, but in HTTP form format. Four items should -be provided in the form: - -- `username`: The username of the user trying to log in. -- `password`: The password of the user trying to log in. -- `next`: The path of the redirect destination, in API browser `"/api/"` is used. -- `csrfmiddlewaretoken`: The CSRF token, usually populated by using Django template `{% csrf_token %}`. - -The `awx_session_id` is provided as a return `Set-Cookie` header. Here is a typical one: - -``` -Set-Cookie: awx_sessionid=lwan8l5ynhrqvps280rg5upp7n3yp6ds; expires=Tue, 21-Nov-2017 16:33:13 GMT; httponly; Max-Age=1209600; Path=/ -``` - -In addition, when the `awx_sessionid` a header called `X-API-Session-Cookie-Name` this header will only be displayed once on a successful logging and denotes the name of the session cookie name. By default this is `awx_sessionid` but can be changed (see below). - -Any client should follow the standard rules of [cookie protocol](https://tools.ietf.org/html/rfc6265) to -parse that header to obtain information about the session, such as session cookie name (`awx_sessionid`), -session cookie value, expiration date, duration, etc. - -The name of the cookie is configurable by Tower Configuration setting `SESSION_COOKIE_NAME` under the category `authentication`. It is a string. The default session cookie name is `awx_sessionid`. - -The duration of the cookie is configurable by Tower Configuration setting `SESSION_COOKIE_AGE` under -category `authentication`. It is an integer denoting the number of seconds the session cookie should -live. The default session cookie age is two weeks. - -After a valid session is acquired, a client should provide the `awx_sessionid` as a cookie for subsequent requests -in order to be authenticated. For example: - -``` -Cookie: awx_sessionid=lwan8l5ynhrqvps280rg5upp7n3yp6ds; ... -``` - -User should use the `/api/logout/` endpoint to log out. In the API browser, a logged-in user can do that by -simply clicking logout button on the nav bar. Under the hood, the click issues a GET to `/api/logout/`. -Upon success, the server will invalidate the current session and the response header will indicate for the client -to delete the session cookie. The user should no longer try using this invalid session. - -The duration of a session is constant. However, a user can extend the expiration date of a valid session -by performing session acquire with the session provided. - -A Tower configuration setting, `SESSIONS_PER_USER` under category `authentication`, is used to set the -maximum number of valid sessions a user can have at the same time. For example, if `SESSIONS_PER_USER` -is set to three and the same user is logged in from five different places, the earliest two sessions created will be invalidated. Tower will try -broadcasting, via websocket, to all available clients. The websocket message body will contain a list of -invalidated sessions. If a client finds its session in that list, it should try logging out. - -Unlike tokens, sessions are meant to be short-lived and UI-only; therefore, whenever a user's password -is updated, all sessions she owned will be invalidated and deleted. - -## Acceptance Criteria - -- Users should be able to log in via the `/api/login/` endpoint by correctly providing all necessary fields. -- Logged-in users should be able to authenticate themselves by providing correct session auth info. -- Logged-in users should be able to log out via `/api/logout/`. -- The duration of a session cookie should be configurable by `SESSION_COOKIE_AGE`. -- The maximum number of concurrent login for one user should be configurable by `SESSIONS_PER_USER`, - and over-limit user sessions should be warned by websocket. -- When a user's password is changed, all her sessions should be invalidated and deleted. -- User should not be able to authenticate by HTTPS(S) request nor websocket connection using invalid - sessions. -- No existing behavior, like job runs, inventory updates or callback receiver, should be affected - by session auth. diff --git a/docs/auth/tacacsplus.md b/docs/auth/tacacsplus.md deleted file mode 100644 index f895ed4aeb35..000000000000 --- a/docs/auth/tacacsplus.md +++ /dev/null @@ -1,51 +0,0 @@ -# TACACS+ -[Terminal Access Controller Access-Control System Plus (TACACS+)](https://en.wikipedia.org/wiki/TACACS) is a protocol developed by Cisco to handle remote authentication and related services for networked access control through a centralized server. In specific, TACACS+ provides authentication, authorization and accounting (AAA) services. AWX currently utilizes its authentication service. - -TACACS+ is configured by settings configuration and is available under `/api/v2/settings/tacacsplus/`. Here is a typical configuration with every configurable field included: -``` -{ - "TACACSPLUS_HOST": "127.0.0.1", - "TACACSPLUS_PORT": 49, - "TACACSPLUS_SECRET": "secret", - "TACACSPLUS_SESSION_TIMEOUT": 5, - "TACACSPLUS_AUTH_PROTOCOL": "ascii", - "TACACSPLUS_REM_ADDR": "false" -} -``` -Each field is explained below: - -| Field Name | Field Value Type | Field Value Default | Description | -|------------------------------|---------------------|---------------------|--------------------------------------------------------------------| -| `TACACSPLUS_HOST` | String | '' (empty string) | Hostname of TACACS+ server. Empty string disables TACACS+ service. | -| `TACACSPLUS_PORT` | Integer | 49 | Port number of TACACS+ server. | -| `TACACSPLUS_SECRET` | String | '' (empty string) | Shared secret for authenticating to TACACS+ server. | -| `TACACSPLUS_SESSION_TIMEOUT` | Integer | 5 | TACACS+ session timeout value in seconds. | -| `TACACSPLUS_AUTH_PROTOCOL` | String with choices | 'ascii' | The authentication protocol used by TACACS+ client (choices are `ascii` and `pap`). | -| `TACACSPLUS_REM_ADDR` | Boolean | false | Enable the client address sending by TACACS+ client. | - -Under the hood, AWX uses [open-source TACACS+ python client](https://github.com/ansible/tacacs_plus) to communicate with the remote TACACS+ server. During authentication, AWX passes username and password to TACACS+ client, which packs up auth information and sends it to the TACACS+ server. Based on what the server returns, AWX will invalidate login attempt if authentication fails. If authentication passes, AWX will create a user if she does not exist in database, and log the user in. - -## Test Environment Setup - -The suggested TACACS+ server for testing is [shrubbery TACACS+ daemon](http://www.shrubbery.net/tac_plus/). It is supposed to run on a CentOS machine. A verified candidate is CentOS 6.3 AMI in AWS EC2 Community AMIs (search for `CentOS 6.3 x86_64 HVM - Minimal with cloud-init aws-cfn-bootstrap and ec2-api-tools`). Note that it is required to keep TCP port 49 open, since it's the default port used by the TACACS+ daemon. - -We provide [a playbook](https://github.com/jangsutsr/ansible-role-tacacs) to install a working TACACS+ server. Here is a typical test setup using the provided playbook: - -1. In AWS EC2, spawn the CentOS 6 machine. -2. In Tower, create a test project using the stand-alone playbook inventory. -3. In Tower, create a test inventory with the only host to be the spawned CentOS machine. -4. In Tower, create and run a job template using the created project and inventory with parameters setup as below: - -![Example tacacs+ setup jt parameters](../img/auth_tacacsplus_1.png?raw=true) - -The playbook creates a user named 'tower' with ascii password default to 'login' and modifiable by `extra_var` `ascii_password` and pap password default to 'papme' and modifiable by `extra_var` `pap_password`. In order to configure TACACS+ server to meet custom test needs, we need to modify server-side file `/etc/tac_plus.conf` and `sudo service tac_plus restart` to restart the daemon. Details on how to modify config file can be found [here](http://manpages.ubuntu.com/manpages/xenial/man5/tac_plus.conf.5.html). - - -## Acceptance Criteria - -* All specified in configuration fields should be shown and configurable as documented. -* A user defined by the TACACS+ server should be able to log into AWX. -* User not defined by TACACS+ server should not be able to log into AWX via TACACS+. -* A user existing in TACACS+ server but not in AWX should be created after the first successful log in. -* TACACS+ backend should stop an authentication attempt after configured timeout and should not block the authentication pipeline in any case. -* If exceptions occur on TACACS+ server side, the exception details should be logged in AWX, and AWX should not authenticate that user via TACACS+. diff --git a/docs/credentials/extract_credentials.md b/docs/credentials/extract_credentials.md index abbbdad88aea..e8394198a5a5 100644 --- a/docs/credentials/extract_credentials.md +++ b/docs/credentials/extract_credentials.md @@ -15,7 +15,7 @@ If necessary, credentials and encrypted settings can be extracted using the AWX $ awx-manage shell_plus >>> from awx.main.utils import decrypt_field >>> print(decrypt_field(Credential.objects.get(name="my private key"), "ssh_key_data")) # Example for a credential ->>> print(decrypt_field(Setting.objects.get(key='SOCIAL_AUTH_AZUREAD_OAUTH2_SECRET'), 'value')) # Example for a setting +>>> print(decrypt_field(Setting.objects.get(key='setting'), 'value')) # Example for a setting ``` If you are running a kubernetes based deployment, you can execute awx-manage like this: diff --git a/docs/docsite/rst/administration/awx-manage.rst b/docs/docsite/rst/administration/awx-manage.rst index a468a4633d65..3de9e6cb819a 100644 --- a/docs/docsite/rst/administration/awx-manage.rst +++ b/docs/docsite/rst/administration/awx-manage.rst @@ -146,7 +146,7 @@ Use this command to clear tokens which have already been revoked. Refer to `Djan ``expire_sessions`` ^^^^^^^^^^^^^^^^^^^^^^^^ -Use this command to terminate all sessions or all sessions for a specific user. Consider using this command when a user changes role in an organization, is removed from assorted groups in LDAP/AD, or the administrator wants to ensure the user can no longer execute jobs due to membership in these groups. +Use this command to terminate all sessions or all sessions for a specific user. Consider using this command when a user changes role in an organization, is removed from assorted groups in AD, or the administrator wants to ensure the user can no longer execute jobs due to membership in these groups. :: diff --git a/docs/docsite/rst/administration/configure_awx.rst b/docs/docsite/rst/administration/configure_awx.rst index 3087ab31c066..3c7c8fecf50e 100644 --- a/docs/docsite/rst/administration/configure_awx.rst +++ b/docs/docsite/rst/administration/configure_awx.rst @@ -10,24 +10,10 @@ AWX Configuration You can configure various AWX settings within the Settings screen in the following tabs: -.. image:: ../common/images/ug-settings-menu-screen.png - :alt: Screenshot of the AWX settings menu screen. - Each tab contains fields with a **Reset** button, allowing you to revert any value entered back to the default value. **Reset All** allows you to revert all the values to their factory default values. **Save** applies changes you make, but it does not exit the edit dialog. To return to the Settings screen, click **Settings** from the left navigation bar or use the breadcrumbs at the top of the current view. - -Authentication -================= -.. index:: - single: social authentication - single: authentication - pair: configuration; authentication - -.. include:: ./configure_awx_authentication.rst - - .. _configure_awx_jobs: Jobs @@ -66,7 +52,7 @@ The System tab allows you to define the base URL for the AWX host, configure ale 2. The right side of the Settings window is a set of configurable System settings. Select from the following options: - **Miscellaneous System settings**: enable activity streams, specify the default execution environment, define the base URL for the AWX host, enable AWX administration alerts, set user visibility, define analytics, specify usernames and passwords, and configure proxies. - - **Miscellaneous Authentication settings**: configure options associated with authentication methods (built-in or SSO), sessions (timeout, number of sessions logged in, tokens), and social authentication mapping. + - **Miscellaneous Authentication settings**: configure options associated with authentication methods and sessions (timeout, number of sessions logged in, tokens). - **Logging settings**: configure logging options based on the type you choose: .. image:: ../common/images/configure-awx-system-logging-types.png diff --git a/docs/docsite/rst/administration/configure_awx_authentication.rst b/docs/docsite/rst/administration/configure_awx_authentication.rst deleted file mode 100644 index f90576f918d5..000000000000 --- a/docs/docsite/rst/administration/configure_awx_authentication.rst +++ /dev/null @@ -1,16 +0,0 @@ -Through the AWX user interface, you can set up a simplified login through various authentication types: GitHub, Google, LDAP, and RADIUS. After you create and register your developer application with the appropriate service, you can set up authorizations for them. - -1. From the left navigation bar, click **Settings**. - -2. The left side of the Settings window is a set of configurable Authentication settings. Select from the following options: - -- :ref:`ag_auth_azure` -- :ref:`ag_auth_github` -- :ref:`ag_auth_google_oauth2` -- :ref:`LDAP settings ` -- :ref:`ag_auth_radius` -- :ref:`ag_auth_tacacs` - -Different authentication types require you to enter different information. Be sure to include all the information as required. - -3. Click **Save** to apply the settings or **Cancel** to abandon the changes. \ No newline at end of file diff --git a/docs/docsite/rst/administration/ent_auth.rst b/docs/docsite/rst/administration/ent_auth.rst deleted file mode 100644 index 73039f46779c..000000000000 --- a/docs/docsite/rst/administration/ent_auth.rst +++ /dev/null @@ -1,125 +0,0 @@ -.. _ag_ent_auth: - -Setting up Enterprise Authentication -================================================== - - -.. index:: - single: enterprise authentication - single: authentication - -This section describes setting up authentication for the following enterprise systems: - -.. contents:: - :local: - -.. note:: - - For LDAP authentication, see :ref:`ag_auth_ldap`. - -Azure, RADIUS, and TACACS+ users are categorized as 'Enterprise' users. The following rules apply to Enterprise users: - -- Enterprise users can only be created via the first successful login attempt from remote authentication backend. -- Enterprise users cannot be created/authenticated if non-enterprise users with the same name has already been created in AWX. -- AWX passwords of enterprise users should always be empty and cannot be set by any user if there are enterprise backend-enabled. -- If enterprise backends are disabled, an enterprise user can be converted to a normal AWX user by setting the password field. However, this operation is irreversible, as the converted AWX user can no longer be treated as enterprise user. - - -.. _ag_auth_azure: - -Azure AD settings -------------------- - -.. index:: - pair: authentication; Azure AD - -To set up enterprise authentication for Microsoft Azure Active Directory (AD), you will need to obtain an OAuth2 key and secret by registering your organization-owned application from Azure at https://docs.microsoft.com/en-us/azure/active-directory/develop/quickstart-register-app. Each key and secret must belong to a unique application and cannot be shared or reused between different authentication backends. In order to register the application, you must supply it with your webpage URL, which is the Callback URL shown in the Settings Authentication screen. - -1. Click **Settings** from the left navigation bar. - -2. On the left side of the Settings window, click **Azure AD settings** from the list of Authentication options. - -3. The **Azure AD OAuth2 Callback URL** field is already pre-populated and non-editable. - Once the application is registered, Azure displays the Application ID and Object ID. - -4. Click **Edit** and copy and paste Azure's Application ID to the **Azure AD OAuth2 Key** field. - - Following Azure AD's documentation for connecting your app to Microsoft Azure Active Directory, supply the key (shown at one time only) to the client for authentication. - -5. Copy and paste the actual secret key created for your Azure AD application to the **Azure AD OAuth2 Secret** field of the Settings - Authentication screen. - -6. For details on completing the mapping fields, see :ref:`ag_org_team_maps`. - -7. Click **Save** when done. - -8. To verify that the authentication was configured correctly, logout of AWX and the login screen will now display the Microsoft Azure logo to allow logging in with those credentials. - -.. image:: ../common/images/configure-awx-auth-azure-logo.png - :alt: AWX login screen displaying the Microsoft Azure logo for authentication. - - -For application registering basics in Azure AD, refer to the `Azure AD Identity Platform (v2)`_ overview. - -.. _`Azure AD Identity Platform (v2)`: https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-overview - - -LDAP Authentication ---------------------- - -Refer to the :ref:`ag_auth_ldap` section. - - -.. _ag_auth_radius: - -RADIUS settings ------------------- - -.. index:: - pair: authentication; RADIUS Authentication Settings - - -AWX can be configured to centrally use RADIUS as a source for authentication information. - -1. Click **Settings** from the left navigation bar. - -2. On the left side of the Settings window, click **RADIUS settings** from the list of Authentication options. - -3. Click **Edit** and enter the Host or IP of the Radius server in the **Radius Server** field. If this field is left blank, Radius authentication is disabled. - -4. Enter the port and secret information in the next two fields. - -5. Click **Save** when done. - - -.. _ag_auth_tacacs: - -TACACS+ settings ------------------ - -.. index:: - pair: authentication; TACACS+ Authentication Settings - - -Terminal Access Controller Access-Control System Plus (TACACS+) is a protocol that handles remote authentication and related services for networked access control through a centralized server. In particular, TACACS+ provides authentication, authorization and accounting (AAA) services, in which you can configure AWX to use as a source for authentication. - -.. note:: - - This feature is deprecated and will be removed in a future release. - -1. Click **Settings** from the left navigation bar. - -2. On the left side of the Settings window, click **TACACs+ settings** from the list of Authentication options. - -3. Click **Edit** and enter information in the following fields: - -- **TACACS+ Server**: Provide the hostname or IP address of the TACACS+ server with which to authenticate. If this field is left blank, TACACS+ authentication is disabled. -- **TACACS+ Port**: TACACS+ uses port 49 by default, which is already pre-populated. -- **TACACS+ Secret**: Secret key for TACACS+ authentication server. -- **TACACS+ Auth Session Timeout**: Session timeout value in seconds. The default is 5 seconds. -- **TACACS+ Authentication Protocol**: The protocol used by TACACS+ client. Options are **ascii** or **pap**. - -.. image:: ../common/images/configure-awx-auth-tacacs.png - :alt: TACACS+ configuration details in AWX settings. - -4. Click **Save** when done. - diff --git a/docs/docsite/rst/administration/index.rst b/docs/docsite/rst/administration/index.rst index 5ba867d856f0..7a4ac043da7b 100644 --- a/docs/docsite/rst/administration/index.rst +++ b/docs/docsite/rst/administration/index.rst @@ -39,11 +39,7 @@ Need help or want to discuss AWX including the documentation? See the :ref:`Comm configure_awx isolation_variables oauth2_token_auth - social_auth - ent_auth - ldap_auth authentication_timeout - kerberos_auth session_limits custom_rebranding troubleshooting diff --git a/docs/docsite/rst/administration/kerberos_auth.rst b/docs/docsite/rst/administration/kerberos_auth.rst deleted file mode 100644 index 96f5855b7418..000000000000 --- a/docs/docsite/rst/administration/kerberos_auth.rst +++ /dev/null @@ -1,117 +0,0 @@ -User Authentication with Kerberos -================================== - -.. index:: - pair: user authentication; Kerberos - pair: Kerberos; Active Directory (AD) - -User authentication via Active Directory (AD), also referred to as authentication through Kerberos, is supported through AWX. - -To get started, first set up the Kerberos packages in AWX so that you can successfully generate a Kerberos ticket. To install the packages, use the following steps: - -:: - - yum install krb5-workstation - yum install krb5-devel - yum install krb5-libs - -Once installed, edit the ``/etc/krb5.conf`` file, as follows, to provide the address of the AD, the domain, etc.: - -:: - - [logging] - default = FILE:/var/log/krb5libs.log - kdc = FILE:/var/log/krb5kdc.log - admin_server = FILE:/var/log/kadmind.log - - [libdefaults] - default_realm = WEBSITE.COM - dns_lookup_realm = false - dns_lookup_kdc = false - ticket_lifetime = 24h - renew_lifetime = 7d - forwardable = true - - [realms] - WEBSITE.COM = { - kdc = WIN-SA2TXZOTVMV.website.com - admin_server = WIN-SA2TXZOTVMV.website.com - } - - [domain_realm] - .website.com = WEBSITE.COM - website.com = WEBSITE.COM - -After the configuration file has been updated, you should be able to successfully authenticate and get a valid token. -The following steps show how to authenticate and get a token: - -:: - - [root@ip-172-31-26-180 ~]# kinit username - Password for username@WEBSITE.COM: - [root@ip-172-31-26-180 ~]# - - Check if we got a valid ticket. - - [root@ip-172-31-26-180 ~]# klist - Ticket cache: FILE:/tmp/krb5cc_0 - Default principal: username@WEBSITE.COM - - Valid starting Expires Service principal - 01/25/16 11:42:56 01/25/16 21:42:53 krbtgt/WEBSITE.COM@WEBSITE.COM - renew until 02/01/16 11:42:56 - [root@ip-172-31-26-180 ~]# - -Once you have a valid ticket, you can check to ensure that everything is working as expected from command line. To test this, make sure that your inventory looks like the following: - -:: - - [windows] - win01.WEBSITE.COM - - [windows:vars] - ansible_user = username@WEBSITE.COM - ansible_connection = winrm - ansible_port = 5986 - -You should also: - -- Ensure that the hostname is the proper client hostname matching the entry in AD and is not the IP address. - -- In the username declaration, ensure that the domain name (the text after ``@``) is properly entered with regard to upper- and lower-case letters, as Kerberos is case sensitive. For AWX, you should also ensure that the inventory looks the same. - - -.. note:: - - If you encounter a ``Server not found in Kerberos database`` error message, and your inventory is configured using FQDNs (**not IP addresses**), ensure that the service principal name is not missing or mis-configured. - - -Now, running a playbook should run as expected. You can test this by running the playbook as the ``awx`` user. - -Once you have verified that playbooks work properly, integration with AWX is easy. Generate the Kerberos ticket as the ``awx`` user and AWX should automatically pick up the generated ticket for authentication. - -.. note:: - - The python ``kerberos`` package must be installed. Ansible is designed to check if ``kerberos`` package is installed and, if so, it uses kerberos authentication. - - -AD and Kerberos Credentials ------------------------------- - -Active Directory only: - -- If you are only planning to run playbooks against Windows machines with AD usernames and passwords as machine credentials, you can use "user@" format for the username and an associated password. - -With Kerberos: - -- If Kerberos is installed, you can create a machine credential with the username and password, using the "user@" format for the username. - - -Working with Kerberos Tickets -------------------------------- - -Ansible defaults to automatically managing Kerberos tickets when both the username and password are specified in the machine credential for a host that is configured for kerberos. A new ticket is created in a temporary credential cache for each host, before each task executes (to minimize the chance of ticket expiration). The temporary credential caches are deleted after each task, and will not interfere with the default credential cache. - -To disable automatic ticket management (e.g., to use an existing SSO ticket or call ``kinit`` manually to populate the default credential cache), set ``ansible_winrm_kinit_mode=manual`` via the inventory. - -Automatic ticket management requires a standard kinit binary on the control host system path. To specify a different location or binary name, set the ``ansible_winrm_kinit_cmd`` inventory variable to the fully-qualified path to an MIT krbv5 kinit-compatible binary. diff --git a/docs/docsite/rst/administration/ldap_auth.rst b/docs/docsite/rst/administration/ldap_auth.rst deleted file mode 100644 index 62372d949b4a..000000000000 --- a/docs/docsite/rst/administration/ldap_auth.rst +++ /dev/null @@ -1,361 +0,0 @@ -.. _ag_auth_ldap: - -Setting up LDAP Authentication -================================ - -.. index:: - single: LDAP - pair: authentication; LDAP - -This chapter describes how to integrate LDAP authentication with AWX. - -.. note:: - - If the LDAP server you want to connect to has a certificate that is self-signed or signed by a corporate internal certificate authority (CA), the CA certificate must be added to the system's trusted CAs. Otherwise, connection to the LDAP server will result in an error that the certificate issuer is not recognized. - -Administrators use LDAP as a source for account authentication information for AWX users. User authentication is provided, but not the synchronization of user permissions and credentials. Organization membership (as well as the organization admin) and team memberships can be synchronized. - -When so configured, a user who logs in with an LDAP username and password automatically gets an AWX account created for them and they can be automatically placed into organizations as either regular users or organization administrators. - -Users created locally in the user interface, take precedence over those logging into controller for their first time with an alternative authentication solution. You must delete the local user if you want to re-use it with another authentication method, such as LDAP. - -Users created through an LDAP login cannot change their username, given name, surname, or set a local password for themselves. You can also configure this to restrict editing of other field names. - -To configure LDAP integration for AWX: - -1. First, create a user in LDAP that has access to read the entire LDAP structure. - -2. Test if you can make successful queries to the LDAP server, use the ``ldapsearch`` command, which is a command line tool that can be installed on AWX command line as well as on other Linux and OSX systems. Use the following command to query the ldap server, where *josie* and *Josie4Cloud* are replaced by attributes that work for your setup: - -:: - - ldapsearch -x -H ldap://win -D "CN=josie,CN=Users,DC=website,DC=com" -b "dc=website,dc=com" -w Josie4Cloud - -Here ``CN=josie,CN=users,DC=website,DC=com`` is the Distinguished Name of the connecting user. - -.. note:: - - The ``ldapsearch`` utility is not automatically pre-installed with AWX, however, you can install it from the ``openldap-clients`` package. - -3. In the AWX User Interface, click **Settings** from the left navigation and click to select **LDAP settings** from the list of Authentication options. - - - Multiple LDAP configurations are not needed per LDAP server, but you can configure multiple LDAP servers from this page, otherwise, leave the server at **Default**: - - .. image:: ../common/images/configure-awx-auth-ldap-servers.png - - | - - The equivalent API endpoints will show ``AUTH_LDAP_*`` repeated: ``AUTH_LDAP_1_*``, ``AUTH_LDAP_2_*``, ..., ``AUTH_LDAP_5_*`` to denote server designations. - - -4. To enter or modify the LDAP server address to connect to, click **Edit** and enter in the **LDAP Server URI** field using the same format as the one prepopulated in the text field: - -.. image:: ../common/images/configure-awx-auth-ldap-server-uri.png - -.. note:: - - Multiple LDAP servers may be specified by separating each with spaces or commas. Click the |help| icon to comply with proper syntax and rules. - -.. |help| image:: ../common/images/tooltips-icon.png - -5. Enter the password to use for the Binding user in the **LDAP Bind Password** text field. In this example, the password is 'passme': - -.. image:: ../common/images/configure-awx-auth-ldap-bind-pwd.png - -6. Click to select a group type from the **LDAP Group Type** drop-down menu list. - - LDAP Group Types include: - - - ``PosixGroupType`` - - ``GroupOfNamesType`` - - ``GroupOfUniqueNamesType`` - - ``ActiveDirectoryGroupType`` - - ``OrganizationalRoleGroupType`` - - ``MemberDNGroupType`` - - ``NISGroupType`` - - ``NestedGroupOfNamesType`` - - ``NestedGroupOfUniqueNamesType`` - - ``NestedActiveDirectoryGroupType`` - - ``NestedOrganizationalRoleGroupType`` - - ``NestedMemberDNGroupType`` - - ``PosixUIDGroupType`` - - The LDAP Group Types that are supported by leveraging the underlying `django-auth-ldap library`_. To specify the parameters for the selected group type, see :ref:`Step 15 ` below. - - .. _`django-auth-ldap library`: https://django-auth-ldap.readthedocs.io/en/latest/groups.html#types-of-groups - - -7. The **LDAP Start TLS** is disabled by default. To enable TLS when the LDAP connection is not using SSL/TLS, click the toggle to **ON**. - -.. image:: ../common/images/configure-awx-auth-ldap-start-tls.png - -8. Enter the Distinguished Name in the **LDAP Bind DN** text field to specify the user that AWX uses to connect (Bind) to the LDAP server. Below uses the example, ``CN=josie,CN=users,DC=website,DC=com``: - -.. image:: ../common/images/configure-awx-auth-ldap-bind-dn.png - - -9. If that name is stored in key ``sAMAccountName``, the **LDAP User DN Template** populates with ``(sAMAccountName=%(user)s)``. Active Directory stores the username to ``sAMAccountName``. Similarly, for OpenLDAP, the key is ``uid``--hence the line becomes ``(uid=%(user)s)``. - -10. Enter the group distinguish name to allow users within that group to access AWX in the **LDAP Require Group** field, using the same format as the one shown in the text field, ``CN=awx Users,OU=Users,DC=website,DC=com``. - -.. image:: ../common/images/configure-awx-auth-ldap-req-group.png - -11. Enter the group distinguish name to prevent users within that group to access AWX in the **LDAP Deny Group** field, using the same format as the one shown in the text field. In this example, leave the field blank. - - -12. Enter where to search for users while authenticating in the **LDAP User Search** field using the same format as the one shown in the text field. In this example, use: - -:: - - [ - "OU=Users,DC=website,DC=com", - "SCOPE_SUBTREE", - "(cn=%(user)s)" - ] - -The first line specifies where to search for users in the LDAP tree. In the above example, the users are searched recursively starting from ``DC=website,DC=com``. - -The second line specifies the scope where the users should be searched: - - - SCOPE_BASE: This value is used to indicate searching only the entry at the base DN, resulting in only that entry being returned - - SCOPE_ONELEVEL: This value is used to indicate searching all entries one level under the base DN - but not including the base DN and not including any entries under that one level under the base DN. - - SCOPE_SUBTREE: This value is used to indicate searching of all entries at all levels under and including the specified base DN. - -The third line specifies the key name where the user name is stored. - -.. image:: ../common/images/configure-awx-authen-ldap-user-search.png - -.. note:: - - For multiple search queries, the proper syntax is: - :: - - [ - [ - "OU=Users,DC=northamerica,DC=acme,DC=com", - "SCOPE_SUBTREE", - "(sAMAccountName=%(user)s)" - ], - [ - "OU=Users,DC=apac,DC=corp,DC=com", - "SCOPE_SUBTREE", - "(sAMAccountName=%(user)s)" - ], - [ - "OU=Users,DC=emea,DC=corp,DC=com", - "SCOPE_SUBTREE", - "(sAMAccountName=%(user)s)" - ] - ] - - -13. In the **LDAP Group Search** text field, specify which groups should be searched and how to search them. In this example, use: - -:: - - [ - "dc=example,dc=com", - "SCOPE_SUBTREE", - "(objectClass=group)" - ] - -- The first line specifies the BASE DN where the groups should be searched. -- The second lines specifies the scope and is the same as that for the user directive. -- The third line specifies what the ``objectclass`` of a group object is in the LDAP you are using. - -.. image:: ../common/images/configure-awx-authen-ldap-group-search.png - -14. Enter the user attributes in the **LDAP User Attribute Map** the text field. In this example, use: - -:: - - { - "first_name": "givenName", - "last_name": "sn", - "email": "mail" - } - - -The above example retrieves users by last name from the key ``sn``. You can use the same LDAP query for the user to figure out what keys they are stored under. - -.. image:: ../common/images/configure-awx-auth-ldap-user-attrb-map.png - -.. _ldap_grp_params: - -15. Depending on the selected **LDAP Group Type**, different parameters are available in the **LDAP Group Type Parameters** field to account for this. ``LDAP_GROUP_TYPE_PARAMS`` is a dictionary, which will be converted by AWX to kwargs and passed to the LDAP Group Type class selected. There are two common parameters used by any of the LDAP Group Type; ``name_attr`` and ``member_attr``. Where ``name_attr`` defaults to ``cn`` and ``member_attr`` defaults to ``member``: - - :: - - {"name_attr": "cn", "member_attr": "member"} - - To determine what parameters a specific LDAP Group Type expects. refer to the `django_auth_ldap`_ documentation around the classes ``init`` parameters. - - .. _`django_auth_ldap`: https://django-auth-ldap.readthedocs.io/en/latest/reference.html#django_auth_ldap.config.LDAPGroupType - - -16. Enter the user profile flags in the **LDAP User Flags by Group** the text field. In this example, use the following syntax to set LDAP users as "Superusers" and "Auditors": - -:: - - { - "is_superuser": "cn=superusers,ou=groups,dc=website,dc=com", - "is_system_auditor": "cn=auditors,ou=groups,dc=website,dc=com" - } - -The above example retrieves users who are flagged as superusers or as auditor in their profile. - -.. image:: ../common/images/configure-awx-auth-ldap-user-flags.png - -17. For details on completing the mapping fields, see :ref:`ag_ldap_org_team_maps`. - -.. image:: ../common/images/configure-ldap-orgs-teams-mapping.png - -18. Click **Save** when done. - -With these values entered on this form, you can now make a successful authentication with LDAP. - -.. note:: - - AWX does not actively sync users, but they are created during their initial login. - To improve performance associated with LDAP authentication, see :ref:`ldap_auth_perf_tips` at the end of this chapter. - - -.. _ag_ldap_org_team_maps: - -LDAP Organization and Team Mapping -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. index:: - single: organization mapping - single: LDAP mapping - pair: authentication; LDAP mapping - pair: authentication; organization mapping - pair: authentication; LDAP team mapping - pair: authentication; team mapping - single: team mapping - -You can control which users are placed into which organizations based on LDAP attributes (mapping out between your organization admins/users and LDAP groups). - -Keys are organization names. Organizations will be created if not present. Values are dictionaries defining the options for each organization's membership. For each organization, it is possible to specify what groups are automatically users of the organization and also what groups can administer the organization. - -**admins**: None, True/False, string or list/tuple of strings. - - If **None**, organization admins will not be updated based on LDAP values. - - If **True**, all users in LDAP will automatically be added as admins of the organization. - - If **False**, no LDAP users will be automatically added as admins of the organization. - - If a string or list of strings, specifies the group DN(s) that will be added of the organization if they match any of the specified groups. - -**remove_admins**: True/False. Defaults to **False**. - - When **True**, a user who is not an member of the given groups will be removed from the organization's administrative list. - -**users**: None, True/False, string or list/tuple of strings. Same rules apply as for **admins**. - -**remove_users**: True/False. Defaults to **False**. Same rules apply as **remove_admins**. - -:: - - { - "LDAP Organization": { - "admins": "cn=engineering_admins,ou=groups,dc=example,dc=com", - "remove_admins": false, - "users": [ - "cn=engineering,ou=groups,dc=example,dc=com", - "cn=sales,ou=groups,dc=example,dc=com", - "cn=it,ou=groups,dc=example,dc=com" - ], - "remove_users": false - }, - "LDAP Organization 2": { - "admins": [ - "cn=Administrators,cn=Builtin,dc=example,dc=com" - ], - "remove_admins": false, - "users": true, - "remove_users": false - } - } - -Mapping between team members (users) and LDAP groups. Keys are team names (will be created if not present). Values are dictionaries of options for each team's membership, where each can contain the following parameters: - -**organization**: string. The name of the organization to which the team belongs. The team will be created if the combination of organization and team name does not exist. The organization will first be created if it does not exist. - -**users**: None, True/False, string or list/tuple of strings. - - - If **None**, team members will not be updated. - - If **True/False**, all LDAP users will be added/removed as team members. - - If a string or list of strings, specifies the group DN(s). User will be added as a team member if the user is a member of ANY of these groups. - -**remove**: True/False. Defaults to **False**. When **True**, a user who is not a member of the given groups will be removed from the team. - -:: - - { - "LDAP Engineering": { - "organization": "LDAP Organization", - "users": "cn=engineering,ou=groups,dc=example,dc=com", - "remove": true - }, - "LDAP IT": { - "organization": "LDAP Organization", - "users": "cn=it,ou=groups,dc=example,dc=com", - "remove": true - }, - "LDAP Sales": { - "organization": "LDAP Organization", - "users": "cn=sales,ou=groups,dc=example,dc=com", - "remove": true - } - } - - -.. _ldap_logging: - -Enabling Logging for LDAP -~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. index:: - single: LDAP - pair: authentication; LDAP - -To enable logging for LDAP, you must set the level to ``DEBUG`` in the Settings configuration window: - -1. Click **Settings** from the left navigation pane and click to select **Logging settings** from the System list of options. -2. Click **Edit**. -3. Set the **Logging Aggregator Level Threshold** field to **Debug**. - -.. image:: ../common/images/settings-system-logging-debug.png - -4. Click **Save** to save your changes. - - -Referrals -~~~~~~~~~~~ - -.. index:: - pair: LDAP; referrals - pair: troubleshooting; LDAP referrals - -Active Directory uses "referrals" in case the queried object is not available in its database. It has been noted that this does not work properly with the django LDAP client and, most of the time, it helps to disable referrals. Disable LDAP referrals by adding the following lines to your ``/etc/awx/conf.d/custom.py`` file: - - .. code-block:: bash - - AUTH_LDAP_GLOBAL_OPTIONS = { - ldap.OPT_REFERRALS: False, - } - - -.. _ldap_auth_perf_tips: - -LDAP authentication performance tips -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. index:: - pair: best practices; ldap - -When an LDAP user authenticates, by default, all user-related attributes will be updated in the database on each log in. In some environments, this operation can be skipped due to performance issues. To avoid it, you can disable the option `AUTH_LDAP_ALWAYS_UPDATE_USER`. - -.. warning:: - - - With this option set to False, no changes to LDAP user's attributes will be updated. Attributes will only be updated the first time the user is created. - diff --git a/docs/docsite/rst/administration/logging.rst b/docs/docsite/rst/administration/logging.rst index ff5453839a54..bf484ab9cd41 100644 --- a/docs/docsite/rst/administration/logging.rst +++ b/docs/docsite/rst/administration/logging.rst @@ -363,11 +363,3 @@ Troubleshoot Logging API 4XX Errors ~~~~~~~~~~~~~~~~~~~~ You can include the API error message for 4XX errors by modifying the log format for those messages. Refer to the :ref:`logging-api-400-error-config` section for more detail. - -LDAP -~~~~~~ -You can enable logging messages for the LDAP adapter. Refer to the :ref:`ldap_logging` section for more detail. - -SAML -~~~~~~~ -You can enable logging messages for the SAML adapter the same way you can enable logging for LDAP. Refer to the :ref:`ldap_logging` section for more detail. diff --git a/docs/docsite/rst/administration/oauth2_token_auth.rst b/docs/docsite/rst/administration/oauth2_token_auth.rst index e6a3497f5ec0..d99604cf6fe2 100644 --- a/docs/docsite/rst/administration/oauth2_token_auth.rst +++ b/docs/docsite/rst/administration/oauth2_token_auth.rst @@ -449,11 +449,6 @@ Revoking an access token by this method is the same as deleting the token resour The special OAuth 2 endpoints only support using the ``x-www-form-urlencoded`` **Content-type**, so as a result, none of the ``api/o/*`` endpoints accept ``application/json``. -.. note:: - - The **Allow External Users to Create Oauth2 Tokens** (``ALLOW_OAUTH2_FOR_EXTERNAL_USERS`` in the API) setting is disabled by default. External users refer to users authenticated externally with a service like LDAP, or any of the other SSO services. This setting ensures external users cannot *create* their own tokens. If you enable then disable it, any tokens created by external users in the meantime will still exist, and are not automatically revoked. - - Alternatively, you can use the ``manage`` utility, :ref:`ag_manage_utility_revoke_tokens`, to revoke tokens as described in the :ref:`ag_token_utility` section. diff --git a/docs/docsite/rst/administration/performance.rst b/docs/docsite/rst/administration/performance.rst index 32d3c96cd0b8..05b1c0f62be5 100644 --- a/docs/docsite/rst/administration/performance.rst +++ b/docs/docsite/rst/administration/performance.rst @@ -80,15 +80,6 @@ Metrics added in this release to track: - **callback_receiver_event_processing_avg_seconds** - Proxy for “how far behind the callback receiver workers are in processing output". If this number stays large, consider horizontally scaling the control plane and reducing the ``capacity_adjustment`` value on the node. -LDAP login and basic authentication -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. index:: - pair: improvements; LDAP - pair: improvements; basic auth - -Enhancements were made to the authentication backend that syncs LDAP configuration with the organizations and teams in the AWX. Logging in with large mappings between LDAP groups and organizations and teams is now up to 10 times faster than in previous versions. - - Capacity Planning ------------------ .. index:: @@ -382,4 +373,4 @@ For workloads with high levels of API interaction, best practices include: - Use dynamic inventory sources instead of individually creating inventory hosts via the API - Use webhook notifications instead of polling for job status -Since the published blog, additional observations have been made in the field regarding authentication methods. For automation clients that will make many requests in rapid succession, using tokens is a best practice, because depending on the type of user, there may be additional overhead when using basic authentication. For example, LDAP users using basic authentication trigger a process to reconcile if the LDAP user is correctly mapped to particular organizations, teams and roles. Refer to :ref:`ag_oauth2_token_auth` for detail on how to generate and use tokens. +Since the published blog, additional observations have been made in the field regarding authentication methods. For automation clients that will make many requests in rapid succession, using tokens is a best practice, because depending on the type of user, there may be additional overhead when using basic authentication. Refer to :ref:`ag_oauth2_token_auth` for detail on how to generate and use tokens. diff --git a/docs/docsite/rst/administration/secret_handling.rst b/docs/docsite/rst/administration/secret_handling.rst index 41b35d731008..9b025c825bb0 100644 --- a/docs/docsite/rst/administration/secret_handling.rst +++ b/docs/docsite/rst/administration/secret_handling.rst @@ -23,8 +23,7 @@ AWX manages three sets of secrets: User passwords for local users ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -AWX hashes local AWX user passwords with the PBKDF2 algorithm using a SHA256 hash. Users who authenticate via external -account mechanisms (LDAP, SAML, OAuth, and others) do not have any password or secret stored. +AWX hashes local AWX user passwords with the PBKDF2 algorithm using a SHA256 hash. Secret handling for operational use ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/docsite/rst/administration/security_best_practices.rst b/docs/docsite/rst/administration/security_best_practices.rst index 8695082fbf86..14d62cb98957 100644 --- a/docs/docsite/rst/administration/security_best_practices.rst +++ b/docs/docsite/rst/administration/security_best_practices.rst @@ -79,12 +79,6 @@ Existing security functionality Do not disable SELinux, and do not disable AWX’s existing multi-tenant containment. Use AWX’s role-based access control (RBAC) to delegate the minimum level of privileges required to run automation. Use Teams in AWX to assign permissions to groups of users rather than to users individually. See :ref:`rbac-ug` in the |atu|. -External account stores -^^^^^^^^^^^^^^^^^^^^^^^^^ - -Maintaining a full set of users just in AWX can be a time-consuming task in a large organization, prone to error. AWX supports connecting to external account sources via :ref:`LDAP ` and certain :ref:`OAuth providers `. Using this eliminates a source of error when working with permissions. - - .. _ag_security_django_password: Django password policies diff --git a/docs/docsite/rst/administration/social_auth.rst b/docs/docsite/rst/administration/social_auth.rst deleted file mode 100644 index 9979fb0d3934..000000000000 --- a/docs/docsite/rst/administration/social_auth.rst +++ /dev/null @@ -1,393 +0,0 @@ -.. _ag_social_auth: - -Setting up Social Authentication -================================== - -.. index:: - single: social authentication - single: authentication - -Authentication methods help simplify logins for end users--offering single sign-ons using existing login information to sign into a third party website rather than creating a new login account specifically for that website. - -Account authentication can be configured in the AWX User Interface and saved to the PostgreSQL database. For instructions, refer to the :ref:`ag_configure_awx` section. - -Account authentication in AWX can be configured to centrally use OAuth2, while enterprise-level account authentication can be configured for :ref:`Azure `, :ref:`RADIUS `, or even :ref:`LDAP ` as a source for authentication information. See :ref:`ag_ent_auth` for more detail. - -For websites, such as Microsoft Azure, Google or GitHub, that provide account information, account information is often implemented using the OAuth standard. OAuth is a secure authorization protocol which is commonly used in conjunction with account authentication to grant 3rd party applications a "session token" allowing them to make API calls to providers on the user’s behalf. - -The :ref:`RADIUS ` distributed client/server system allows you to secure networks against unauthorized access and can be implemented in network environments requiring high levels of security while maintaining network access for remote users. - - -.. _ag_auth_github: - -GitHub settings ----------------- - -.. index:: - pair: authentication; GitHub OAuth2 - -To set up social authentication for GitHub, you will need to obtain an OAuth2 key and secret for a web application. To do this, you must first register the new application with GitHub at https://github.com/settings/developers. In order to register the application, you must supply it with your homepage URL, which is the **Callback URL** shown in the Details tab for the GitHub default settings page. The OAuth2 key (Client ID) and secret (Client Secret) will be used to supply the required fields in the AWX User Interface. - -1. Click **Settings** from the left navigation bar. - -2. On the left side of the Settings window, click **GitHub settings** from the list of Authentication options. - -3. Click the **GitHub Default** tab if not already selected. - -The **GitHub OAuth2 Callback URL** field is already pre-populated and non-editable. Once the application is registered, GitHub displays the Client ID and Client Secret. - -4. Click **Edit** and copy and paste GitHub's Client ID into the **GitHub OAuth2 Key** field. - -5. Copy and paste GitHub's Client Secret into the **GitHub OAuth2 Secret** field. - -6. For details on completing the mapping fields, see :ref:`ag_org_team_maps`. - -7. Click **Save** when done. - -8. To verify that the authentication was configured correctly, logout of AWX and the login screen will now display the GitHub logo to allow logging in with those credentials. - -.. image:: ../common/images/configure-awx-auth-github-logo.png - - -.. _ag_auth_github_org: - -GitHub Organization settings -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. index:: - pair: authentication; GitHub Org - -When defining account authentication with either an organization or a team within an organization, you should use the specific organization and team settings. Account authentication can be limited by an organization as well as by a team within an organization. - -You can also choose to allow all by specifying non-organization or non-team based settings (as shown above). - -You can limit users who can login to AWX by limiting only those in an organization or on a team within an organization. - -To set up social authentication for a GitHub Organization, you will need to obtain an OAuth2 key and secret for a web application. To do this, you must first register your organization-owned application at ``https://github.com/organizations//settings/applications``. In order to register the application, you must supply it with your Authorization callback URL, which is the **Callback URL** shown in the Details page. Each key and secret must belong to a unique application and cannot be shared or reused between different authentication backends. The OAuth2 key (Client ID) and secret (Client Secret) will be used to supply the required fields in the AWX User Interface. - -1. Click **Settings** from the left navigation bar. - -2. On the left side of the Settings window, click **GitHub settings** from the list of Authentication options. - -3. Click the **GitHub Organization** tab. - -The **GitHub Organization OAuth2 Callback URL** field is already pre-populated and non-editable. - -Once the application is registered, GitHub displays the Client ID and Client Secret. - -4. Click **Edit** and copy and paste GitHub's Client ID into the **GitHub Organization OAuth2 Key** field. - -5. Copy and paste GitHub's Client Secret into the **GitHub Organization OAuth2 Secret** field. - -6. Enter the name of your GitHub organization, as used in your organization's URL (e.g., https://github.com//) in the **GitHub Organization Name** field. - -7. For details on completing the mapping fields, see :ref:`ag_org_team_maps`. - -8. Click **Save** when done. - -9. To verify that the authentication was configured correctly, logout of AWX and the login screen will now display the GitHub Organization logo to allow logging in with those credentials. - -.. image:: ../common/images/configure-awx-auth-github-orgs-logo.png - - -.. _ag_auth_github_team: - -GitHub Team settings -~~~~~~~~~~~~~~~~~~~~~~~~ - -.. index:: - pair: authentication; GitHub Team - - -To set up social authentication for a GitHub Team, you will need to obtain an OAuth2 key and secret for a web application. To do this, you must first register your team-owned application at ``https://github.com/organizations//settings/applications``. In order to register the application, you must supply it with your Authorization callback URL, which is the **Callback URL** shown in the Details page. Each key and secret must belong to a unique application and cannot be shared or reused between different authentication backends. The OAuth2 key (Client ID) and secret (Client Secret) will be used to supply the required fields in the AWX User Interface. - -1. Find the numeric team ID using the GitHub API: http://fabian-kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/. The Team ID will be used to supply a required field in the AWX User Interface. - -2. Click **Settings** from the left navigation bar. - -3. On the left side of the Settings window, click **GitHub settings** from the list of Authentication options. - -4. Click the **GitHub Team** tab. - -The **GitHub Team OAuth2 Callback URL** field is already pre-populated and non-editable. Once the application is registered, GitHub displays the Client ID and Client Secret. - -5. Click **Edit** and copy and paste GitHub's Client ID into the **GitHub Team OAuth2 Key** field. - -6. Copy and paste GitHub's Client Secret into the **GitHub Team OAuth2 Secret** field. - -7. Copy and paste GitHub's team ID in the **GitHub Team ID** field. - -8. For details on completing the mapping fields, see :ref:`ag_org_team_maps`. - -9. Click **Save** when done. - -10. To verify that the authentication was configured correctly, logout of AWX and the login screen will now display the GitHub Team logo to allow logging in with those credentials. - -.. image:: ../common/images/configure-awx-auth-github-teams-logo.png - - -GitHub Enterprise settings -~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. index:: - pair: authentication; GitHub Enterprise - -To set up social authentication for a GitHub Enterprise, you will need to obtain a GitHub Enterprise URL, an API URL, OAuth2 key and secret for a web application. To obtain the URLs, refer to the GitHub documentation on `GitHub Enterprise administration `_ . To obtain the key and secret, you must first register your enterprise-owned application at ``https://github.com/organizations//settings/applications``. In order to register the application, you must supply it with your Authorization callback URL, which is the **Callback URL** shown in the Details page. Because its hosted on site and not github.com, you must specify which auth adapter it will talk to. - -Each key and secret must belong to a unique application and cannot be shared or reused between different authentication backends. The OAuth2 key (Client ID) and secret (Client Secret) will be used to supply the required fields in the AWX User Interface. - -1. Click **Settings** from the left navigation bar. - -2. On the left side of the Settings window, click **GitHub settings** from the list of Authentication options. - -3. Click the **GitHub Enterprise** tab. - -The **GitHub Enterprise OAuth2 Callback URL** field is already pre-populated and non-editable. Once the application is registered, GitHub displays the Client ID and Client Secret. - -4. Click **Edit** to configure GitHub Enterprise settings. - -5. In the **GitHub Enterprise URL** field, enter the hostname of the GitHub Enterprise instance (e.g., https://github.example.com). - -6. In the **GitHub Enterprise API URL** field, enter the API URL of the GitHub Enterprise instance (e.g., https://github.example.com/api/v3) - -7. Copy and paste GitHub's Client ID into the **GitHub Enterprise OAuth2 Key** field. - -8. Copy and paste GitHub's Client Secret into the **GitHub Enterprise OAuth2 Secret** field. - -9. For details on completing the mapping fields, see :ref:`ag_org_team_maps`. - -10. Click **Save** when done. - -11. To verify that the authentication was configured correctly, logout of AWX and the login screen will now display the GitHub Enterprise logo to allow logging in with those credentials. - -.. image:: ../common/images/configure-awx-auth-github-ent-logo.png - - -GitHub Enterprise Organization settings -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. index:: - pair: authentication; GitHub Enterprise Org - -To set up social authentication for a GitHub Enterprise Org, you will need to obtain a GitHub Enterprise Org URL, an Org API URL, an Org OAuth2 key and secret for a web application. To obtain the URLs, refer to the GitHub documentation on `GitHub Enterprise administration `_ . To obtain the key and secret, you must first register your enterprise organization-owned application at ``https://github.com/organizations//settings/applications``. In order to register the application, you must supply it with your Authorization callback URL, which is the **Callback URL** shown in the Details page. Because its hosted on site and not github.com, you must specify which auth adapter it will talk to. - -Each key and secret must belong to a unique application and cannot be shared or reused between different authentication backends. The OAuth2 key (Client ID) and secret (Client Secret) will be used to supply the required fields in the AWX User Interface. - -1. Click **Settings** from the left navigation bar. - -2. On the left side of the Settings window, click **GitHub settings** from the list of Authentication options. - -3. Click the **GitHub Enterprise Organization** tab. - -The **GitHub Enterprise Organization OAuth2 Callback URL** field is already pre-populated and non-editable. Once the application is registered, GitHub displays the Client ID and Client Secret. - -4. Click **Edit** to configure GitHub Enterprise Organization settings. - -5. In the **GitHub Enterprise Organization URL** field, enter the hostname of the GitHub Enterprise Org instance (e.g., https://github.orgexample.com). - -6. In the **GitHub Enterprise Organization API URL** field, enter the API URL of the GitHub Enterprise Org instance (e.g., https://github.orgexample.com/api/v3) - -7. Copy and paste GitHub's Client ID into the **GitHub Enterprise Organization OAuth2 Key** field. - -8. Copy and paste GitHub's Client Secret into the **GitHub Enterprise Organization OAuth2 Secret** field. - -9. Enter the name of your GitHub Enterprise organization, as used in your organization's URL (e.g., https://github.com//) in the **GitHub Enterprise Organization Name** field. - -10. For details on completing the mapping fields, see :ref:`ag_org_team_maps`. - -11. Click **Save** when done. - -12. To verify that the authentication was configured correctly, logout of AWX and the login screen will now display the GitHub Enterprise Organization logo to allow logging in with those credentials. - -.. image:: ../common/images/configure-awx-auth-github-ent-org-logo.png - - -GitHub Enterprise Team settings -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. index:: - pair: authentication; GitHub Enterprise Team - -To set up social authentication for a GitHub Enterprise teams, you will need to obtain a GitHub Enterprise Org URL, an Org API URL, an Org OAuth2 key and secret for a web application. To obtain the URLs, refer to the GitHub documentation on `GitHub Enterprise administration `_ . To obtain the key and secret, you must first register your enterprise team-owned application at ``https://github.com/organizations//settings/applications``. In order to register the application, you must supply it with your Authorization callback URL, which is the **Callback URL** shown in the Details page. Because its hosted on site and not github.com, you must specify which auth adapter it will talk to. - -Each key and secret must belong to a unique application and cannot be shared or reused between different authentication backends. The OAuth2 key (Client ID) and secret (Client Secret) will be used to supply the required fields in the AWX User Interface. - -1. Find the numeric team ID using the GitHub API: http://fabian-kostadinov.github.io/2015/01/16/how-to-find-a-github-team-id/. The Team ID will be used to supply a required field in the AWX User Interface. - -2. Click **Settings** from the left navigation bar. - -3. On the left side of the Settings window, click **GitHub settings** from the list of Authentication options. - -4. Click the **GitHub Enterprise Team** tab. - -The **GitHub Enterprise Team OAuth2 Callback URL** field is already pre-populated and non-editable. Once the application is registered, GitHub displays the Client ID and Client Secret. - -5. Click **Edit** to configure GitHub Enterprise Team settings. - -6. In the **GitHub Enterprise Team URL** field, enter the hostname of the GitHub Enterprise team instance (e.g., https://github.teamexample.com). - -7. In the **GitHub Enterprise Team API URL** field, enter the API URL of the GitHub Enterprise team instance (e.g., https://github.teamexample.com/api/v3) - -8. Copy and paste GitHub's Client ID into the **GitHub Enterprise Team OAuth2 Key** field. - -9. Copy and paste GitHub's Client Secret into the **GitHub Enterprise Team OAuth2 Secret** field. - -10. Copy and paste GitHub's team ID in the **GitHub Enterprise Team ID** field. - -11. For details on completing the mapping fields, see :ref:`ag_org_team_maps`. - -12. Click **Save** when done. - -13. To verify that the authentication was configured correctly, logout of AWX and the login screen will now display the GitHub Enterprise Teams logo to allow logging in with those credentials. - -.. image:: ../common/images/configure-awx-auth-github-ent-teams-logo.png - - -.. _ag_auth_google_oauth2: - -Google OAuth2 settings ------------------------ - -.. index:: - pair: authentication; Google OAuth2 - -To set up social authentication for Google, you will need to obtain an OAuth2 key and secret for a web application. To do this, you must first create a project and set it up with Google. Refer to https://support.google.com/googleapi/answer/6158849 for instructions. If you already completed the setup process, you can access those credentials by going to the Credentials section of the `Google API Manager Console `_. The OAuth2 key (Client ID) and secret (Client secret) will be used to supply the required fields in the AWX User Interface. - -1. Click **Settings** from the left navigation bar. - -2. On the left side of the Settings window, click **Google OAuth 2 settings** from the list of Authentication options. - -The **Google OAuth2 Callback URL** field is already pre-populated and non-editable. - -3. The following fields are also pre-populated. If not, use the credentials Google supplied during the web application setup process, and look for the values with the same format as the ones shown in the example below: - - - Click **Edit** and copy and paste Google's Client ID into the **Google OAuth2 Key** field. - - - Copy and paste Google's Client secret into the **Google OAuth2 Secret** field. - - .. image:: ../common/images/configure-awx-auth-google.png - -4. To complete the remaining optional fields, refer to the tooltips in each of the fields for instructions and required format. - -5. For details on completing the mapping fields, see :ref:`ag_org_team_maps`. - -6. Click **Save** when done. - -7. To verify that the authentication was configured correctly, logout of AWX and the login screen will now display the Google logo to indicate it as a alternate method of logging into AWX. - -.. image:: ../common/images/configure-awx-auth-google-logo.png - - - -.. _ag_org_team_maps: - -Organization and Team Mapping ---------------------------------- - -.. index:: - single: organization mapping - pair: authentication; organization mapping - pair: authentication; team mapping - single: team mapping - -Organization mapping -~~~~~~~~~~~~~~~~~~~~~ - -You will need to control which users are placed into which organizations based on their username and email address (mapping out your organization admins/users from social or enterprise-level authentication accounts). - -Dictionary keys are organization names. Organizations will be created, if not already present and if the license allows for multiple organizations. Otherwise, the single default organization is used regardless of the key. - -Values are dictionaries defining the options for each organization's membership. For each organization, it is possible to specify which users are automatically users of the organization and also which users can administer the organization. - -**admins**: None, True/False, string or list/tuple of strings. - - - If **None**, organization admins will not be updated. - - If **True**, all users using account authentication will automatically be added as admins of the organization. - - If **False**, no account authentication users will be automatically added as admins of the organization. - - If a string or list of strings, specifies the usernames and emails for users who will be added to the organization. Strings beginning and ending with ``/`` will be compiled into regular expressions; modifiers ``i`` (case-insensitive) and ``m`` (multi-line) may be specified after the ending ``/``. - -**remove_admins**: True/False. Defaults to **True**. - - - When **True**, a user who does not match is removed from the organization's administrative list. - -**users**: None, True/False, string or list/tuple of strings. Same rules apply as for **admins**. - -**remove_users**: True/False. Defaults to **True**. Same rules apply as for **remove_admins**. - - -:: - - { - "Default": { - "users": true - }, - "Test Org": { - "admins": ["admin@example.com"], - "users": true - }, - "Test Org 2": { - "admins": ["admin@example.com", "/^awx-[^@]+?@.*$/i"], - "users": "/^[^@].*?@example\\.com$/" - } - } - -Organization mappings may be specified separately for each account authentication backend. If defined, these configurations will take precedence over the global configuration above. - -:: - - SOCIAL_AUTH_GOOGLE_OAUTH2_ORGANIZATION_MAP = {} - SOCIAL_AUTH_GITHUB_ORGANIZATION_MAP = {} - SOCIAL_AUTH_GITHUB_ORG_ORGANIZATION_MAP = {} - SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP = {} - - -Team mapping -~~~~~~~~~~~~~~ - -Team mapping is the mapping of team members (users) from social auth accounts. Keys are team names (will be created if not present). Values are dictionaries of options for each team's membership, where each can contain the following parameters: - -**organization**: string. The name of the organization to which the team -belongs. The team will be created if the combination of organization and -team name does not exist. The organization will first be created if it -does not exist. If the license does not allow for multiple organizations, -the team will always be assigned to the single default organization. - -**users**: None, True/False, string or list/tuple of strings. - - - If **None**, team members will not be updated. - - If **True**/**False**, all social auth users will be added/removed as team members. - - If a string or list of strings, specifies expressions used to match users. User will be added as a team member if the username or email matches. Strings beginning and ending with ``/`` will be compiled into regular expressions; modifiers ``i`` (case-insensitive) and ``m`` (multi-line) may be specified after the ending ``/``. - -**remove**: True/False. Defaults to **True**. When **True**, a user who does not match the rules above is removed from the team. - -:: - - { - "My Team": { - "organization": "Test Org", - "users": ["/^[^@]+?@test\\.example\\.com$/"], - "remove": true - }, - "Other Team": { - "organization": "Test Org 2", - "users": ["/^[^@]+?@test\\.example\\.com$/"], - "remove": false - } - } - - -Team mappings may be specified separately for each account authentication backend, based on which of these you setup. When defined, these configurations take precedence over the global configuration above. - -:: - - SOCIAL_AUTH_GOOGLE_OAUTH2_TEAM_MAP = {} - SOCIAL_AUTH_GITHUB_TEAM_MAP = {} - SOCIAL_AUTH_GITHUB_ORG_TEAM_MAP = {} - SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP = {} - -Uncomment the line below (i.e. set ``SOCIAL_AUTH_USER_FIELDS`` to an empty list) to prevent new user accounts from being created. Only users who have previously logged in to AWX using social or enterprise-level authentication or have a user account with a matching email address will be able to login. - -:: - - SOCIAL_AUTH_USER_FIELDS = [] - diff --git a/docs/docsite/rst/administration/troubleshooting.rst b/docs/docsite/rst/administration/troubleshooting.rst index 43c9bfa8d909..f363695265b5 100644 --- a/docs/docsite/rst/administration/troubleshooting.rst +++ b/docs/docsite/rst/administration/troubleshooting.rst @@ -52,9 +52,6 @@ Example configuration of ``extra_settings`` parameter: - setting: MAX_PAGE_SIZE value: "500" - - setting: AUTH_LDAP_BIND_DN - value: "cn=admin,dc=example,dc=com" - - setting: LOG_AGGREGATOR_LEVEL value: "'DEBUG'" diff --git a/docs/docsite/rst/common/images/ug-settings-menu-screen-authentication.png b/docs/docsite/rst/common/images/ug-settings-menu-screen-authentication.png deleted file mode 100644 index ab98c73d5d53..000000000000 Binary files a/docs/docsite/rst/common/images/ug-settings-menu-screen-authentication.png and /dev/null differ diff --git a/docs/docsite/rst/common/images/ug-settings-menu-screen.png b/docs/docsite/rst/common/images/ug-settings-menu-screen.png deleted file mode 100644 index 081d0db8b004..000000000000 Binary files a/docs/docsite/rst/common/images/ug-settings-menu-screen.png and /dev/null differ diff --git a/docs/docsite/rst/common/settings-menu.rst b/docs/docsite/rst/common/settings-menu.rst index b47179ea4910..cfce9c5df672 100644 --- a/docs/docsite/rst/common/settings-menu.rst +++ b/docs/docsite/rst/common/settings-menu.rst @@ -5,9 +5,6 @@ pair: settings menu; configure the controller -To enter the Settings window for AWX, click **Settings** from the left navigation bar. This page allows you to modify your AWX configuration, such as settings associated with authentication, jobs, system, and user interface. - -.. image:: ../common/images/ug-settings-menu-screen.png - :alt: The main settings window for AWX. +To enter the Settings window for AWX, click **Settings** from the left navigation bar. This page allows you to modify your AWX configuration, such as settings associated with jobs, system, and user interface. For more information on configuring these settings, refer to :ref:`ag_configure_awx` section of the |ata|. \ No newline at end of file diff --git a/docs/docsite/rst/quickstart/examine_dashboard.rst b/docs/docsite/rst/quickstart/examine_dashboard.rst index 00bfcde9557f..cbe645e6140d 100644 --- a/docs/docsite/rst/quickstart/examine_dashboard.rst +++ b/docs/docsite/rst/quickstart/examine_dashboard.rst @@ -42,9 +42,6 @@ The very last item in the navigation bar is **Settings**, which provides access The Settings page allows administrators to configure authentication, jobs, system-level attributes, and customize the user interface. Refer to :ref:`ag_configure_awx` section for more detail. -.. image:: ../common/images/ug-settings-menu-screen.png - - Regardless of the window or action you're performing, the very top of each page next to the your user icon is the About (|about|) icon, which provides you the versions of AWX and Ansible you are currently running. .. |about| image:: ../common/images/help-about-icon.png diff --git a/docs/docsite/rst/release_notes/known_issues.rst b/docs/docsite/rst/release_notes/known_issues.rst index ae685f152c2e..0d51430bd92e 100644 --- a/docs/docsite/rst/release_notes/known_issues.rst +++ b/docs/docsite/rst/release_notes/known_issues.rst @@ -14,7 +14,6 @@ Known Issues pair: known issues; live event statuses pair: live event statuses; green dot pair: live event statuses; red dot - pair: known issues; LDAP authentication pair: known issues; lost isolated jobs pair: known issues; sosreport pair: known issues; local management @@ -28,8 +27,6 @@ Known Issues pair: known issues; Ansible Azure dependencies pair: known issues; authentication (reactive user) pair: known issues; user cannot log in using authentication - pair: known issues; login problems with social authentication - pair: known issues; OAuth account recreation pair: known issues; login via http pair: known issues; web sockets in safari pair: known issues; browser auto-complete @@ -97,13 +94,6 @@ Misuse of job slicing can cause errors in job scheduling .. include:: ../common/job-slicing-rule.rst - -Default LDAP directory must be configured to use LDAP Authentication -====================================================================== - -The ability to configure up to six LDAP directories for authentication requires a value. On the settings page for LDAP, there is a "Default" LDAP configuration followed by five-numbered configuration slots. If the "Default" is not populated, AWX will not try to authenticate using the other directory configurations. - - Potential security issue using ``X_FORWARDED_FOR`` in ``REMOTE_HOST_HEADERS`` ================================================================================= @@ -181,12 +171,6 @@ Database server installed on nodes All nodes in the cluster get a database server even if the nodes do not have a database. This is unexpected and may take up space. -Reactivating OAuth authentication accounts which have been deleted -=================================================================== - -Once a user who logs in using social authentication has been deleted, the user will not be able to login again or be recreated until the system administrator runs a ``cleanup_deleted`` action with ``days=0`` to allow users to login again. Once ``cleanup_deleted`` has been run, AWX must be restarted. Accounts which have been deleted prior to having the ``cleanup_deleted`` action run will receive a "Your account is inactive" message upon trying to login. - - Using vaulted variables in inventory sourced from a project =========================================================== diff --git a/docs/docsite/rst/rest_api/authentication.rst b/docs/docsite/rst/rest_api/authentication.rst index 0349bbfa388f..5c636ec7dba5 100644 --- a/docs/docsite/rst/rest_api/authentication.rst +++ b/docs/docsite/rst/rest_api/authentication.rst @@ -185,16 +185,3 @@ If you need to write custom requests, you can write a Python script using `Pytho # prints json returned from awx with formatting print(json.dumps(response.json(), indent=4, sort_keys=True)) - - -.. _api_sso_auth: - -SSO Authentication -------------------- - -Single sign-on (SSO) authentication methods are fundamentally different from other methods because the authentication of the user happens external to AWX, like Google SSO, Azure SSO, SAML, or GitHub. For example, with GitHub SSO, GitHub is the single source of truth, which verifies your identity based on the username and password you gave AWX. - -You can configure SSO authentication using AWX inside a large organization with a central Identity Provider. Once you have configured an SSO method in AWX, a button for that SSO will be present on the login screen. If you click that button, it will redirect you to the Identity Provider, in this case GitHub, where you will present your credentials. If the Identity Provider verifies you successfully, then AWX will make a user linked to your GitHub user (if this is your first time logging in via this SSO method), and log you in. - -For the various types of supported SSO authentication methods, see :ref:`ag_social_auth` and :ref:`ag_ent_auth` in the |ata|. - diff --git a/docs/docsite/rst/userguide/glossary.rst b/docs/docsite/rst/userguide/glossary.rst index f55659f69ca8..d0279cdfb779 100644 --- a/docs/docsite/rst/userguide/glossary.rst +++ b/docs/docsite/rst/userguide/glossary.rst @@ -61,7 +61,7 @@ Glossary A collection of hosts against which Jobs may be launched. Inventory Script - A very simple program (or a complicated one) that looks up hosts, group membership for hosts, and variable information from an external resource--whether that be a SQL database, a CMDB solution, or something like LDAP. This concept was adapted from Puppet (where it is called an “External Nodes Classifier”) and works more or less exactly the same way. + A very simple program (or a complicated one) that looks up hosts, group membership for hosts, and variable information from an external resource--whether that be a SQL database or a CMDB solution. This concept was adapted from Puppet (where it is called an “External Nodes Classifier”) and works more or less exactly the same way. Inventory Source Information about a cloud or other script that should be merged into the current inventory group, resulting in the automatic population of Groups, Hosts, and variables about those groups and hosts. diff --git a/docs/docsite/rst/userguide/overview.rst b/docs/docsite/rst/userguide/overview.rst index 59f44063f9a8..318c61349e61 100644 --- a/docs/docsite/rst/userguide/overview.rst +++ b/docs/docsite/rst/userguide/overview.rst @@ -189,7 +189,6 @@ Authentication Enhancements pair: features; authentication pair: features; OAuth 2 token -AWX supports LDAP, SAML, token-based authentication. Enhanced LDAP and SAML support allows you to integrate your account information in a more flexible manner. Token-based Authentication allows for easily authentication of third-party tools and services with AWX via integrated OAuth 2 token support. Cluster Management ~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/docsite/rst/userguide/projects.rst b/docs/docsite/rst/userguide/projects.rst index 73c37515bdfb..5a715d7b5109 100644 --- a/docs/docsite/rst/userguide/projects.rst +++ b/docs/docsite/rst/userguide/projects.rst @@ -473,7 +473,7 @@ Before AWX can use |ah| as the default source for collections content, you need - **Galaxy Server URL** = ``https://cloud.redhat.com/api/automation-hub/`` - - **AUTH SEVER URL** = ``https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token`` + - **AUTH SERVER URL** = ``https://sso.redhat.com/auth/realms/redhat-external/protocol/openid-connect/token`` 4. To use a private |ah|, create an |ah| credential using a token retrieved from the Repo Management dashboard of your local |ah| and pointing to the published repo URL as shown: diff --git a/docs/docsite/rst/userguide/rbac.rst b/docs/docsite/rst/userguide/rbac.rst index f94e95763721..1eb31e9f94ac 100644 --- a/docs/docsite/rst/userguide/rbac.rst +++ b/docs/docsite/rst/userguide/rbac.rst @@ -248,7 +248,7 @@ Often, you will have many Roles in a system and you will want some roles to incl .. |rbac-heirarchy-morecomplex| image:: ../common/images/rbac-heirarchy-morecomplex.png -RBAC controls also give you the capability to explicitly permit User and Teams of Users to run playbooks against certain sets of hosts. Users and teams are restricted to just the sets of playbooks and hosts to which they are granted capabilities. And, with AWX, you can create or import as many Users and Teams as you require--create users and teams manually or import them from LDAP or Active Directory. +RBAC controls also give you the capability to explicitly permit User and Teams of Users to run playbooks against certain sets of hosts. Users and teams are restricted to just the sets of playbooks and hosts to which they are granted capabilities. And, with AWX, you can create or import as many Users and Teams as you require--create users and teams manually or import them from Active Directory. RBACs are easiest to think of in terms of who or what can see, change, or delete an "object" for which a specific capability is being determined. diff --git a/docs/tower_configuration.md b/docs/tower_configuration.md index ec917fd924ef..69bf73f3fb6e 100644 --- a/docs/tower_configuration.md +++ b/docs/tower_configuration.md @@ -5,28 +5,6 @@ AWX configuration gives AWX users the ability to adjust multiple runtime paramet #### To Use: The REST endpoint for CRUD operations against AWX configurations can be found at `/api/v2/settings/`. GETing to that endpoint will return a list of available AWX configuration categories and their URLs, such as `"system": "/api/v2/settings/system/"`. The URL given to each category is the endpoint for CRUD operations against individual settings under that category. -Here is a typical AWX configuration category GET response: -``` -GET /api/v2/settings/github-team/ -HTTP 200 OK -Allow: GET, PUT, PATCH, DELETE, HEAD, OPTIONS -Content-Type: application/json -Vary: Accept -X-API-Node: tower -X-API-Query-Count: 6 -X-API-Query-Time: 0.004s -X-API-Time: 0.026s - -{ - "SOCIAL_AUTH_GITHUB_TEAM_CALLBACK_URL": "https://platformhost/sso/complete/github-team/", - "SOCIAL_AUTH_GITHUB_TEAM_KEY": "", - "SOCIAL_AUTH_GITHUB_TEAM_SECRET": "", - "SOCIAL_AUTH_GITHUB_TEAM_ID": "", - "SOCIAL_AUTH_GITHUB_TEAM_ORGANIZATION_MAP": null, - "SOCIAL_AUTH_GITHUB_TEAM_TEAM_MAP": null -} -``` - The returned body is a JSON of key-value pairs, where the key is the name of the AWX configuration setting, and the value is the value of that setting. To update the settings, simply update setting values and PUT/PATCH to the same endpoint. #### To Develop: diff --git a/licenses/async-timeout.txt b/licenses/async-timeout.txt deleted file mode 100644 index 8dada3edaf50..000000000000 --- a/licenses/async-timeout.txt +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/licenses/python3-openid.txt b/licenses/awx-plugins.interfaces.txt similarity index 99% rename from licenses/python3-openid.txt rename to licenses/awx-plugins.interfaces.txt index d64569567334..e72929ee9931 100644 --- a/licenses/python3-openid.txt +++ b/licenses/awx-plugins.interfaces.txt @@ -1,4 +1,3 @@ - Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ @@ -200,3 +199,4 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. + \ No newline at end of file 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/django-auth-ldap.txt b/licenses/django-auth-ldap.txt deleted file mode 100644 index b16b2c01ff6a..000000000000 --- a/licenses/django-auth-ldap.txt +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) 2009, Peter Sagerson -All rights reserved. - -Redistribution and use in source and binary forms, with or without modification, -are permitted provided that the following conditions are met: - -- Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -- 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. - -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 HOLDER 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/django-radius.txt b/licenses/django-radius.txt deleted file mode 100644 index 78cf4098c7a6..000000000000 --- a/licenses/django-radius.txt +++ /dev/null @@ -1,24 +0,0 @@ -Copyright (c) 2015, Rob Golding. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * 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. - * Neither the name of Rob Golding, 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 HOLDER 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/ecdsa.txt b/licenses/ecdsa.txt deleted file mode 100644 index 474479a2cef6..000000000000 --- a/licenses/ecdsa.txt +++ /dev/null @@ -1,24 +0,0 @@ -"python-ecdsa" Copyright (c) 2010 Brian Warner - -Portions written in 2005 by Peter Pearson and placed in the public domain. - -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/lxml.txt b/licenses/lxml.txt deleted file mode 100644 index 76c02ef80751..000000000000 --- a/licenses/lxml.txt +++ /dev/null @@ -1,63 +0,0 @@ -lxml is copyright Infrae and distributed under the BSD license (see -doc/licenses/BSD.txt), with the following exceptions: - -Some code, such a selftest.py, selftest2.py and -src/lxml/_elementpath.py are derived from ElementTree and -cElementTree. See doc/licenses/elementtree.txt for the license text. - -lxml.cssselect and lxml.html are copyright Ian Bicking and distributed -under the BSD license (see doc/licenses/BSD.txt). - -test.py, the test-runner script, is GPL and copyright Shuttleworth -Foundation. See doc/licenses/GPL.txt. It is believed the unchanged -inclusion of test.py to run the unit test suite falls under the -"aggregation" clause of the GPL and thus does not affect the license -of the rest of the package. - -The doctest.py module is taken from the Python library and falls under -the PSF Python License. - -The isoschematron implementation uses several XSL and RelaxNG resources: - * The (XML syntax) RelaxNG schema for schematron, copyright International - Organization for Standardization (see - src/lxml/isoschematron/resources/rng/iso-schematron.rng for the license - text) - * The skeleton iso-schematron-xlt1 pure-xslt schematron implementation - xsl stylesheets, copyright Rick Jelliffe and Academia Sinica Computing - Center, Taiwan (see the xsl files here for the license text: - src/lxml/isoschematron/resources/xsl/iso-schematron-xslt1/) - * The xsd/rng schema schematron extraction xsl transformations are unlicensed - and copyright the respective authors as noted (see - src/lxml/isoschematron/resources/xsl/RNG2Schtrn.xsl and - src/lxml/isoschematron/resources/xsl/XSD2Schtrn.xsl) - -doc/licenses/BSD.txt: -Copyright (c) 2004 Infrae. 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 Infrae 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 INFRAE 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/netaddr.txt b/licenses/netaddr.txt deleted file mode 100644 index 2aeb473594b4..000000000000 --- a/licenses/netaddr.txt +++ /dev/null @@ -1,43 +0,0 @@ -Here are the copyright notices applicable to the netaddr library. - -------- -netaddr -------- - -Copyright (c) 2008 by David P. D. Moss. All rights reserved. - -Released under the BSD license. See the LICENSE file for details. - ------------------------------------------- -IANA (Internet Assigned Numbers Authority) ------------------------------------------- - -netaddr is not sponsored nor endorsed by IANA. - -Use of data from IANA (Internet Assigned Numbers Authority) is subject to -copyright and is provided with prior written permission. - -IANA data files included with netaddr are not modified in any way but are -parsed and made available to end users through an API. - -See README file and source code for URLs to latest copies of the relevant -files. - ------------------------------------------- -IEEE (Institution of Electrical Engineers) ------------------------------------------- - -netaddr is not sponsored nor endorsed by the IEEE. - -Use of data from the IEEE (Institute of Electrical and Electronics -Engineers) is subject to copyright. See the following URL for -details :- - -http://www.ieee.org/web/publications/rights/legal.html - -IEEE data files included with netaddr are not modified in any way but are -parsed and made available to end users through an API. There is no -guarantee that referenced files are not out of date. - -See README file and source code for URLs to latest copies of the relevant -files. diff --git a/licenses/pyrad.txt b/licenses/pyrad.txt deleted file mode 100644 index e6b4fe653b10..000000000000 --- a/licenses/pyrad.txt +++ /dev/null @@ -1,28 +0,0 @@ -Copyright 2002-2008 Wichert Akkerman. All rights reserved. -Copyright 2007-2008 Simplon. All rights reserved. - -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 the University 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 REGENTS 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 REGENTS 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/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/python-ldap.txt b/licenses/python-ldap.txt deleted file mode 100644 index cece5f74cb92..000000000000 --- a/licenses/python-ldap.txt +++ /dev/null @@ -1,73 +0,0 @@ -The MIT License applies to contributions committed after July 1st, 2021, and -to all contributions by the following authors: - -* A. Karl Kornel -* Alex Willmer -* Aymeric Augustin -* Bernhard M. Wiedemann -* Bradley Baetz -* Christian Heimes -* Éloi Rivard -* Eyal Cherevatzki -* Florian Best -* Fred Thomsen -* Ivan A. Melnikov -* johnthagen -* Jonathon Reinhart -* Jon Dufresne -* Martin Basti -* Marti Raudsepp -* Miro Hrončok -* Paul Aurich -* Petr Viktorin -* Pieterjan De Potter -* Raphaël Barrois -* Robert Kuska -* Stanislav Láznička -* Tobias Bräutigam -* Tom van Dijk -* Wentao Han -* William Brown - - -------------------------------------------------------------------------------- - -MIT License - -Copyright (c) 2021 python-ldap contributors - -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. - - - - -Previous license: - -The python-ldap package is distributed under Python-style license. - -Standard disclaimer: - This software is made available by the author(s) to the public for free - and "as is". All users of this free software are solely and entirely - responsible for their own choice and use of this software for their - own purposes. By using this software, each user agrees that the - author(s) shall not be liable for damages of any kind in relation to - its use or performance. The author(s) do not warrant that this software - is fit for any purpose. - -$Id: LICENCE,v 1.1 2002/09/18 18:51:22 stroeder Exp $ diff --git a/licenses/python3-saml.txt b/licenses/python3-saml.txt deleted file mode 100644 index dbbca9c6cbce..000000000000 --- a/licenses/python3-saml.txt +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) 2010-2016 OneLogin, Inc. - -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/licenses/tacacs-plus.txt b/licenses/tacacs-plus.txt deleted file mode 100644 index 56b2d91f18ad..000000000000 --- a/licenses/tacacs-plus.txt +++ /dev/null @@ -1,24 +0,0 @@ -# Copyright (c) 2017 Ansible by Red Hat -# All Rights Reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * 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. - * Neither the name of the 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 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/tomli.txt b/licenses/tomli.txt deleted file mode 100644 index e859590f886c..000000000000 --- a/licenses/tomli.txt +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2021 Taneli Hukkinen - -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/xmlsec.txt b/licenses/xmlsec.txt deleted file mode 100644 index 89ba0cdd01d8..000000000000 --- a/licenses/xmlsec.txt +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Ryan Leckey - -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/requirements/README.md b/requirements/README.md index d29b81c9029c..4978d0bf4058 100644 --- a/requirements/README.md +++ b/requirements/README.md @@ -49,18 +49,6 @@ Make sure to delete the old tarball if it is an upgrade. Anything pinned in `*.in` files involves additional manual work in order to upgrade. Some information related to that work is outlined here. -### social-auth-app-django - -django-social keeps a list of backends in memory that it gathers -based on the value of `settings.AUTHENTICATION_BACKENDS` *at import time*: -https://github.com/python-social-auth/social-app-django/blob/c1e2795b00b753d58a81fa6a0261d8dae1d9c73d/social_django/utils.py#L13 - -Our `settings.AUTHENTICATION_BACKENDS` can *change* -dynamically as settings are changed (i.e., if somebody -configures Github OAuth2 integration), so we need to -_overwrite_ this in-memory value at the top of every request so -that we have the latest version - ### django-oauth-toolkit Versions later than 1.4.1 throw an error about id_token_id, due to the diff --git a/requirements/requirements.in b/requirements/requirements.in index 8a2bd21ae664..e840bf0f6cc8 100644 --- a/requirements/requirements.in +++ b/requirements/requirements.in @@ -13,7 +13,6 @@ Cython<3 # due to https://github.com/yaml/pyyaml/pull/702 daphne distro django==4.2.10 # CVE-2024-24680 -django-auth-ldap django-cors-headers django-crum django-extensions @@ -21,7 +20,6 @@ django-guid django-oauth-toolkit<2.0.0 # Version 2.0.0 has breaking changes that will need to be worked out before upgrading django-polymorphic django-pglocks -django-radius django-solo django-split-settings djangorestframework>=3.15.0 @@ -52,17 +50,13 @@ pyparsing==2.4.6 # Upgrading to v3 of pyparsing introduce errors on smart host python-daemon>3.0.0 python-dsv-sdk>=1.0.4 python-tss-sdk>=1.2.1 -python-ldap 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 slack-sdk -tacacs_plus==1.0 # UPGRADE BLOCKER: auth does not work with later versions twilio twisted[tls]>=23.10.0 # CVE-2023-46137 uWSGI diff --git a/requirements/requirements.txt b/requirements/requirements.txt index 2838fb6e15f1..94a462052fd2 100644 --- a/requirements/requirements.txt +++ b/requirements/requirements.txt @@ -25,10 +25,6 @@ asgiref==3.7.2 # django-cors-headers asn1==2.7.0 # via -r /awx_devel/requirements/requirements.in -async-timeout==4.0.3 - # via - # aiohttp - # redis attrs==23.2.0 # via # aiohttp @@ -43,6 +39,11 @@ autocommand==2.2.2 # via jaraco-text automat==22.10.0 # via twisted + # via -r /awx_devel/requirements/requirements_git.txt +awx-plugins.interfaces @ git+https://github.com/ansible/awx_plugins.interfaces.git + # via + # -r /awx_devel/requirements/requirements_git.txt + # awx-plugins-core azure-common==1.1.28 # via # azure-keyvault-certificates @@ -56,9 +57,13 @@ azure-core==1.30.0 # azure-keyvault-secrets # msrest azure-identity==1.15.0 - # via -r /awx_devel/requirements/requirements.in + # via + # -r /awx_devel/requirements/requirements.in + # awx-plugins-core azure-keyvault==4.2.0 - # via -r /awx_devel/requirements/requirements.in + # via + # -r /awx_devel/requirements/requirements.in + # awx-plugins-core azure-keyvault-certificates==4.7.0 # via azure-keyvault azure-keyvault-keys==4.8.0 @@ -66,7 +71,9 @@ azure-keyvault-keys==4.8.0 azure-keyvault-secrets==4.7.0 # via azure-keyvault boto3==1.34.47 - # via -r /awx_devel/requirements/requirements.in + # via + # -r /awx_devel/requirements/requirements.in + # awx-plugins-core botocore==1.34.47 # via # -r /awx_devel/requirements/requirements.in @@ -106,17 +113,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 @@ -129,7 +131,6 @@ django==4.2.10 # -r /awx_devel/requirements/requirements.in # channels # django-ansible-base - # django-auth-ldap # django-cors-headers # django-crum # django-extensions @@ -138,10 +139,7 @@ django==4.2.10 # django-polymorphic # django-solo # djangorestframework - # social-auth-app-django # via -r /awx_devel/requirements/requirements_git.txt -django-auth-ldap==4.6.0 - # via -r /awx_devel/requirements/requirements.in django-cors-headers==4.3.1 # via -r /awx_devel/requirements/requirements.in django-crum==0.7.9 @@ -158,8 +156,6 @@ django-pglocks==1.0.4 # via -r /awx_devel/requirements/requirements.in django-polymorphic==3.1.0 # via -r /awx_devel/requirements/requirements.in -django-radius==1.5.1 - # via -r /awx_devel/requirements/requirements.in django-solo==2.2.0 # via -r /awx_devel/requirements/requirements.in django-split-settings==1.3.2 @@ -174,8 +170,6 @@ djangorestframework-yaml==2.0.0 # via -r /awx_devel/requirements/requirements.in docutils==0.20.1 # via python-daemon -ecdsa==0.18.0 - # via python-jose enum-compat==0.0.3 # via asn1 filelock==3.13.1 @@ -211,10 +205,7 @@ idna==3.6 # twisted # yarl importlib-metadata==6.2.1 - # via - # ansible-runner - # markdown - # opentelemetry-api + # via opentelemetry-api incremental==22.10.0 # via twisted inflect==7.0.0 @@ -229,7 +220,6 @@ isodate==0.6.1 # azure-keyvault-keys # azure-keyvault-secrets # msrest - # python3-saml jaraco-collections==5.0.0 # via irc jaraco-context==4.3.0 @@ -265,10 +255,6 @@ kubernetes==29.0.0 # via openshift lockfile==0.12.2 # via python-daemon -lxml==4.9.4 - # via - # python3-saml - # xmlsec markdown==3.5.2 # via -r /awx_devel/requirements/requirements.in markupsafe==2.1.5 @@ -293,19 +279,18 @@ msgpack==1.0.5 msrest==0.7.1 # via msrestazure msrestazure==0.6.4 - # via -r /awx_devel/requirements/requirements.in + # via + # -r /awx_devel/requirements/requirements.in + # awx-plugins-core multidict==6.0.5 # via # aiohttp # yarl -netaddr==1.2.1 - # via pyrad oauthlib==3.2.2 # via # 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 @@ -372,14 +357,11 @@ ptyprocess==0.7.0 pyasn1==0.5.1 # via # pyasn1-modules - # python-jose - # python-ldap # rsa # service-identity pyasn1-modules==0.3.0 # via # google-auth - # python-ldap # service-identity pycparser==2.21 # via cffi @@ -394,7 +376,6 @@ pyjwt[crypto]==2.8.0 # adal # django-ansible-base # msal - # social-auth-core # twilio pyopenssl==24.0.0 # via @@ -402,8 +383,6 @@ pyopenssl==24.0.0 # twisted pyparsing==2.4.6 # via -r /awx_devel/requirements/requirements.in -pyrad==2.4 - # via django-radius python-daemon==3.0.1 # via # -r /awx_devel/requirements/requirements.in @@ -415,20 +394,15 @@ python-dateutil==2.8.2 # kubernetes # receptorctl python-dsv-sdk==1.0.4 - # via -r /awx_devel/requirements/requirements.in -python-jose==3.3.0 - # via social-auth-core -python-ldap==3.4.4 # via # -r /awx_devel/requirements/requirements.in - # django-auth-ldap + # awx-plugins-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 + # via + # -r /awx_devel/requirements/requirements.in + # awx-plugins-core pytz==2024.1 # via # irc @@ -437,6 +411,7 @@ pyyaml==6.0.1 # via # -r /awx_devel/requirements/requirements.in # ansible-runner + # awx-plugins-core # djangorestframework-yaml # kubernetes # receptorctl @@ -456,6 +431,7 @@ requests==2.31.0 # via # -r /awx_devel/requirements/requirements.in # adal + # awx-plugins-core # azure-core # django-ansible-base # django-oauth-toolkit @@ -466,21 +442,17 @@ 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 # referencing rsa==4.9 - # via - # google-auth - # python-jose + # via google-auth s3transfer==0.10.0 # via boto3 semantic-version==2.10.0 @@ -496,40 +468,24 @@ six==1.16.0 # automat # azure-core # django-pglocks - # ecdsa # isodate # kubernetes # msrestazure # openshift # pygerduty - # pyrad # python-dateutil - # tacacs-plus 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 # django -tacacs-plus==1.0 - # via -r /awx_devel/requirements/requirements.in tempora==5.5.1 # via # irc # jaraco-logging -tomli==2.0.1 - # via - # maturin - # setuptools-rust - # setuptools-scm twilio==8.13.0 # via -r /awx_devel/requirements/requirements.in twisted[tls]==23.10.0 @@ -540,7 +496,6 @@ txaio==23.1.1 # via autobahn typing-extensions==4.9.0 # via - # asgiref # azure-core # azure-keyvault-certificates # azure-keyvault-keys @@ -570,8 +525,6 @@ wrapt==1.16.0 # via # deprecated # opentelemetry-instrumentation -xmlsec==1.3.13 - # via python3-saml yarl==1.9.4 # via aiohttp zipp==3.17.0 diff --git a/requirements/requirements_dev.txt b/requirements/requirements_dev.txt index 0d88a477fed5..7dfc0f67a942 100644 --- a/requirements/requirements_dev.txt +++ b/requirements/requirements_dev.txt @@ -19,7 +19,6 @@ logutils jupyter # matplotlib - Caused issues when bumping to setuptools 58 backports.tempfile # support in unit tests for py32+ tempfile.TemporaryDirectory -git+https://github.com/artefactual-labs/mockldap.git@master#egg=mockldap gprof2dot atomicwrites flake8 diff --git a/requirements/requirements_git.txt b/requirements/requirements_git.txt index 24ccccfa1c2d..f5aaccb8e3bd 100644 --- a/requirements/requirements_git.txt +++ b/requirements/requirements_git.txt @@ -1,7 +1,6 @@ git+https://github.com/ansible/system-certifi.git@devel#egg=certifi # Remove pbr from requirements.in when moving ansible-runner to requirements.in git+https://github.com/ansible/ansible-runner.git@devel#egg=ansible-runner -git+https://github.com/ansible/python3-saml.git@devel#egg=python3-saml django-ansible-base @ git+https://github.com/ansible/django-ansible-base@devel#egg=django-ansible-base[rest_filters,jwt_consumer,resource_registry,rbac] awx-plugins-core @ git+https://git@github.com/ansible/awx-plugins.git@devel#egg=awx-plugins-core awx_plugins.interfaces @ git+https://github.com/ansible/awx_plugins.interfaces.git diff --git a/requirements/updater.sh b/requirements/updater.sh index 4b16d8db3879..92b466282d6d 100755 --- a/requirements/updater.sh +++ b/requirements/updater.sh @@ -16,7 +16,7 @@ _cleanup() { generate_requirements() { venv="`pwd`/venv" echo $venv - /usr/bin/python3 -m venv "${venv}" + /usr/bin/python3.11 -m venv "${venv}" # shellcheck disable=SC1090 source ${venv}/bin/activate diff --git a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 index dbfc821e89fe..0ab08ca6d108 100644 --- a/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 +++ b/tools/ansible/roles/dockerfile/templates/Dockerfile.j2 @@ -44,7 +44,6 @@ RUN dnf -y update && dnf install -y 'dnf-command(config-manager)' && \ libtool-ltdl-devel \ make \ nss \ - openldap-devel \ patch \ postgresql \ postgresql-devel \ @@ -127,7 +126,6 @@ RUN dnf -y update && dnf install -y 'dnf-command(config-manager)' && \ glibc-langpack-en \ krb5-workstation \ nginx \ - "openldap >= 2.6.2-3" \ postgresql \ python3.11 \ "python3.11-devel" \ diff --git a/tools/docker-compose/README.md b/tools/docker-compose/README.md index 34db342021ff..6b4014a699da 100644 --- a/tools/docker-compose/README.md +++ b/tools/docker-compose/README.md @@ -193,7 +193,7 @@ The first time you start the environment, database migrations need to run in ord ```bash awx_1 | Operations to perform: awx_1 | Synchronize unmigrated apps: solo, api, staticfiles, debug_toolbar, messages, channels, django_extensions, ui, rest_framework, polymorphic -awx_1 | Apply all migrations: sso, taggit, sessions, sites, kombu_transport_django, social_auth, contenttypes, auth, conf, main +awx_1 | Apply all migrations: sso, taggit, sessions, sites, kombu_transport_django, contenttypes, auth, conf, main awx_1 | Synchronizing apps without migrations: awx_1 | Creating tables... awx_1 | Running deferred SQL... @@ -271,10 +271,7 @@ $ make docker-compose - [Using Logstash](./docs/logstash.md) - [Start a Cluster](#start-a-cluster) - [Start with Minikube](#start-with-minikube) -- [SAML and OIDC Integration](#saml-and-oidc-integration) -- [OpenLDAP Integration](#openldap-integration) - [Splunk Integration](#splunk-integration) -- [tacacs+ Integration](#tacacs+-integration) ### Start a Shell @@ -354,123 +351,6 @@ If you want to clean all things once your are done, you can do: (host)$ make docker-compose-container-group-clean ``` -### SAML and OIDC Integration -Keycloak can be used as both a SAML and OIDC provider and can be used to test AWX social auth. This section describes how to build a reference Keycloak instance and plumb it with AWX for testing purposes. - -First, be sure that you have the awx.awx collection installed by running `make install_collection`. -Next, make sure you have your containers running by running `make docker-compose`. - -Note: The following instructions assume we are using the built-in postgres database container. If you are not using the internal database you can use this guide as a reference, updating the database fields as required for your connection. - -We are now ready to run two one time commands to build and pre-populate the Keycloak database. - -The first one time command will be creating a Keycloak database in your postgres database by running: -```bash -docker exec tools_postgres_1 /usr/bin/psql -U postgres --command 'CREATE DATABASE keycloak WITH OWNER=awx encoding "UTF8";' -``` - -After running this command the following message should appear and you should be returned to your prompt: -```base -CREATE DATABASE -``` - -The second one time command will be to start a Keycloak container to build our admin user; be sure to set pg_username and pg_password to work for you installation. Note: the command below set the username as admin with a password of admin, you can change this if you want. Also, if you are using your own container or have changed the pg_username please update the command accordingly. -```bash -PG_PASSWORD=`cat tools/docker-compose/_sources/secrets/pg_password.yml | cut -f 2 -d \'` -docker run --rm -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin --net=sources_awx \ - -e DB_VENDOR=postgres -e DB_ADDR=postgres -e DB_DATABASE=keycloak -e DB_USER=awx -e DB_PASSWORD=${PG_PASSWORD} \ - quay.io/keycloak/keycloak:15.0.2 -``` - -Once you see a message like: `WFLYSRV0051: Admin console listening on http://127.0.0.1:9990` you can stop the container. - -Now that we have performed the one time setup anytime you want to run a Keycloak instance alongside AWX we can start docker-compose with the KEYCLOAK option to get a Keycloak instance with the command: -```bash -KEYCLOAK=true make docker-compose -``` - -Go ahead and stop your existing docker-compose run and restart with Keycloak before proceeding to the next steps. - -Once the containers come up a new port (8443) should be exposed and the Keycloak interface should be running on that port. Connect to this through a url like `https://localhost:8443` to confirm that Keycloak has stared. If you wanted to login and look at Keycloak itself you could select the "Administration console" link and log into the UI the username/password set in the previous `docker run` command. For more information about Keycloak and links to their documentation see their project at https://github.com/keycloak/keycloak. - -Now we are ready to configure and plumb Keycloak with AWX. To do this we have provided a playbook which will: -* Create a certificate for SAML data exchange between Keycloak and AWX. -* Create a realm in Keycloak with a client for AWX via SAML and OIDC and 3 users. -* Backup and configure the SMAL and OIDC adapter in AWX. NOTE: the private key of any existing SAML or OIDC adapters can not be backed up through the API, you need a DB backup to recover this. - -Before we can run the playbook we need to understand that SAML works by sending redirects between AWX and Keycloak through the browser. Because of this we have to tell both AWX and Keycloak how they will construct the redirect URLs. On the Keycloak side, this is done within the realm configuration and on the AWX side its done through the SAML settings. The playbook requires a variable called `container_reference` to be set. The container_reference variable needs to be how your browser will be able to talk to the running containers. Here are some examples of how to choose a proper container_reference. -* If you develop on a mac which runs a Fedora VM which has AWX running within that and the browser you use to access AWX runs on the mac. The VM with the container has its own IP that is mapped to a name like `tower.home.net`. In this scenario your "container_reference" could be either the IP of the VM or the tower.home.net friendly name. -* If you are on a Fedora work station running AWX and also using a browser on your workstation you could use localhost, your work stations IP or hostname as the container_reference. - -In addition, OIDC works similar but slightly differently. OIDC has browser redirection but OIDC will also communicate from the AWX docker instance to the Keycloak docker instance directly. Any hostnames you might have are likely not propagated down into the AWX container. So we need a method for both the browser and AWX container to talk to Keycloak. For this we will likely use your machines IP address. This can be passed in as a variable called `oidc_reference`. If unset this will default to container_reference which may be viable for some configurations. - -In addition to container_reference, there are some additional variables which you can override if you need/choose to do so. Here are their names and default values: -```yaml - keycloak_user: admin - keycloak_pass: admin - cert_subject: "/C=US/ST=NC/L=Durham/O=awx/CN=" -``` - -* keycloak_(user|pass) need to change if you modified the user when starting the initial container above. -* cert_subject will be the subject line of the certificate shared between AWX and keycloak you can change this if you like or just use the defaults. - -To override any of the variables above you can add more `-e` arguments to the playbook run below. For example, if you simply need to change the `keycloak_pass` add the argument `-e keycloak_pass=my_secret_pass` to the following ansible-playbook command. - -In addition, you may need to override the username or password to get into your AWX instance. We log into AWX in order to read and write the SAML and OIDC settings. This can be done in several ways because we are using the awx.awx collection. The easiest way is to set environment variables such as `CONTROLLER_USERNAME`. See the awx.awx documentation for more information on setting environment variables. In the example provided below we are showing an example of specifying a username/password for authentication. - -Now that we have all of our variables covered we can run the playbook like: -```bash -export CONTROLLER_USERNAME= -export CONTROLLER_PASSWORD= -ansible-playbook tools/docker-compose/ansible/plumb_keycloak.yml -e container_reference= -e oidc_reference= -``` - -Once the playbook is done running both SAML and OIDC should now be setup in your development environment. This realm has three users with the following username/passwords: -1. awx_unpriv:unpriv123 -2. awx_admin:admin123 -3. awx_auditor:audit123 - -The first account is a normal user. The second account has the SMAL attribute is_superuser set in Keycloak so will be a super user in AWX if logged in through SAML. The third account has the SAML is_system_auditor attribute in Keycloak so it will be a system auditor in AWX if logged in through SAML. To log in with one of these Keycloak users go to the AWX login screen and click the small "Sign In With SAML Keycloak" button at the bottom of the login box. - -Note: The OIDC adapter performs authentication only, not authorization. So any user created in AWX will not have any permissions on it at all. - -If you Keycloak configuration is not working and you need to rerun the playbook to try a different `container_reference` or `oidc_reference` you can log into the Keycloak admin console on port 8443 and select the AWX realm in the upper left drop down. Then make sure you are on "Ream Settings" in the Configure menu option and click the trash can next to AWX in the main page window pane. This will completely remove the AWX ream (which has both SAML and OIDC settings) enabling you to re-run the plumb playbook. - -### OpenLDAP Integration - -OpenLDAP is an LDAP provider that can be used to test AWX with LDAP integration. This section describes how to build a reference OpenLDAP instance and plumb it with your AWX for testing purposes. - -First, be sure that you have the awx.awx collection installed by running `make install_collection`. - -Anytime you want to run an OpenLDAP instance alongside AWX we can start docker-compose with the LDAP option to get an LDAP instance with the command: -```bash -LDAP=true make docker-compose -``` - -Once the containers come up two new ports (389, 636) should be exposed and the LDAP server should be running on those ports. The first port (389) is non-SSL and the second port (636) is SSL enabled. - -Now we are ready to configure and plumb OpenLDAP with AWX. To do this we have provided a playbook which will: -* Backup and configure the LDAP adapter in AWX. NOTE: this will back up your existing settings but the password fields can not be backed up through the API, you need a DB backup to recover this. - -Note: The default configuration will utilize the non-tls connection. If you want to use the tls configuration you will need to work through TLS negotiation issues because the LDAP server is using a self signed certificate. - -You can run the playbook like: -```bash -export CONTROLLER_USERNAME= -export CONTROLLER_PASSWORD= -ansible-playbook tools/docker-compose/ansible/plumb_ldap.yml -``` - - -Once the playbook is done running LDAP should now be setup in your development environment. This realm has four users with the following username/passwords: -1. awx_ldap_unpriv:unpriv123 -2. awx_ldap_admin:admin123 -3. awx_ldap_auditor:audit123 -4. awx_ldap_org_admin:orgadmin123 - -The first account is a normal user. The second account will be a super user in AWX. The third account will be a system auditor in AWX. The fourth account is an org admin. All users belong to an org called "LDAP Organization". To log in with one of these users go to the AWX login screen enter the username/password. - - ### Splunk Integration Splunk is a log aggregation tool that can be used to test AWX with external logging integration. This section describes how to build a reference Splunk instance and plumb it with your AWX for testing purposes. @@ -501,30 +381,6 @@ ansible-playbook tools/docker-compose/ansible/plumb_splunk.yml Once the playbook is done running Splunk should now be setup in your development environment. You can log into the admin console (see above for username/password) and click on "Searching and Reporting" in the left hand navigation. In the search box enter `source="http:tower_logging_collections"` and click search. -### - tacacs+ Integration - -tacacs+ is an networking protocol that provides external authentication which can be used with AWX. This section describes how to build a reference tacacs+ instance and plumb it with your AWX for testing purposes. - -First, be sure that you have the awx.awx collection installed by running `make install_collection`. - -Anytime you want to run a tacacs+ instance alongside AWX we can start docker-compose with the TACACS option to get a containerized instance with the command: -```bash -TACACS=true make docker-compose -``` - -Once the containers come up a new port (49) should be exposed and the tacacs+ server should be running on those ports. - -Now we are ready to configure and plumb tacacs+ with AWX. To do this we have provided a playbook which will: -* Backup and configure the tacacsplus adapter in AWX. NOTE: this will back up your existing settings but the password fields can not be backed up through the API, you need a DB backup to recover this. - -```bash -export CONTROLLER_USERNAME= -export CONTROLLER_PASSWORD= -ansible-playbook tools/docker-compose/ansible/plumb_tacacs.yml -``` - -Once the playbook is done running tacacs+ should now be setup in your development environment. This server has the accounts listed on https://hub.docker.com/r/dchidell/docker-tacacs - ### HashiVault Integration Run a HashiVault container alongside of AWX. @@ -550,7 +406,7 @@ To create a secret connected to this vault in AWX you can run the following play ```bash export CONTROLLER_USERNAME= export CONTROLLER_PASSWORD= -ansible-playbook tools/docker-compose/ansible/plumb_vault.yml -e enable_ldap=false +ansible-playbook tools/docker-compose/ansible/plumb_vault.yml ``` This will create the following items in your AWX instance: @@ -575,53 +431,6 @@ If you have a playbook like: And run it through AWX with the credential `Credential From Vault via Token Auth` tied to it, the debug should result in `this_is_the_secret_value`. If you run it through AWX with the credential `Credential From Vault via Userpass Auth`, the debug should result in `this_is_the_userpass_secret_value`. -### HashiVault with LDAP - -If you wish to have your OpenLDAP container connected to the Vault container, you will first need to have the OpenLDAP container running alongside AWX and Vault. - - -```bash - -VAULT=true LDAP=true make docker-compose - -``` - -Similar to the above, you will need to unseal the vault before we can run the other needed playbooks. - -```bash - -ansible-playbook tools/docker-compose/ansible/unseal_vault.yml - -``` - -Now that the vault is unsealed, we can plumb the vault container now while passing true to enable_ldap extra var. - - -```bash - -export CONTROLLER_USERNAME= - -export CONTROLLER_PASSWORD= - -ansible-playbook tools/docker-compose/ansible/plumb_vault.yml -e enable_ldap=true - -``` - -This will populate your AWX instance with LDAP specific items. - -- A vault LDAP Lookup Cred tied to the LDAP `awx_ldap_vault` user called `Vault LDAP Lookup Cred` -- A credential called `Credential From HashiCorp Vault via LDAP Auth` which is of the created type using the `Vault LDAP Lookup Cred` to get the secret. - -And run it through AWX with the credential `Credential From HashiCorp Vault via LDAP Auth` tied to it, the debug should result in `this_is_the_ldap_secret_value`. - -The extremely non-obvious input is the fact that the fact prefixes "data/" unexpectedly. -This was discovered by inspecting the secret with the vault CLI, which may help with future troubleshooting. - -``` -docker exec -it -e VAULT_TOKEN= tools_vault_1 vault kv get --address=http://127.0.0.1:1234 my_engine/my_root/my_folder -``` - - ### Prometheus and Grafana integration See docs at https://github.com/ansible/awx/blob/devel/tools/grafana/README.md diff --git a/tools/docker-compose/ansible/plumb_keycloak.yml b/tools/docker-compose/ansible/plumb_keycloak.yml deleted file mode 100644 index c18e3b02a792..000000000000 --- a/tools/docker-compose/ansible/plumb_keycloak.yml +++ /dev/null @@ -1,96 +0,0 @@ ---- -- name: Plumb a keycloak instance - hosts: localhost - connection: local - gather_facts: False - vars: - private_key_file: ../_sources/keycloak.key - public_key_file: ../_sources/keycloak.cert - awx_host: "https://localhost:8043" - keycloak_realm_template: ../_sources/keycloak.awx.realm.json - keycloak_user: admin - keycloak_pass: admin - cert_subject: "/C=US/ST=NC/L=Durham/O=awx/CN=" - tasks: - - name: Generate certificates for keycloak - ansible.builtin.command: 'openssl req -new -x509 -days 365 -nodes -out {{ public_key_file }} -keyout {{ private_key_file }} -subj "{{ cert_subject }}"' - args: - creates: "{{ public_key_file }}" - - - name: Load certs, existing and new SAML settings - ansible.builtin.set_fact: - private_key: "{{ private_key_content }}" - public_key: "{{ public_key_content }}" - public_key_trimmed: "{{ public_key_content | regex_replace('-----BEGIN CERTIFICATE-----\\\\n', '') | regex_replace('\\\\n-----END CERTIFICATE-----', '') }}" - existing_saml: "{{ lookup('awx.awx.controller_api', 'settings/saml', host=awx_host, verify_ssl=false) }}" - new_saml: "{{ lookup('template', 'saml_settings.json.j2') }}" - existing_oidc: "{{ lookup('awx.awx.controller_api', 'settings/oidc', host=awx_host, verify_ssl=false) }}" - new_oidc: "{{ lookup('template', 'oidc_settings.json.j2') }}" - vars: - # We add the extra \\ in here so that when jinja is templating out the files we end up with \n in the strings. - public_key_content: "{{ lookup('file', public_key_file) | regex_replace('\n', '\\\\n') }}" - private_key_content: "{{ lookup('file', private_key_file) | regex_replace('\n', '\\\\n') }}" - - - name: Displauy existing SAML configuration - ansible.builtin.debug: - msg: - - "Here is your existing SAML configuration for reference:" - - "{{ existing_saml }}" - - "Here is your existing OIDC configuration for reference:" - - "{{ existing_oidc }}" - - - ansible.builtin.pause: - prompt: "Continuing to run this will replace your existing saml and OIDC settings (displayed above). They will all be captured except for your private key. Be sure that is backed up before continuing" - - - name: Write out the existing content - ansible.builtin.copy: - dest: "../_sources/{{ item.filename }}" - content: "{{ item.content }}" - loop: - - filename: "existing_saml_adapter_settings.json" - content: "{{ existing_saml }}" - - filename: "existing_oidc_adapter_settings.json" - content: "{{ existing_oidc }}" - - - name: Configure AWX SAML adapter - awx.awx.settings: - settings: "{{ new_saml }}" - controller_host: "{{ awx_host }}" - validate_certs: False - - - name: Configure AWX OIDC adapter - awx.awx.settings: - settings: "{{ new_oidc }}" - controller_host: "{{ awx_host }}" - validate_certs: False - - - name: Get a keycloak token - ansible.builtin.uri: - url: "https://localhost:8443/auth/realms/master/protocol/openid-connect/token" - method: POST - body_format: form-urlencoded - body: - client_id: "admin-cli" - username: "{{ keycloak_user }}" - password: "{{ keycloak_pass }}" - grant_type: "password" - validate_certs: False - register: keycloak_response - - - name: Template the AWX realm - ansible.builtin.template: - src: keycloak.awx.realm.json.j2 - dest: "{{ keycloak_realm_template }}" - - - name: Create the AWX realm - ansible.builtin.uri: - url: "https://localhost:8443/auth/admin/realms" - method: POST - body_format: json - body: "{{ lookup('file', keycloak_realm_template) }}" - validate_certs: False - headers: - Authorization: "Bearer {{ keycloak_response.json.access_token }}" - status_code: 201 - register: realm_creation - changed_when: True diff --git a/tools/docker-compose/ansible/plumb_ldap.yml b/tools/docker-compose/ansible/plumb_ldap.yml deleted file mode 100644 index 56b3dcdbabf2..000000000000 --- a/tools/docker-compose/ansible/plumb_ldap.yml +++ /dev/null @@ -1,32 +0,0 @@ ---- -- name: Plumb an ldap instance - hosts: localhost - connection: local - gather_facts: False - vars: - awx_host: "https://localhost:8043" - tasks: - - name: Load existing and new LDAP settings - ansible.builtin.set_fact: - existing_ldap: "{{ lookup('awx.awx.controller_api', 'settings/ldap', host=awx_host, verify_ssl=false) }}" - new_ldap: "{{ lookup('template', 'ldap_settings.json.j2') }}" - - - name: Display existing LDAP configuration - ansible.builtin.debug: - msg: - - "Here is your existing LDAP configuration for reference:" - - "{{ existing_ldap }}" - - - ansible.builtin.pause: - prompt: "Continuing to run this will replace your existing ldap settings (displayed above). They will all be captured. Be sure that is backed up before continuing" - - - name: Write out the existing content - ansible.builtin.copy: - dest: "../_sources/existing_ldap_adapter_settings.json" - content: "{{ existing_ldap }}" - - - name: Configure AWX LDAP adapter - awx.awx.settings: - settings: "{{ new_ldap }}" - controller_host: "{{ awx_host }}" - validate_certs: False diff --git a/tools/docker-compose/ansible/plumb_splunk.yml b/tools/docker-compose/ansible/plumb_splunk.yml index 9ab04edceb70..031ed820c847 100644 --- a/tools/docker-compose/ansible/plumb_splunk.yml +++ b/tools/docker-compose/ansible/plumb_splunk.yml @@ -30,12 +30,6 @@ existing_logging: "{{ lookup('awx.awx.controller_api', 'settings/logging', host=awx_host, verify_ssl=false) }}" new_logging: "{{ lookup('template', 'logging.json.j2') }}" - - name: Display existing Logging configuration - ansible.builtin.debug: - msg: - - "Here is your existing SAML configuration for reference:" - - "{{ existing_logging }}" - - pause: ansible.builtin.prompt: "Continuing to run this will replace your existing logging settings (displayed above). They will all be captured except for your connection password. Be sure that is backed up before continuing" diff --git a/tools/docker-compose/ansible/plumb_tacacs.yml b/tools/docker-compose/ansible/plumb_tacacs.yml deleted file mode 100644 index b18a72284a3e..000000000000 --- a/tools/docker-compose/ansible/plumb_tacacs.yml +++ /dev/null @@ -1,32 +0,0 @@ ---- -- name: Plumb a tacacs+ instance - hosts: localhost - connection: local - gather_facts: False - vars: - awx_host: "https://localhost:8043" - tasks: - - name: Load existing and new tacacs+ settings - ansible.builtin.set_fact: - existing_tacacs: "{{ lookup('awx.awx.controller_api', 'settings/tacacsplus', host=awx_host, verify_ssl=false) }}" - new_tacacs: "{{ lookup('template', 'tacacsplus_settings.json.j2') }}" - - - name: Display existing tacacs+ configuration - ansible.builtin.debug: - msg: - - "Here is your existing tacacsplus configuration for reference:" - - "{{ existing_tacacs }}" - - - ansible.builtin.pause: - prompt: "Continuing to run this will replace your existing tacacs settings (displayed above). They will all be captured. Be sure that is backed up before continuing" - - - name: Write out the existing content - ansible.builtin.copy: - dest: "../_sources/existing_tacacsplus_adapter_settings.json" - content: "{{ existing_tacacs }}" - - - name: Configure AWX tacacs+ adapter - awx.awx.settings: - settings: "{{ new_tacacs }}" - controller_host: "{{ awx_host }}" - validate_certs: False diff --git a/tools/docker-compose/ansible/roles/sources/defaults/main.yml b/tools/docker-compose/ansible/roles/sources/defaults/main.yml index 669f2cfe2002..4e8fbd487e41 100644 --- a/tools/docker-compose/ansible/roles/sources/defaults/main.yml +++ b/tools/docker-compose/ansible/roles/sources/defaults/main.yml @@ -20,18 +20,6 @@ work_sign_key_dir: '../_sources/receptor' work_sign_private_keyfile: "{{ work_sign_key_dir }}/work_private_key.pem" work_sign_public_keyfile: "{{ work_sign_key_dir }}/work_public_key.pem" -# SSO variables -enable_keycloak: false - -enable_ldap: false -ldap_public_key_file_name: 'ldap.cert' -ldap_private_key_file_name: 'ldap.key' -ldap_cert_dir: '{{ sources_dest }}/ldap_certs' -ldap_diff_dir: '{{ sources_dest }}/ldap_diffs' -ldap_public_key_file: '{{ ldap_cert_dir }}/{{ ldap_public_key_file_name }}' -ldap_private_key_file: '{{ ldap_cert_dir }}/{{ ldap_private_key_file_name }}' -ldap_cert_subject: "/C=US/ST=NC/L=Durham/O=awx/CN=" - # Hashicorp Vault enable_vault: false vault_tls: false diff --git a/tools/docker-compose/ansible/roles/sources/tasks/ldap.yml b/tools/docker-compose/ansible/roles/sources/tasks/ldap.yml deleted file mode 100644 index 1e0185a0885f..000000000000 --- a/tools/docker-compose/ansible/roles/sources/tasks/ldap.yml +++ /dev/null @@ -1,21 +0,0 @@ ---- -- name: Create LDAP cert directory - file: - path: "{{ item }}" - state: directory - loop: - - "{{ ldap_cert_dir }}" - - "{{ ldap_diff_dir }}" - -- name: include vault vars - include_vars: "{{ hashivault_vars_file }}" - -- name: General LDAP cert - command: 'openssl req -new -x509 -days 365 -nodes -out {{ ldap_public_key_file }} -keyout {{ ldap_private_key_file }} -subj "{{ ldap_cert_subject }}"' - args: - creates: "{{ ldap_public_key_file }}" - -- name: Copy ldap.diff - ansible.builtin.template: - src: "ldap.ldif.j2" - dest: "{{ ldap_diff_dir }}/ldap.ldif" diff --git a/tools/docker-compose/ansible/roles/sources/tasks/main.yml b/tools/docker-compose/ansible/roles/sources/tasks/main.yml index 0f1149053ebd..5637f6254601 100644 --- a/tools/docker-compose/ansible/roles/sources/tasks/main.yml +++ b/tools/docker-compose/ansible/roles/sources/tasks/main.yml @@ -97,10 +97,6 @@ creates: "{{ work_sign_public_keyfile }}" when: sign_work | bool -- name: Include LDAP tasks if enabled - include_tasks: ldap.yml - when: enable_ldap | bool - - name: Include vault TLS tasks if enabled include_tasks: vault_tls.yml when: enable_vault | bool diff --git a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 index a56f861fda57..d202707ebf83 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/docker-compose.yml.j2 @@ -127,50 +127,6 @@ services: - "awx_{{ container_postfix }}" {% endfor %} {% endif %} -{% if enable_keycloak|bool %} - keycloak: - image: quay.io/keycloak/keycloak:15.0.2 - container_name: tools_keycloak_1 - hostname: keycloak - user: "{{ ansible_user_uid }}" - networks: - - awx - ports: - - "8443:8443" - environment: - DB_VENDOR: postgres - DB_ADDR: postgres - DB_DATABASE: keycloak - DB_USER: {{ pg_username }} - DB_PASSWORD: {{ pg_password }} - depends_on: - - postgres -{% endif %} -{% if enable_ldap|bool %} - ldap: - image: bitnami/openldap:2 - container_name: tools_ldap_1 - hostname: ldap - user: "{{ ansible_user_uid }}" - networks: - - awx - ports: - - "389:1389" - - "636:1636" - environment: - LDAP_ADMIN_USERNAME: admin - LDAP_ADMIN_PASSWORD: admin - LDAP_CUSTOM_LDIF_DIR: /opt/bitnami/openldap/ldiffs - LDAP_ENABLE_TLS: "yes" - LDAP_LDAPS_PORT_NUMBER: 1636 - LDAP_TLS_CERT_FILE: /opt/bitnami/openldap/certs/{{ ldap_public_key_file_name }} - LDAP_TLS_CA_FILE: /opt/bitnami/openldap/certs/{{ ldap_public_key_file_name }} - LDAP_TLS_KEY_FILE: /opt/bitnami/openldap/certs/{{ ldap_private_key_file_name }} - volumes: - - 'openldap_data:/bitnami/openldap' - - '../../docker-compose/_sources/ldap_certs:/opt/bitnami/openldap/certs' - - '../../docker-compose/_sources/ldap_diffs:/opt/bitnami/openldap/ldiffs' -{% endif %} {% if enable_splunk|bool %} splunk: image: splunk/splunk:latest @@ -213,14 +169,6 @@ services: - "grafana_storage:/var/lib/grafana:rw" depends_on: - prometheus -{% endif %} -{% if enable_tacacs|bool %} - tacacs: - image: dchidell/docker-tacacs - container_name: tools_tacacs_1 - hostname: tacacs - ports: - - "49:49" {% endif %} # A useful container that simply passes through log messages to the console # helpful for testing awx/tower logging @@ -376,11 +324,6 @@ volumes: redis_socket_{{ container_postfix }}: name: tools_redis_socket_{{ container_postfix }} {% endfor -%} -{% if enable_ldap|bool %} - openldap_data: - name: tools_ldap_1 - driver: local -{% endif %} {% if enable_vault|bool %} hashicorp_vault_data: name: tools_vault_1 diff --git a/tools/docker-compose/ansible/roles/sources/templates/ldap.ldif.j2 b/tools/docker-compose/ansible/roles/sources/templates/ldap.ldif.j2 deleted file mode 100644 index 9deaf836cd61..000000000000 --- a/tools/docker-compose/ansible/roles/sources/templates/ldap.ldif.j2 +++ /dev/null @@ -1,99 +0,0 @@ -dn: dc=example,dc=org -objectClass: dcObject -objectClass: organization -dc: example -o: example - -dn: ou=users,dc=example,dc=org -ou: users -objectClass: organizationalUnit - -dn: cn=awx_ldap_admin,ou=users,dc=example,dc=org -mail: admin@example.org -sn: LdapAdmin -cn: awx_ldap_admin -objectClass: top -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -userPassword: admin123 -givenName: awx - -dn: cn=awx_ldap_auditor,ou=users,dc=example,dc=org -mail: auditor@example.org -sn: LdapAuditor -cn: awx_ldap_auditor -objectClass: top -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -userPassword: audit123 -givenName: awx - -dn: cn=awx_ldap_unpriv,ou=users,dc=example,dc=org -mail: unpriv@example.org -sn: LdapUnpriv -cn: awx_ldap_unpriv -objectClass: top -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -givenName: awx -userPassword: unpriv123 - -dn: ou=groups,dc=example,dc=org -ou: groups -objectClass: top -objectClass: organizationalUnit - -dn: cn=awx_users,ou=groups,dc=example,dc=org -cn: awx_users -objectClass: top -objectClass: groupOfNames -member: cn=awx_ldap_admin,ou=users,dc=example,dc=org -member: cn=awx_ldap_auditor,ou=users,dc=example,dc=org -member: cn=awx_ldap_unpriv,ou=users,dc=example,dc=org -member: cn=awx_ldap_org_admin,ou=users,dc=example,dc=org - -dn: cn=awx_admins,ou=groups,dc=example,dc=org -cn: awx_admins -objectClass: top -objectClass: groupOfNames -member: cn=awx_ldap_admin,ou=users,dc=example,dc=org - -dn: cn=awx_auditors,ou=groups,dc=example,dc=org -cn: awx_auditors -objectClass: top -objectClass: groupOfNames -member: cn=awx_ldap_auditor,ou=users,dc=example,dc=org - -dn: cn=awx_ldap_org_admin,ou=users,dc=example,dc=org -mail: org.admin@example.org -sn: LdapOrgAdmin -cn: awx_ldap_org_admin -objectClass: top -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -givenName: awx -userPassword: orgadmin123 - -dn: cn=awx_org_admins,ou=groups,dc=example,dc=org -cn: awx_org_admins -objectClass: top -objectClass: groupOfNames -member: cn=awx_ldap_org_admin,ou=users,dc=example,dc=org - -{% if enable_ldap|bool and enable_vault|bool %} -dn: cn={{ vault_ldap_username }},ou=users,dc=example,dc=org -changetype: add -mail: vault@example.org -sn: LdapVaultAdmin -cn: {{ vault_ldap_username }} -objectClass: top -objectClass: person -objectClass: organizationalPerson -objectClass: inetOrgPerson -userPassword: {{ vault_ldap_password }} -givenName: awx -{% endif %} diff --git a/tools/docker-compose/ansible/roles/sources/templates/local_settings.py.j2 b/tools/docker-compose/ansible/roles/sources/templates/local_settings.py.j2 index 42a5d56366f4..1be38f43e28a 100644 --- a/tools/docker-compose/ansible/roles/sources/templates/local_settings.py.j2 +++ b/tools/docker-compose/ansible/roles/sources/templates/local_settings.py.j2 @@ -42,10 +42,6 @@ OPTIONAL_API_URLPATTERN_PREFIX = '{{ api_urlpattern_prefix }}' # Enable the following line to turn on database settings logging. # LOGGING['loggers']['awx.conf']['level'] = 'DEBUG' -# Enable the following lines to turn on LDAP auth logging. -# LOGGING['loggers']['django_auth_ldap']['handlers'] = ['console'] -# LOGGING['loggers']['django_auth_ldap']['level'] = 'DEBUG' - {% if enable_otel|bool %} LOGGING['handlers']['otel'] |= { 'class': 'awx.main.utils.handlers.OTLPHandler', diff --git a/tools/docker-compose/ansible/roles/vault/defaults/main.yml b/tools/docker-compose/ansible/roles/vault/defaults/main.yml index 58e0153b7f1e..36feeb28684a 100644 --- a/tools/docker-compose/ansible/roles/vault/defaults/main.yml +++ b/tools/docker-compose/ansible/roles/vault/defaults/main.yml @@ -5,8 +5,5 @@ vault_cert_dir: "{{ sources_dest }}/vault_certs" vault_server_cert: "{{ vault_cert_dir }}/server.crt" vault_client_cert: "{{ vault_cert_dir }}/client.crt" vault_client_key: "{{ vault_cert_dir }}/client.key" -ldap_ldif: "{{ sources_dest }}/ldap.ldifs/ldap.ldif" -vault_ldap_username: "awx_ldap_vault" -vault_ldap_password: "vault123" vault_userpass_username: "awx_userpass_admin" vault_userpass_password: "userpass123" diff --git a/tools/docker-compose/ansible/roles/vault/tasks/initialize.yml b/tools/docker-compose/ansible/roles/vault/tasks/initialize.yml index 8c7230c6d146..ac7d60b8ecfd 100644 --- a/tools/docker-compose/ansible/roles/vault/tasks/initialize.yml +++ b/tools/docker-compose/ansible/roles/vault/tasks/initialize.yml @@ -92,74 +92,6 @@ validate_certs: false token: "{{ Initial_Root_Token }}" - - name: Configure the vault ldap auth - block: - - name: Create ldap auth mount - flowerysong.hvault.write: - path: "sys/auth/ldap" - vault_addr: "{{ vault_addr_from_host }}" - validate_certs: false - token: "{{ Initial_Root_Token }}" - data: - type: "ldap" - register: vault_auth_ldap - changed_when: vault_auth_ldap.result.errors | default([]) | length == 0 - failed_when: - - vault_auth_ldap.result.errors | default([]) | length > 0 - - "'path is already in use at ldap/' not in vault_auth_ldap.result.errors | default([])" - - - name: Create ldap engine - flowerysong.hvault.engine: - path: "ldap_engine" - type: "kv" - vault_addr: "{{ vault_addr_from_host }}" - validate_certs: false - token: "{{ Initial_Root_Token }}" - - - name: Create a ldap secret - flowerysong.hvault.kv: - mount_point: "ldap_engine/ldaps_root" - key: "ldap_secret" - value: - my_key: "this_is_the_ldap_secret_value" - vault_addr: "{{ vault_addr_from_host }}" - validate_certs: false - token: "{{ Initial_Root_Token }}" - - - name: Configure ldap auth - flowerysong.hvault.ldap_config: - vault_addr: "{{ vault_addr_from_host }}" - validate_certs: false - token: "{{ Initial_Root_Token }}" - url: "ldap://ldap:1389" - binddn: "cn=awx_ldap_vault,ou=users,dc=example,dc=org" - bindpass: "vault123" - userdn: "ou=users,dc=example,dc=org" - deny_null_bind: "false" - discoverdn: "true" - - - name: Create ldap access policy - flowerysong.hvault.policy: - vault_addr: "{{ vault_addr_from_host }}" - validate_certs: false - token: "{{ Initial_Root_Token }}" - name: "ldap_engine" - policy: - ldap_engine/*: [create, read, update, delete, list] - sys/mounts:/*: [create, read, update, delete, list] - sys/mounts: [read] - - - name: Add awx_ldap_vault user to auth_method - flowerysong.hvault.ldap_user: - vault_addr: "{{ vault_addr_from_host }}" - validate_certs: false - token: "{{ Initial_Root_Token }}" - state: present - name: "{{ vault_ldap_username }}" - policies: - - "ldap_engine" - when: enable_ldap | bool - - name: Create userpass engine flowerysong.hvault.engine: path: "userpass_engine" diff --git a/tools/docker-compose/ansible/roles/vault/tasks/plumb.yml b/tools/docker-compose/ansible/roles/vault/tasks/plumb.yml index 0e87daef6fa5..f3fc709b84d5 100644 --- a/tools/docker-compose/ansible/roles/vault/tasks/plumb.yml +++ b/tools/docker-compose/ansible/roles/vault/tasks/plumb.yml @@ -78,56 +78,6 @@ secret_path: "/my_root/my_folder" secret_version: "" -- name: Create a HashiCorp Vault Credential for LDAP - awx.awx.credential: - credential_type: HashiCorp Vault Secret Lookup - name: Vault LDAP Lookup Cred - organization: Default - controller_host: "{{ awx_host }}" - controller_username: admin - controller_password: "{{ admin_password }}" - validate_certs: false - inputs: - api_version: "v1" - default_auth_path: "ldap" - kubernetes_role: "" - namespace: "" - url: "{{ vault_addr_from_container }}" - username: "{{ vault_ldap_username }}" - password: "{{ vault_ldap_password }}" - register: vault_ldap_cred - when: enable_ldap | bool - -- name: Create a credential from the Vault LDAP Custom Cred Type - awx.awx.credential: - credential_type: "{{ custom_vault_cred_type.id }}" - controller_host: "{{ awx_host }}" - controller_username: admin - controller_password: "{{ admin_password }}" - validate_certs: false - name: Credential From HashiCorp Vault via LDAP Auth - inputs: {} - organization: Default - register: custom_credential_via_ldap - when: enable_ldap | bool - -- name: Use the Vault LDAP Credential the new credential - awx.awx.credential_input_source: - input_field_name: password - target_credential: "{{ custom_credential_via_ldap.id }}" - source_credential: "{{ vault_ldap_cred.id }}" - controller_host: "{{ awx_host }}" - controller_username: admin - controller_password: "{{ admin_password }}" - validate_certs: false - metadata: - auth_path: "" - secret_backend: "ldap_engine" - secret_key: "my_key" - secret_path: "ldaps_root/ldap_secret" - secret_version: "" - when: enable_ldap | bool - - name: Create a HashiCorp Vault Credential for UserPass awx.awx.credential: credential_type: HashiCorp Vault Secret Lookup diff --git a/tools/docker-compose/ansible/templates/keycloak.awx.realm.json.j2 b/tools/docker-compose/ansible/templates/keycloak.awx.realm.json.j2 deleted file mode 100644 index dbe8144f84b1..000000000000 --- a/tools/docker-compose/ansible/templates/keycloak.awx.realm.json.j2 +++ /dev/null @@ -1,1873 +0,0 @@ -{# - This template is an export from Keycloak. - See https://github.com/keycloak/keycloak-documentation/blob/main/server_admin/topics/export-import.adoc for instructions on how to run the export. - Once you have the export you want to variablize the public cert, private cert, and the endpoints. - The endpoints should be replaced with either the variable {{ container_reference }} or {{ oidc_reference }} - Some of the keys have \n's in there and some references do not. - The ones with the \n can be variablized by {{ private_key }} and {{ public_key }}. - The public key in the setting `saml.signing.certificate` should be replaced with {{ public_key_trimmed }} - - Additionally, when you run the export from Keycloak it will export everything in the realm, this is not needed for import. - Look at the fields in the JSON and remove any additional fields that comes from your export that you don't need. -#} -{ - "id": "AWX Realm", - "realm": "awx", - "displayName": "AWX Realm", - "notBefore": 0, - "defaultSignatureAlgorithm": "RS256", - "revokeRefreshToken": false, - "refreshTokenMaxReuse": 0, - "accessTokenLifespan": 300, - "accessTokenLifespanForImplicitFlow": 900, - "ssoSessionIdleTimeout": 1800, - "ssoSessionMaxLifespan": 36000, - "ssoSessionIdleTimeoutRememberMe": 0, - "ssoSessionMaxLifespanRememberMe": 0, - "offlineSessionIdleTimeout": 2592000, - "offlineSessionMaxLifespanEnabled": false, - "offlineSessionMaxLifespan": 5184000, - "clientSessionIdleTimeout": 0, - "clientSessionMaxLifespan": 0, - "clientOfflineSessionIdleTimeout": 0, - "clientOfflineSessionMaxLifespan": 0, - "accessCodeLifespan": 60, - "accessCodeLifespanUserAction": 300, - "accessCodeLifespanLogin": 1800, - "actionTokenGeneratedByAdminLifespan": 43200, - "actionTokenGeneratedByUserLifespan": 300, - "oauth2DeviceCodeLifespan": 600, - "oauth2DevicePollingInterval": 5, - "enabled": true, - "sslRequired": "external", - "registrationAllowed": false, - "registrationEmailAsUsername": false, - "rememberMe": false, - "verifyEmail": false, - "loginWithEmailAllowed": true, - "duplicateEmailsAllowed": false, - "resetPasswordAllowed": false, - "editUsernameAllowed": false, - "bruteForceProtected": false, - "permanentLockout": false, - "maxFailureWaitSeconds": 900, - "minimumQuickLoginWaitSeconds": 60, - "waitIncrementSeconds": 60, - "quickLoginCheckMilliSeconds": 1000, - "maxDeltaTimeSeconds": 43200, - "failureFactor": 30, - "roles": { - "realm": [ - { - "id": "b7f7a161-2a18-4b95-b58b-99448b2fc50e", - "name": "default-roles-awx realm", - "description": "${role_default-roles}", - "composite": true, - "composites": { - "realm": [ - "offline_access", - "uma_authorization" - ], - "client": { - "account": [ - "view-profile", - "manage-account" - ] - } - }, - "clientRole": false, - "containerId": "AWX Realm", - "attributes": {} - }, - { - "id": "ea2c2864-93b0-4022-9ef1-202bc2f9c87a", - "name": "uma_authorization", - "description": "${role_uma_authorization}", - "composite": false, - "clientRole": false, - "containerId": "AWX Realm", - "attributes": {} - }, - { - "id": "3764c3ca-d706-424e-8802-65be0d2e060d", - "name": "offline_access", - "description": "${role_offline-access}", - "composite": false, - "clientRole": false, - "containerId": "AWX Realm", - "attributes": {} - } - ], - "client": { - "{{ container_reference }}:8043": [], - "awx_oidc_client": [] - } - }, - "groups": [], - "defaultRole": { - "id": "b7f7a161-2a18-4b95-b58b-99448b2fc50e", - "name": "default-roles-awx realm", - "description": "${role_default-roles}", - "composite": true, - "clientRole": false, - "containerId": "AWX Realm" - }, - "requiredCredentials": [ - "password" - ], - "otpPolicyType": "totp", - "otpPolicyAlgorithm": "HmacSHA1", - "otpPolicyInitialCounter": 0, - "otpPolicyDigits": 6, - "otpPolicyLookAheadWindow": 1, - "otpPolicyPeriod": 30, - "otpSupportedApplications": [ - "FreeOTP", - "Google Authenticator" - ], - "webAuthnPolicyRpEntityName": "keycloak", - "webAuthnPolicySignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyRpId": "", - "webAuthnPolicyAttestationConveyancePreference": "not specified", - "webAuthnPolicyAuthenticatorAttachment": "not specified", - "webAuthnPolicyRequireResidentKey": "not specified", - "webAuthnPolicyUserVerificationRequirement": "not specified", - "webAuthnPolicyCreateTimeout": 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyAcceptableAaguids": [], - "webAuthnPolicyPasswordlessRpEntityName": "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms": [ - "ES256" - ], - "webAuthnPolicyPasswordlessRpId": "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference": "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment": "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey": "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement": "not specified", - "webAuthnPolicyPasswordlessCreateTimeout": 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister": false, - "webAuthnPolicyPasswordlessAcceptableAaguids": [], - "clients": [ - { - "id": "0cb841a2-0a31-4976-86fa-a765f09426da", - "clientId": "{{ container_reference }}:8043", - "name": "Ansible Tower Instance", - "rootUrl": "https://{{ container_reference }}:8043", - "baseUrl": "/", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "**********", - "redirectUris": [ - "https://{{ container_reference }}:8043/sso/complete/saml/" - ], - "webOrigins": [ - "https://192.168.0.56:8043" - ], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": false, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": true, - "protocol": "saml", - "attributes": { - "saml.force.post.binding": "true", - "saml.multivalued.roles": "false", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "saml.signing.certificate": "{{ public_key_trimmed }}", - "oidc.ciba.grant.enabled": "false", - "backchannel.logout.session.required": "false", - "client_credentials.use_refresh_token": "false", - "saml.signature.algorithm": "RSA_SHA256", - "require.pushed.authorization.requests": "false", - "saml.client.signature": "false", - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "true", - "saml.encrypt": "false", - "saml_assertion_consumer_url_post": "https://{{ container_reference }}:8043/sso/complete/saml/", - "saml.server.signature": "true", - "saml_idp_initiated_sso_url_name": "{{ container_reference }}", - "exclude.session.state.from.auth.response": "false", - "saml.artifact.binding.identifier": "agnl7Dg3+QU8/5lb62pWlOcQWQw=", - "saml.artifact.binding": "false", - "saml_force_name_id_format": "false", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "true", - "display.on.consent.screen": "false", - "saml_name_id_format": "username", - "saml.onetimeuse.condition": "false", - "saml_signature_canonicalization_method": "http://www.w3.org/2001/10/xml-exc-c14n#" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "protocolMappers": [ - { - "id": "88b7ee19-7175-4968-bdd2-bfc40a8d82ee", - "name": "Email", - "protocol": "saml", - "protocolMapper": "saml-user-property-mapper", - "consentRequired": false, - "config": { - "attribute.nameformat": "Basic", - "user.attribute": "email", - "friendly.name": "Email", - "attribute.name": "email" - } - }, - { - "id": "02c06f47-6e9d-4317-b4f2-c7f4b3b42c03", - "name": "User_permanent_id", - "protocol": "saml", - "protocolMapper": "saml-user-property-mapper", - "consentRequired": false, - "config": { - "attribute.nameformat": "Basic", - "user.attribute": "uid", - "friendly.name": "name_id", - "attribute.name": "name_id" - } - }, - { - "id": "250e837f-8e9d-4899-b67c-043f1047e798", - "name": "Last_name", - "protocol": "saml", - "protocolMapper": "saml-user-property-mapper", - "consentRequired": false, - "config": { - "attribute.nameformat": "Basic", - "user.attribute": "lastName", - "friendly.name": "Last Name", - "attribute.name": "last_name" - } - }, - { - "id": "fd3d4150-ccaa-4ce8-b4a5-e7220279e7d7", - "name": "is_superuser", - "protocol": "saml", - "protocolMapper": "saml-user-attribute-mapper", - "consentRequired": false, - "config": { - "attribute.nameformat": "Basic", - "user.attribute": "is_superuser", - "aggregate.attrs": "true", - "friendly.name": "Is Super User", - "attribute.name": "is_superuser" - } - }, - { - "id": "a885cc97-c873-4ef8-8c7e-9490bf04c1e4", - "name": "Is_system_auditor", - "protocol": "saml", - "protocolMapper": "saml-user-attribute-mapper", - "consentRequired": false, - "config": { - "attribute.nameformat": "Basic", - "user.attribute": "is_system_auditor", - "aggregate.attrs": "true", - "friendly.name": "Is System Auditor", - "attribute.name": "is_system_auditor" - } - }, - { - "id": "5d09800d-960e-4ce5-bcc6-c5ec7da9ad07", - "name": "User_name", - "protocol": "saml", - "protocolMapper": "saml-user-property-mapper", - "consentRequired": false, - "config": { - "attribute.nameformat": "Basic", - "user.attribute": "username", - "friendly.name": "User Name", - "attribute.name": "username" - } - }, - { - "id": "35df9db1-fd61-4ef9-ad57-23c34032525d", - "name": "First_name", - "protocol": "saml", - "protocolMapper": "saml-user-property-mapper", - "consentRequired": false, - "config": { - "attribute.nameformat": "Basic", - "user.attribute": "firstName", - "friendly.name": "First Name", - "attribute.name": "first_name" - } - } - ], - "defaultClientScopes": [ - "role_list" - ], - "optionalClientScopes": [] - }, - { - "id": "525e0eeb-56ee-429f-a040-c6fc18072dc4", - "clientId": "awx_oidc_client", - "baseUrl": "", - "surrogateAuthRequired": false, - "enabled": true, - "alwaysDisplayInConsole": false, - "clientAuthenticatorType": "client-secret", - "secret": "7b1c3527-8702-4742-af69-2b74ee5742e8", - "redirectUris": [ - {% if oidc_reference is defined %} - "https://{{ oidc_reference }}:8043/sso/complete/oidc/", - {% endif %} - "https://{{ container_reference }}:8043/sso/complete/oidc/" - ], - "webOrigins": [], - "notBefore": 0, - "bearerOnly": false, - "consentRequired": false, - "standardFlowEnabled": true, - "implicitFlowEnabled": false, - "directAccessGrantsEnabled": true, - "serviceAccountsEnabled": false, - "publicClient": false, - "frontchannelLogout": false, - "protocol": "openid-connect", - "attributes": { - "id.token.as.detached.signature": "false", - "saml.assertion.signature": "false", - "saml.force.post.binding": "false", - "saml.multivalued.roles": "false", - "saml.encrypt": "false", - "oauth2.device.authorization.grant.enabled": "false", - "backchannel.logout.revoke.offline.tokens": "false", - "saml.server.signature": "false", - "saml.server.signature.keyinfo.ext": "false", - "use.refresh.tokens": "true", - "exclude.session.state.from.auth.response": "false", - "oidc.ciba.grant.enabled": "false", - "saml.artifact.binding": "false", - "backchannel.logout.session.required": "true", - "client_credentials.use_refresh_token": "false", - "saml_force_name_id_format": "false", - "require.pushed.authorization.requests": "false", - "saml.client.signature": "false", - "tls.client.certificate.bound.access.tokens": "false", - "saml.authnstatement": "false", - "display.on.consent.screen": "false", - "saml.onetimeuse.condition": "false" - }, - "authenticationFlowBindingOverrides": {}, - "fullScopeAllowed": true, - "nodeReRegistrationTimeout": -1, - "protocolMappers": [ - { - "id": "a8f4a0a8-ece4-4a9d-9e7b-830f23ba0067", - "name": "AWX OIDC Group Membership", - "protocol": "openid-connect", - "protocolMapper": "oidc-group-membership-mapper", - "consentRequired": false, - "config": { - "full.path": "false", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "Group", - "userinfo.token.claim": "true" - } - } - ], - "defaultClientScopes": [ - "web-origins", - "profile", - "roles", - "email" - ], - "optionalClientScopes": [ - "address", - "phone", - "offline_access", - "microprofile-jwt" - ] - } - ], - "clientScopes": [ - { - "id": "aaff7bde-8fa0-411b-a49d-9b27248a74d5", - "name": "profile", - "description": "OpenID Connect built-in scope: profile", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${profileScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "dbf37724-d6eb-4415-875e-5f9a34534fac", - "name": "picture", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "picture", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "picture", - "jsonType.label": "String" - } - }, - { - "id": "a57dd686-e1d0-4ade-b561-b9feb64c77b0", - "name": "full name", - "protocol": "openid-connect", - "protocolMapper": "oidc-full-name-mapper", - "consentRequired": false, - "config": { - "id.token.claim": "true", - "access.token.claim": "true", - "userinfo.token.claim": "true" - } - }, - { - "id": "85ac6cde-eec4-4d99-9e88-c2c50d2b4f38", - "name": "username", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "preferred_username", - "jsonType.label": "String" - } - }, - { - "id": "68c49e4e-2643-4c76-bf06-3149d5ee1c9e", - "name": "gender", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "gender", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "gender", - "jsonType.label": "String" - } - }, - { - "id": "d2314e27-3c36-4e8c-b080-fb17d3a3defa", - "name": "profile", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "profile", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "profile", - "jsonType.label": "String" - } - }, - { - "id": "46a791ee-d451-4b11-9ce1-b01c65749784", - "name": "family name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "lastName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "family_name", - "jsonType.label": "String" - } - }, - { - "id": "7260608c-5a52-4bf8-b6f4-b36549fa6da3", - "name": "locale", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "locale", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "locale", - "jsonType.label": "String" - } - }, - { - "id": "415b0de8-45e0-4032-b9dd-4bfcb08bc104", - "name": "website", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "website", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "website", - "jsonType.label": "String" - } - }, - { - "id": "2f4af5f6-236b-4e16-a326-7dd52cc9d8fd", - "name": "zoneinfo", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "zoneinfo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "zoneinfo", - "jsonType.label": "String" - } - }, - { - "id": "a3b93777-4b78-439e-b1a8-943e0cd40f3d", - "name": "birthdate", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "birthdate", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "birthdate", - "jsonType.label": "String" - } - }, - { - "id": "f4c3dc83-d908-49ad-95fc-3a5bbc9bf9a9", - "name": "updated at", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "updatedAt", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "updated_at", - "jsonType.label": "String" - } - }, - { - "id": "e28464a6-5dfa-47da-9bd3-fb92c4db2747", - "name": "middle name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "middleName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "middle_name", - "jsonType.label": "String" - } - }, - { - "id": "1f552301-87a4-4246-b12a-07884debc92b", - "name": "given name", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "firstName", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "given_name", - "jsonType.label": "String" - } - }, - { - "id": "f529b01c-f268-4d29-b626-f803618a943e", - "name": "nickname", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "nickname", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "nickname", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "c3590df9-2323-4fab-ba94-ee2abbc164ae", - "name": "roles", - "description": "OpenID Connect scope for add user roles to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "true", - "consent.screen.text": "${rolesScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "0cd89b4e-1144-4c5b-9274-9e13fe5f5a25", - "name": "client roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-client-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "resource_access.${client_id}.roles", - "jsonType.label": "String", - "multivalued": "true" - } - }, - { - "id": "f0cf07cf-841b-418d-90ad-59bc5cee7055", - "name": "audience resolve", - "protocol": "openid-connect", - "protocolMapper": "oidc-audience-resolve-mapper", - "consentRequired": false, - "config": {} - }, - { - "id": "4f57b44b-961b-4adb-871e-e69115c9c5f1", - "name": "realm roles", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "user.attribute": "foo", - "access.token.claim": "true", - "claim.name": "realm_access.roles", - "jsonType.label": "String", - "multivalued": "true" - } - } - ] - }, - { - "id": "3f054d84-7835-4926-bd2b-516e237959ca", - "name": "address", - "description": "OpenID Connect built-in scope: address", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${addressScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "ac0ccd00-7dd0-4e28-a898-f3cdcf1c14d4", - "name": "address", - "protocol": "openid-connect", - "protocolMapper": "oidc-address-mapper", - "consentRequired": false, - "config": { - "user.attribute.formatted": "formatted", - "user.attribute.country": "country", - "user.attribute.postal_code": "postal_code", - "userinfo.token.claim": "true", - "user.attribute.street": "street", - "id.token.claim": "true", - "user.attribute.region": "region", - "access.token.claim": "true", - "user.attribute.locality": "locality" - } - } - ] - }, - { - "id": "8e3a5e53-50dd-4a81-9009-8f322f499e24", - "name": "microprofile-jwt", - "description": "Microprofile - JWT built-in scope", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "false" - }, - "protocolMappers": [ - { - "id": "224d2538-704b-473e-b424-9db5be0b9757", - "name": "upn", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "username", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "upn", - "jsonType.label": "String" - } - }, - { - "id": "dfe95598-dc32-44e9-abe0-6d62013588ae", - "name": "groups", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-realm-role-mapper", - "consentRequired": false, - "config": { - "multivalued": "true", - "userinfo.token.claim": "true", - "user.attribute": "foo", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "groups", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "44b16c02-57ad-4143-b16e-1018a2f828a4", - "name": "offline_access", - "description": "OpenID Connect built-in scope: offline_access", - "protocol": "openid-connect", - "attributes": { - "consent.screen.text": "${offlineAccessScopeConsentText}", - "display.on.consent.screen": "true" - } - }, - { - "id": "e45624a3-a929-4ec5-9a06-39130c777e29", - "name": "email", - "description": "OpenID Connect built-in scope: email", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${emailScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "ddd0d995-e776-41f7-afbc-b01e73a7a16e", - "name": "email verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "emailVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email_verified", - "jsonType.label": "boolean" - } - }, - { - "id": "a6da2a73-be02-4008-9de6-fab6a44896a3", - "name": "email", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-property-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "email", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "email", - "jsonType.label": "String" - } - } - ] - }, - { - "id": "cc8a0b6d-5af0-439d-a00e-f872afc7082e", - "name": "phone", - "description": "OpenID Connect built-in scope: phone", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "true", - "display.on.consent.screen": "true", - "consent.screen.text": "${phoneScopeConsentText}" - }, - "protocolMappers": [ - { - "id": "80b1d9e3-9206-490f-a8e2-7b516821afbf", - "name": "phone number", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumber", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number", - "jsonType.label": "String" - } - }, - { - "id": "5263588a-1efd-4367-8805-b5b3cc2dbf91", - "name": "phone number verified", - "protocol": "openid-connect", - "protocolMapper": "oidc-usermodel-attribute-mapper", - "consentRequired": false, - "config": { - "userinfo.token.claim": "true", - "user.attribute": "phoneNumberVerified", - "id.token.claim": "true", - "access.token.claim": "true", - "claim.name": "phone_number_verified", - "jsonType.label": "boolean" - } - } - ] - }, - { - "id": "5f1c26ba-4c8f-4362-95fe-1d166e7bb440", - "name": "role_list", - "description": "SAML role list", - "protocol": "saml", - "attributes": { - "consent.screen.text": "${samlRoleListScopeConsentText}", - "display.on.consent.screen": "true" - }, - "protocolMappers": [ - { - "id": "b6872cf0-570a-4e96-9715-5506933ec466", - "name": "role list", - "protocol": "saml", - "protocolMapper": "saml-role-list-mapper", - "consentRequired": false, - "config": { - "single": "true", - "attribute.nameformat": "Basic", - "attribute.name": "Role" - } - } - ] - }, - { - "id": "fae034bf-e914-4d3c-8bb5-b327447a029b", - "name": "web-origins", - "description": "OpenID Connect scope for add allowed web origins to the access token", - "protocol": "openid-connect", - "attributes": { - "include.in.token.scope": "false", - "display.on.consent.screen": "false", - "consent.screen.text": "" - }, - "protocolMappers": [ - { - "id": "520587e1-141b-4cff-a00b-2d4f47452108", - "name": "allowed web origins", - "protocol": "openid-connect", - "protocolMapper": "oidc-allowed-origins-mapper", - "consentRequired": false, - "config": {} - } - ] - } - ], - "defaultDefaultClientScopes": [ - "role_list", - "profile", - "email", - "roles", - "web-origins" - ], - "defaultOptionalClientScopes": [ - "offline_access", - "address", - "phone", - "microprofile-jwt" - ], - "browserSecurityHeaders": { - "contentSecurityPolicyReportOnly": "", - "xContentTypeOptions": "nosniff", - "xRobotsTag": "none", - "xFrameOptions": "SAMEORIGIN", - "contentSecurityPolicy": "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "xXSSProtection": "1; mode=block", - "strictTransportSecurity": "max-age=31536000; includeSubDomains" - }, - "smtpServer": {}, - "eventsEnabled": false, - "eventsListeners": [ - "jboss-logging" - ], - "enabledEventTypes": [], - "adminEventsEnabled": false, - "adminEventsDetailsEnabled": false, - "identityProviders": [], - "identityProviderMappers": [], - "components": { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy": [ - { - "id": "5318c069-e07e-4f86-8057-38709fe791e3", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-usermodel-attribute-mapper", - "oidc-full-name-mapper", - "oidc-usermodel-property-mapper", - "oidc-address-mapper", - "saml-user-property-mapper", - "saml-user-attribute-mapper", - "oidc-sha256-pairwise-sub-mapper", - "saml-role-list-mapper" - ] - } - }, - { - "id": "64915844-107f-4a2c-8e2c-c94a4ccbe5ea", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - }, - { - "id": "c65cd2a0-ea62-4618-9bb2-9c8adffbb4c3", - "name": "Full Scope Disabled", - "providerId": "scope", - "subType": "anonymous", - "subComponents": {}, - "config": {} - }, - { - "id": "07a48654-4097-4deb-8a5e-65bb5715accc", - "name": "Allowed Client Scopes", - "providerId": "allowed-client-templates", - "subType": "anonymous", - "subComponents": {}, - "config": { - "allow-default-scopes": [ - "true" - ] - } - }, - { - "id": "11cb4cfe-868d-41bb-8ce3-97444a67aee6", - "name": "Max Clients Limit", - "providerId": "max-clients", - "subType": "anonymous", - "subComponents": {}, - "config": { - "max-clients": [ - "200" - ] - } - }, - { - "id": "a8de0b41-db78-405f-a83e-d0db3fe633bd", - "name": "Allowed Protocol Mapper Types", - "providerId": "allowed-protocol-mappers", - "subType": "authenticated", - "subComponents": {}, - "config": { - "allowed-protocol-mapper-types": [ - "oidc-address-mapper", - "saml-user-attribute-mapper", - "oidc-usermodel-property-mapper", - "oidc-full-name-mapper", - "oidc-usermodel-attribute-mapper", - "saml-user-property-mapper", - "saml-role-list-mapper", - "oidc-sha256-pairwise-sub-mapper" - ] - } - }, - { - "id": "4f8a2036-9b58-4d7f-9156-584b5d42b761", - "name": "Trusted Hosts", - "providerId": "trusted-hosts", - "subType": "anonymous", - "subComponents": {}, - "config": { - "host-sending-registration-request-must-match": [ - "true" - ], - "client-uris-must-match": [ - "true" - ] - } - }, - { - "id": "518e6f3a-fc1d-4a6f-aaba-59a4f8139c7b", - "name": "Consent Required", - "providerId": "consent-required", - "subType": "anonymous", - "subComponents": {}, - "config": {} - } - ], - "org.keycloak.userprofile.UserProfileProvider": [ - { - "id": "08d54b9a-2070-49b2-8d10-394b878b055b", - "providerId": "declarative-user-profile", - "subComponents": {}, - "config": {} - } - ], - "org.keycloak.keys.KeyProvider": [ - { - "id": "53bcf245-eabe-410e-bff1-f73ae34719ef", - "name": "fallback-HS256", - "providerId": "hmac-generated", - "subComponents": {}, - "config": { - "priority": [ - "-100" - ], - "algorithm": [ - "HS256" - ] - } - }, - { - "id": "ab943836-db0d-494d-ba61-61fc134a869b", - "name": "rsa", - "providerId": "rsa", - "subComponents": {}, - "config": { - "privateKey": [ - "{{ private_key }}" - ], - "certificate": [ - "{{ public_key }}" - ], - "active": [ - "true" - ], - "priority": [ - "0" - ], - "enabled": [ - "true" - ], - "algorithm": [ - "RS256" - ] - } - }, - { - "id": "69d92e66-721d-4b99-9337-acf57dff75de", - "name": "rsa-enc-generated", - "providerId": "rsa-generated", - "subComponents": {}, - "config": { - "keyUse": [ - "enc" - ], - "priority": [ - "100" - ] - } - }, - { - "id": "8de5d01c-75f2-4f32-a6ee-cb15db06038b", - "name": "aes-generated", - "providerId": "aes-generated", - "subComponents": {}, - "config": { - "active": [ - "true" - ], - "secretSize": [ - "16" - ], - "priority": [ - "0" - ], - "enabled": [ - "true" - ] - } - } - ] - }, - "internationalizationEnabled": false, - "supportedLocales": [], - "authenticationFlows": [ - { - "id": "8b9658fd-b6f3-4361-9fbf-bf89f54b1a25", - "alias": "Account verification options", - "description": "Method with which to verity the existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-email-verification", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "flowAlias": "Verify Existing Account by Re-authentication", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "7234371f-c10e-43a9-a300-3322710a0f75", - "alias": "Authentication Options", - "description": "Authentication options.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "basic-auth", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "basic-auth-otp", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "a76fcf8d-ec4a-4d52-a94b-127cc0d7025d", - "alias": "Browser - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "a36d552c-8209-40c0-b289-dd57c215d275", - "alias": "Direct Grant - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "direct-grant-validate-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "0508fa32-f7b3-41c6-8380-e5da0faf4c66", - "alias": "First broker login - Conditional OTP", - "description": "Flow to determine if the OTP is required for the authentication", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-otp-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "3f17c058-b701-453a-be1b-75302ad628dc", - "alias": "Handle Existing Account", - "description": "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-confirm-link", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "flowAlias": "Account verification options", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "7132cb2b-14ae-43db-9d39-b3b61e93f746", - "alias": "Reset - Conditional OTP", - "description": "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "conditional-user-configured", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "reset-otp", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "cdd4c5a8-a244-4e4f-a869-dd9e74825f05", - "alias": "User creation or linking", - "description": "Flow for the existing/non-existing user alternatives", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "create unique user config", - "authenticator": "idp-create-user-if-unique", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 20, - "flowAlias": "Handle Existing Account", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "4ae97d85-2514-44b7-9315-9974176a58a0", - "alias": "Verify Existing Account by Re-authentication", - "description": "Reauthentication of existing account", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "idp-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "flowAlias": "First broker login - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "91d8af19-f866-4891-b20f-5dfb6297f649", - "alias": "browser", - "description": "browser based authentication", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-cookie", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "auth-spnego", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "identity-provider-redirector", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 25, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticatorFlow": true, - "requirement": "ALTERNATIVE", - "priority": 30, - "flowAlias": "forms", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "3ca79c37-9b6b-4412-8aa5-7a88c8fb7876", - "alias": "clients", - "description": "Base authentication for clients", - "providerId": "client-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "client-secret", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "client-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "client-secret-jwt", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "client-x509", - "authenticatorFlow": false, - "requirement": "ALTERNATIVE", - "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "c94ac053-381d-47be-803a-fde7b02dcf7b", - "alias": "direct grant", - "description": "OpenID Connect Resource Owner Grant", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "direct-grant-validate-username", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "direct-grant-validate-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 30, - "flowAlias": "Direct Grant - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "3bb9e96e-63f8-4d2f-bf77-5fc799f351e4", - "alias": "docker auth", - "description": "Used by Docker clients to authenticate against the IDP", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "docker-http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "65fcf3db-58db-43ec-a51f-2c4c91e5c709", - "alias": "first broker login", - "description": "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticatorConfig": "review profile config", - "authenticator": "idp-review-profile", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "flowAlias": "User creation or linking", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "ae73711a-76d9-4061-b130-455324688229", - "alias": "forms", - "description": "Username, password, otp and other auth forms.", - "providerId": "basic-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "auth-username-password-form", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 20, - "flowAlias": "Browser - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "d4f0cf44-27f4-407c-84c9-fa8e4eb6db08", - "alias": "http challenge", - "description": "An authentication flow based on challenge-response HTTP Authentication Schemes", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "no-cookie-redirect", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 20, - "flowAlias": "Authentication Options", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "8f6cc60e-adf5-47a7-95e9-9b25b7fe1582", - "alias": "registration", - "description": "registration flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-page-form", - "authenticatorFlow": true, - "requirement": "REQUIRED", - "priority": 10, - "flowAlias": "registration form", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "2c70944a-54ad-44f9-8b9f-e4e16055c8c2", - "alias": "registration form", - "description": "registration form", - "providerId": "form-flow", - "topLevel": false, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "registration-user-creation", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "registration-profile-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 40, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "registration-password-action", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 50, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "registration-recaptcha-action", - "authenticatorFlow": false, - "requirement": "DISABLED", - "priority": 60, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - }, - { - "id": "9dfda07d-25eb-4ac8-9a48-6a9950409c87", - "alias": "reset credentials", - "description": "Reset credentials for a user if they forgot their password or something", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "reset-credentials-choose-user", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "reset-credential-email", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 20, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticator": "reset-password", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 30, - "userSetupAllowed": false, - "autheticatorFlow": false - }, - { - "authenticatorFlow": true, - "requirement": "CONDITIONAL", - "priority": 40, - "flowAlias": "Reset - Conditional OTP", - "userSetupAllowed": false, - "autheticatorFlow": true - } - ] - }, - { - "id": "5b693fb5-3844-4ab4-9291-a10d3ffd8ded", - "alias": "saml ecp", - "description": "SAML ECP Profile Authentication Flow", - "providerId": "basic-flow", - "topLevel": true, - "builtIn": true, - "authenticationExecutions": [ - { - "authenticator": "http-basic-authenticator", - "authenticatorFlow": false, - "requirement": "REQUIRED", - "priority": 10, - "userSetupAllowed": false, - "autheticatorFlow": false - } - ] - } - ], - "authenticatorConfig": [ - { - "id": "b59e82d0-cf9c-46e7-88c0-73a759ab951a", - "alias": "create unique user config", - "config": { - "require.password.update.after.registration": "false" - } - }, - { - "id": "abbe1e3d-383a-46ef-8d3b-6d221d5e45f7", - "alias": "review profile config", - "config": { - "update.profile.on.first.login": "missing" - } - } - ], - "requiredActions": [ - { - "alias": "CONFIGURE_TOTP", - "name": "Configure OTP", - "providerId": "CONFIGURE_TOTP", - "enabled": true, - "defaultAction": false, - "priority": 10, - "config": {} - }, - { - "alias": "terms_and_conditions", - "name": "Terms and Conditions", - "providerId": "terms_and_conditions", - "enabled": false, - "defaultAction": false, - "priority": 20, - "config": {} - }, - { - "alias": "UPDATE_PASSWORD", - "name": "Update Password", - "providerId": "UPDATE_PASSWORD", - "enabled": true, - "defaultAction": false, - "priority": 30, - "config": {} - }, - { - "alias": "UPDATE_PROFILE", - "name": "Update Profile", - "providerId": "UPDATE_PROFILE", - "enabled": true, - "defaultAction": false, - "priority": 40, - "config": {} - }, - { - "alias": "VERIFY_EMAIL", - "name": "Verify Email", - "providerId": "VERIFY_EMAIL", - "enabled": true, - "defaultAction": false, - "priority": 50, - "config": {} - }, - { - "alias": "delete_account", - "name": "Delete Account", - "providerId": "delete_account", - "enabled": false, - "defaultAction": false, - "priority": 60, - "config": {} - }, - { - "alias": "update_user_locale", - "name": "Update User Locale", - "providerId": "update_user_locale", - "enabled": true, - "defaultAction": false, - "priority": 1000, - "config": {} - } - ], - "browserFlow": "browser", - "registrationFlow": "registration", - "directGrantFlow": "direct grant", - "resetCredentialsFlow": "reset credentials", - "clientAuthenticationFlow": "clients", - "dockerAuthenticationFlow": "docker auth", - "attributes": { - "cibaBackchannelTokenDeliveryMode": "poll", - "cibaExpiresIn": "120", - "cibaAuthRequestedUserHint": "login_hint", - "oauth2DeviceCodeLifespan": "600", - "oauth2DevicePollingInterval": "5", - "clientOfflineSessionMaxLifespan": "0", - "clientSessionIdleTimeout": "0", - "userProfileEnabled": "false", - "clientSessionMaxLifespan": "0", - "parRequestUriLifespan": "60", - "clientOfflineSessionIdleTimeout": "0", - "cibaInterval": "5" - }, - "keycloakVersion": "15.0.2", - "userManagedAccessAllowed": false, - "clientProfiles": { - "profiles": [] - }, - "clientPolicies": { - "policies": [] - }, - "users" : [ { - "id" : "32559338-d619-47e5-839c-cd8c70e4221e", - "createdTimestamp" : 1638304914519, - "username" : "awx_unpriv", - "enabled" : true, - "totp" : false, - "emailVerified" : true, - "firstName" : "AWX", - "lastName" : "Unpriv", - "email" : "noone@nowhere.com", - "credentials" : [ { - "id" : "724329fd-66af-4973-ac17-02b7e7b6d33b", - "type" : "password", - "createdDate" : 1639095379005, - "secretData" : "{\"value\":\"mV1b4nP7BC1G4pY9wlo3IWArn3a5y7w96WpivmMdQaSYENBZF/vz0dMvz8cx4mVPahH/tzScb2vZnN/GzEL0gg==\",\"salt\":\"QN6A8ckQ0o02eUl4crwgxQ==\",\"additionalParameters\":{}}", - "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } ], - "disableableCredentialTypes" : [ ], - "requiredActions" : [ ], - "realmRoles" : [ "default-roles-awx realm" ], - "notBefore" : 0, - "groups" : [ ] - }, { - "id" : "667ec049-cdf3-45d0-a4dc-0465f7505954", - "createdTimestamp" : 1638394966440, - "username" : "awx_admin", - "enabled" : true, - "totp" : false, - "emailVerified" : true, - "firstName" : "AWX", - "lastName" : "Admin", - "email" : "no.one@nowhere.com", - "attributes" : { - "is_superuser" : [ "true" ] - }, - "credentials" : [ { - "id" : "51bcbebe-f17e-43c1-a549-8ab13c906bff", - "type" : "password", - "createdDate" : 1639095348459, - "secretData" : "{\"value\":\"kIzb+2QOUGmdPPntBYrdcjQYPYUH5Hd1o9x/23aBiF4o/zhQDKhikCwc8dnXjwZu0oQ8zR6EPfRET8P79IS8Nw==\",\"salt\":\"dDs/XM3LZj4agFeD/EFDTw==\",\"additionalParameters\":{}}", - "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } ], - "disableableCredentialTypes" : [ ], - "requiredActions" : [ ], - "realmRoles" : [ "default-roles-awx realm" ], - "notBefore" : 0, - "groups" : [ ] - }, { - "id" : "cbe48149-3bc5-49a1-a616-c8a5c8a3a88f", - "createdTimestamp" : 1638394708362, - "username" : "awx_auditor", - "enabled" : true, - "totp" : false, - "emailVerified" : true, - "firstName" : "AWX", - "lastName" : "Auditor", - "email" : "know.one@nowhere.com", - "attributes" : { - "is_system_auditor" : [ "Yup" ] - }, - "credentials" : [ { - "id" : "f94e759e-ddcf-41b4-b088-8f00d481eae4", - "type" : "password", - "createdDate" : 1639095365091, - "secretData" : "{\"value\":\"lr+KxXSfE42YGDgqN/PLukyUXAwD3+RjFW/RmqrGJ+N3B/+zuAYuxGJ7tfjb7U74CAxuimGNoaZoy3Gj7ObB/w==\",\"salt\":\"6PXPj4PMJ7iA2SS5N+r08g==\",\"additionalParameters\":{}}", - "credentialData" : "{\"hashIterations\":27500,\"algorithm\":\"pbkdf2-sha256\",\"additionalParameters\":{}}" - } ], - "disableableCredentialTypes" : [ ], - "requiredActions" : [ ], - "realmRoles" : [ "default-roles-awx realm" ], - "notBefore" : 0, - "groups" : [ ] - } ] -} diff --git a/tools/docker-compose/ansible/templates/ldap_settings.json.j2 b/tools/docker-compose/ansible/templates/ldap_settings.json.j2 deleted file mode 100644 index 793270d7c93c..000000000000 --- a/tools/docker-compose/ansible/templates/ldap_settings.json.j2 +++ /dev/null @@ -1,52 +0,0 @@ -{ - "AUTH_LDAP_1_SERVER_URI": "ldap://ldap:1389", - "AUTH_LDAP_1_BIND_DN": "cn=admin,dc=example,dc=org", - "AUTH_LDAP_1_BIND_PASSWORD": "admin", - "AUTH_LDAP_1_START_TLS": false, - "AUTH_LDAP_1_CONNECTION_OPTIONS": { - "OPT_REFERRALS": 0, - "OPT_NETWORK_TIMEOUT": 30 - }, - "AUTH_LDAP_1_USER_SEARCH": [ - "ou=users,dc=example,dc=org", - "SCOPE_SUBTREE", - "(cn=%(user)s)" - ], - "AUTH_LDAP_1_USER_DN_TEMPLATE": "cn=%(user)s,ou=users,dc=example,dc=org", - "AUTH_LDAP_1_USER_ATTR_MAP": { - "first_name": "givenName", - "last_name": "sn", - "email": "mail" - }, - "AUTH_LDAP_1_GROUP_SEARCH": [ - "ou=groups,dc=example,dc=org", - "SCOPE_SUBTREE", - "(objectClass=groupOfNames)" - ], - "AUTH_LDAP_1_GROUP_TYPE": "MemberDNGroupType", - "AUTH_LDAP_1_GROUP_TYPE_PARAMS": { - "member_attr": "member", - "name_attr": "cn" - }, - "AUTH_LDAP_1_REQUIRE_GROUP": "cn=awx_users,ou=groups,dc=example,dc=org", - "AUTH_LDAP_1_DENY_GROUP": null, - "AUTH_LDAP_1_USER_FLAGS_BY_GROUP": { - "is_superuser": [ - "cn=awx_admins,ou=groups,dc=example,dc=org" - ], - "is_system_auditor": [ - "cn=awx_auditors,ou=groups,dc=example,dc=org" - ] - }, - "AUTH_LDAP_1_ORGANIZATION_MAP": { - "LDAP Organization": { - "users": true, - "remove_admins": false, - "remove_users": true, - "admins": [ - "cn=awx_org_admins,ou=groups,dc=example,dc=org" - ] - } - }, - "AUTH_LDAP_1_TEAM_MAP": {} -} diff --git a/tools/docker-compose/ansible/templates/oidc_settings.json.j2 b/tools/docker-compose/ansible/templates/oidc_settings.json.j2 deleted file mode 100644 index dfeaa5809b39..000000000000 --- a/tools/docker-compose/ansible/templates/oidc_settings.json.j2 +++ /dev/null @@ -1,6 +0,0 @@ -{ - "SOCIAL_AUTH_OIDC_KEY": "awx_oidc_client", - "SOCIAL_AUTH_OIDC_SECRET": "7b1c3527-8702-4742-af69-2b74ee5742e8", - "SOCIAL_AUTH_OIDC_OIDC_ENDPOINT": "https://{{ oidc_reference | default(container_reference) }}:8443/auth/realms/awx", - "SOCIAL_AUTH_OIDC_VERIFY_SSL": "False" -} diff --git a/tools/docker-compose/ansible/templates/saml_settings.json.j2 b/tools/docker-compose/ansible/templates/saml_settings.json.j2 deleted file mode 100644 index 4cf9ffe399d7..000000000000 --- a/tools/docker-compose/ansible/templates/saml_settings.json.j2 +++ /dev/null @@ -1,51 +0,0 @@ -{ - "SAML_AUTO_CREATE_OBJECTS": true, - "SOCIAL_AUTH_SAML_SP_ENTITY_ID": "{{ container_reference }}:8043", - "SOCIAL_AUTH_SAML_SP_PUBLIC_CERT": "{{ public_key_content | regex_replace('\\n', '') }}", - "SOCIAL_AUTH_SAML_SP_PRIVATE_KEY": "{{ private_key_content | regex_replace('\\n', '') }}", - "SOCIAL_AUTH_SAML_ORG_INFO": { - "en-US": { - "url": "https://{{ container_reference }}:8443", - "name": "Keycloak", - "displayname": "Keycloak Solutions Engineering" - } - }, - "SOCIAL_AUTH_SAML_TECHNICAL_CONTACT": { - "givenName": "Me Myself", - "emailAddress": "noone@nowhere.com" - }, - "SOCIAL_AUTH_SAML_SUPPORT_CONTACT": { - "givenName": "Me Myself", - "emailAddress": "noone@nowhere.com" - }, - "SOCIAL_AUTH_SAML_ENABLED_IDPS": { - "Keycloak": { - "attr_user_permanent_id": "name_id", - "entity_id": "https://{{ container_reference }}:8443/auth/realms/awx", - "attr_groups": "groups", - "url": "https://{{ container_reference }}:8443/auth/realms/awx/protocol/saml", - "attr_first_name": "first_name", - "x509cert": "{{ public_key_content | regex_replace('\\n', '') }}", - "attr_email": "email", - "attr_last_name": "last_name", - "attr_username": "username" - } - }, - "SOCIAL_AUTH_SAML_SECURITY_CONFIG": { - "requestedAuthnContext": false - }, - "SOCIAL_AUTH_SAML_SP_EXTRA": null, - "SOCIAL_AUTH_SAML_EXTRA_DATA": null, - "SOCIAL_AUTH_SAML_ORGANIZATION_MAP": { - "Default": { - "users": true - } - }, - "SOCIAL_AUTH_SAML_TEAM_MAP": null, - "SOCIAL_AUTH_SAML_ORGANIZATION_ATTR": {}, - "SOCIAL_AUTH_SAML_TEAM_ATTR": {}, - "SOCIAL_AUTH_SAML_USER_FLAGS_BY_ATTR": { - "is_superuser_attr": "is_superuser", - "is_system_auditor_attr": "is_system_auditor" - } -} diff --git a/tools/docker-compose/ansible/templates/tacacsplus_settings.json.j2 b/tools/docker-compose/ansible/templates/tacacsplus_settings.json.j2 deleted file mode 100644 index fe9dd8c39112..000000000000 --- a/tools/docker-compose/ansible/templates/tacacsplus_settings.json.j2 +++ /dev/null @@ -1,7 +0,0 @@ -{ - "TACACSPLUS_HOST": "tacacs", - "TACACSPLUS_PORT": 49, - "TACACSPLUS_SECRET": "ciscotacacskey", - "TACACSPLUS_SESSION_TIMEOUT": 5, - "TACACSPLUS_AUTH_PROTOCOL": "ascii" -}