From 26b7e9de4044e19410dcb60169cbfc10287eee6a Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Tue, 4 May 2021 11:07:48 -0400 Subject: [PATCH 1/7] Add a new setting, DISABLE_LOCAL_AUTH and expose it in the settings UI. --- awx/api/conf.py | 11 +++++++++++ awx/main/conf.py | 19 +++++++++---------- awx/settings/defaults.py | 1 + .../MiscSystemDetail/MiscSystemDetail.jsx | 1 + .../MiscSystemDetail.test.jsx | 1 + .../MiscSystemEdit/MiscSystemEdit.jsx | 5 +++++ .../MiscSystemEdit/MiscSystemEdit.test.jsx | 1 + .../shared/data.allSettingOptions.json | 17 +++++++++++++++++ .../Setting/shared/data.allSettings.json | 1 + 9 files changed, 47 insertions(+), 10 deletions(-) diff --git a/awx/api/conf.py b/awx/api/conf.py index 0a2ac6a89f..5616842fe0 100644 --- a/awx/api/conf.py +++ b/awx/api/conf.py @@ -27,6 +27,17 @@ register( category=_('Authentication'), category_slug='authentication', ) +register( + '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." + ), + category=_('Authentication'), + category_slug='authentication', +) register( 'AUTH_BASIC_ENABLED', field_class=fields.BooleanField, diff --git a/awx/main/conf.py b/awx/main/conf.py index 644045a79a..41f33711f2 100644 --- a/awx/main/conf.py +++ b/awx/main/conf.py @@ -36,7 +36,7 @@ register( 'ORG_ADMINS_CAN_SEE_ALL_USERS', field_class=fields.BooleanField, label=_('All Users Visible to Organization Admins'), - help_text=_('Controls whether any Organization Admin can view all users and teams, ' 'even those not associated with their Organization.'), + help_text=_('Controls whether any Organization Admin can view all users and teams, even those not associated with their Organization.'), category=_('System'), category_slug='system', ) @@ -59,7 +59,7 @@ register( schemes=('http', 'https'), allow_plain_hostname=True, # Allow hostname only without TLD. label=_('Base URL of the service'), - help_text=_('This setting is used by services like notifications to render ' 'a valid url to the service.'), + help_text=_('This setting is used by services like notifications to render a valid url to the service.'), category=_('System'), category_slug='system', ) @@ -94,13 +94,12 @@ register( category_slug='system', ) - register( 'LICENSE', field_class=fields.DictField, default=lambda: {}, label=_('License'), - help_text=_('The license controls which features and functionality are ' 'enabled. Use /api/v2/config/ to update or change ' 'the license.'), + help_text=_('The license controls which features and functionality are enabled. Use /api/v2/config/ to update or change the license.'), category=_('System'), category_slug='system', ) @@ -194,7 +193,7 @@ register( 'CUSTOM_VENV_PATHS', field_class=fields.StringListPathField, label=_('Custom virtual environment paths'), - help_text=_('Paths where Tower will look for custom virtual environments ' '(in addition to /var/lib/awx/venv/). Enter one path per line.'), + help_text=_('Paths where Tower will look for custom virtual environments (in addition to /var/lib/awx/venv/). Enter one path per line.'), category=_('System'), category_slug='system', default=[], @@ -318,7 +317,7 @@ register( field_class=fields.BooleanField, default=False, label=_('Ignore Ansible Galaxy SSL Certificate Verification'), - help_text=_('If set to true, certificate validation will not be done when ' 'installing content from any Galaxy server.'), + help_text=_('If set to true, certificate validation will not be done when installing content from any Galaxy server.'), category=_('Jobs'), category_slug='jobs', ) @@ -433,7 +432,7 @@ register( allow_null=False, default=200, label=_('Maximum number of forks per job'), - help_text=_('Saving a Job Template with more than this number of forks will result in an error. ' 'When set to 0, no limit is applied.'), + help_text=_('Saving a Job Template with more than this number of forks will result in an error. When set to 0, no limit is applied.'), category=_('Jobs'), category_slug='jobs', ) @@ -454,7 +453,7 @@ register( allow_null=True, default=None, label=_('Logging Aggregator Port'), - help_text=_('Port on Logging Aggregator to send logs to (if required and not' ' provided in Logging Aggregator).'), + help_text=_('Port on Logging Aggregator to send logs to (if required and not provided in Logging Aggregator).'), category=_('Logging'), category_slug='logging', required=False, @@ -561,7 +560,7 @@ register( field_class=fields.IntegerField, default=5, label=_('TCP Connection Timeout'), - help_text=_('Number of seconds for a TCP connection to external log ' 'aggregator to timeout. Applies to HTTPS and TCP log ' 'aggregator protocols.'), + help_text=_('Number of seconds for a TCP connection to external log aggregator to timeout. Applies to HTTPS and TCP log aggregator protocols.'), category=_('Logging'), category_slug='logging', unit=_('seconds'), @@ -627,7 +626,7 @@ register( field_class=fields.BooleanField, default=False, label=_('Enable rsyslogd debugging'), - help_text=_('Enabled high verbosity debugging for rsyslogd. ' 'Useful for debugging connection issues for external log aggregation.'), + help_text=_('Enabled high verbosity debugging for rsyslogd. Useful for debugging connection issues for external log aggregation.'), category=_('Logging'), category_slug='logging', ) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 5d1a3b1dd6..31100f11b3 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -716,6 +716,7 @@ CALLBACK_QUEUE = "callback_tasks" # Note: This setting may be overridden by database settings. ORG_ADMINS_CAN_SEE_ALL_USERS = True MANAGE_ORGANIZATION_AUTH = True +DISABLE_LOCAL_AUTH = False # Note: This setting may be overridden by database settings. TOWER_URL_BASE = "https://towerhost" diff --git a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.jsx b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.jsx index 75c8c0fce8..de5080eec4 100644 --- a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.jsx +++ b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.jsx @@ -48,6 +48,7 @@ function MiscSystemDetail() { 'INSIGHTS_TRACKING_STATE', 'LOGIN_REDIRECT_OVERRIDE', 'MANAGE_ORGANIZATION_AUTH', + 'DISABLE_LOCAL_AUTH', 'OAUTH2_PROVIDER', 'ORG_ADMINS_CAN_SEE_ALL_USERS', 'REDHAT_PASSWORD', diff --git a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.test.jsx b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.test.jsx index 998fc1c61c..bc9e429f83 100644 --- a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.test.jsx +++ b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemDetail/MiscSystemDetail.test.jsx @@ -30,6 +30,7 @@ describe('', () => { INSIGHTS_TRACKING_STATE: false, LOGIN_REDIRECT_OVERRIDE: 'https://redirect.com', MANAGE_ORGANIZATION_AUTH: true, + DISABLE_LOCAL_AUTH: false, OAUTH2_PROVIDER: { ACCESS_TOKEN_EXPIRE_SECONDS: 1, AUTHORIZATION_CODE_EXPIRE_SECONDS: 2, diff --git a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx index 8206fb0a3c..312b4dbf96 100644 --- a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx +++ b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx @@ -48,6 +48,7 @@ function MiscSystemEdit() { 'INSIGHTS_TRACKING_STATE', 'LOGIN_REDIRECT_OVERRIDE', 'MANAGE_ORGANIZATION_AUTH', + 'DISABLE_LOCAL_AUTH', 'OAUTH2_PROVIDER', 'ORG_ADMINS_CAN_SEE_ALL_USERS', 'REDHAT_PASSWORD', @@ -261,6 +262,10 @@ function MiscSystemEdit() { name="MANAGE_ORGANIZATION_AUTH" config={system.MANAGE_ORGANIZATION_AUTH} /> + Date: Fri, 7 May 2021 14:37:39 -0400 Subject: [PATCH 2/7] Write a thin wrapper around the standard Django auth backend --- awx/main/backends.py | 14 ++++++++++++++ awx/settings/defaults.py | 2 +- awx/sso/fields.py | 1 + 3 files changed, 16 insertions(+), 1 deletion(-) create mode 100644 awx/main/backends.py diff --git a/awx/main/backends.py b/awx/main/backends.py new file mode 100644 index 0000000000..722b94805f --- /dev/null +++ b/awx/main/backends.py @@ -0,0 +1,14 @@ +import logging + +from django.conf import settings +from django.contrib.auth.backends import ModelBackend + +logger = logging.getLogger('awx.main.backends') + + +class AWXModelBackend(ModelBackend): + def authenticate(self, request, **kwargs): + if settings.DISABLE_LOCAL_AUTH: + logger.warning(f"User '{kwargs['username']}' attempted login through the disabled local authentication system.") + return + return super().authenticate(request, **kwargs) diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index 31100f11b3..c2ac5f4b02 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -364,7 +364,7 @@ AUTHENTICATION_BACKENDS = ( 'social_core.backends.github_enterprise.GithubEnterpriseTeamOAuth2', 'social_core.backends.azuread.AzureADOAuth2', 'awx.sso.backends.SAMLAuth', - 'django.contrib.auth.backends.ModelBackend', + 'awx.main.backends.AWXModelBackend', ) diff --git a/awx/sso/fields.py b/awx/sso/fields.py index c2ad629d6f..deef330842 100644 --- a/awx/sso/fields.py +++ b/awx/sso/fields.py @@ -196,6 +196,7 @@ class AuthenticationBackendsField(fields.StringListField): ], ), ('django.contrib.auth.backends.ModelBackend', []), + ('awx.main.backends.AWXModelBackend', []), ] ) From 9e7f004ca6cc75ce093680a9f389fb0ac825ca82 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Mon, 10 May 2021 16:39:52 -0400 Subject: [PATCH 3/7] Add a signal handler to invalidate sessions and tokens for local users when this setting gets turned on. --- awx/conf/signals.py | 33 +++++++++++++++++-- .../management/commands/expire_sessions.py | 1 + 2 files changed, 32 insertions(+), 2 deletions(-) diff --git a/awx/conf/signals.py b/awx/conf/signals.py index 00e486734b..e0788f8936 100644 --- a/awx/conf/signals.py +++ b/awx/conf/signals.py @@ -3,10 +3,11 @@ import logging # Django from django.conf import settings +from django.core.cache import cache from django.core.signals import setting_changed from django.db.models.signals import post_save, pre_delete, post_delete -from django.core.cache import cache from django.dispatch import receiver +from django.utils import timezone # AWX from awx.conf import settings_registry @@ -25,7 +26,7 @@ def handle_setting_change(key, for_delete=False): # Note: Doesn't handle multiple levels of dependencies! setting_keys.append(dependent_key) # NOTE: This block is probably duplicated. - cache_keys = set([Setting.get_cache_key(k) for k in setting_keys]) + cache_keys = {Setting.get_cache_key(k) for k in setting_keys} cache.delete_many(cache_keys) # Send setting_changed signal with new value for each setting. @@ -58,3 +59,31 @@ 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 django.contrib.sessions.models import Session + 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 session and 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)) + + user_ids = set(qs.values_list('id', flat=True)) + + start = timezone.now() + sessions = Session.objects.filter(expire_date__gte=start).iterator() + for session in sessions: + decoded = session.get_decoded() + user_id = int(decoded.get('_auth_user_id') or '-1') + if user_id in user_ids: + # The Session model instance doesn't have .flush(), we need a SessionStore instance. + session_store = session.get_session_store_class()(session.session_key) + session_store.flush() diff --git a/awx/main/management/commands/expire_sessions.py b/awx/main/management/commands/expire_sessions.py index 65053e3e50..2f7a17c450 100644 --- a/awx/main/management/commands/expire_sessions.py +++ b/awx/main/management/commands/expire_sessions.py @@ -31,6 +31,7 @@ class Command(BaseCommand): for session in sessions: user_id = session.get_decoded().get('_auth_user_id') if (user is None) or (user_id and user.id == int(user_id)): + # The Session model instance doesn't have .flush(), we need a SessionStore instance. session = import_module(settings.SESSION_ENGINE).SessionStore(session.session_key) # Log out the session, but without the need for a request object. session.flush() From 81de9317111d9bd0f89537380f5bba9b6e637259 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Tue, 11 May 2021 10:48:34 -0400 Subject: [PATCH 4/7] Add a new middleware to force-logout local-only users when the DISABLE_LOCAL_AUTH setting is set. This avoids the ugliness of getting a SuspiciousOperation error for any request/response cycles that are in flight when a user gets bounced. --- awx/conf/signals.py | 16 +--------------- awx/main/middleware.py | 16 ++++++++++++++++ awx/settings/defaults.py | 1 + 3 files changed, 18 insertions(+), 15 deletions(-) diff --git a/awx/conf/signals.py b/awx/conf/signals.py index e0788f8936..843900d9e6 100644 --- a/awx/conf/signals.py +++ b/awx/conf/signals.py @@ -7,7 +7,6 @@ from django.core.cache import cache from django.core.signals import setting_changed from django.db.models.signals import post_save, pre_delete, post_delete from django.dispatch import receiver -from django.utils import timezone # AWX from awx.conf import settings_registry @@ -65,25 +64,12 @@ def on_post_delete_setting(sender, **kwargs): def disable_local_auth(**kwargs): if (kwargs['setting'], kwargs['value']) == ('DISABLE_LOCAL_AUTH', True): from django.contrib.auth.models import User - from django.contrib.sessions.models import Session 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 session and token invalidation for local users.") + 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)) - - user_ids = set(qs.values_list('id', flat=True)) - - start = timezone.now() - sessions = Session.objects.filter(expire_date__gte=start).iterator() - for session in sessions: - decoded = session.get_decoded() - user_id = int(decoded.get('_auth_user_id') or '-1') - if user_id in user_ids: - # The Session model instance doesn't have .flush(), we need a SessionStore instance. - session_store = session.get_session_store_class()(session.session_key) - session_store.flush() diff --git a/awx/main/middleware.py b/awx/main/middleware.py index 05c4564ffa..05e03777fb 100644 --- a/awx/main/middleware.py +++ b/awx/main/middleware.py @@ -7,6 +7,7 @@ import time import urllib.parse from django.conf import settings +from django.contrib.auth import logout from django.contrib.auth.models import User from django.db.migrations.executor import MigrationExecutor from django.db import connection @@ -71,6 +72,21 @@ class SessionTimeoutMiddleware(MiddlewareMixin): return response +class DisableLocalAuthMiddleware(MiddlewareMixin): + """ + Respects the presence of the DISABLE_LOCAL_AUTH setting and forces + local-only users to logout when they make a request. + """ + + def process_request(self, request): + if settings.DISABLE_LOCAL_AUTH: + 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) + + def _customize_graph(): from awx.main.models import Instance, Schedule, UnifiedJobTemplate diff --git a/awx/settings/defaults.py b/awx/settings/defaults.py index c2ac5f4b02..33215af87f 100644 --- a/awx/settings/defaults.py +++ b/awx/settings/defaults.py @@ -914,6 +914,7 @@ MIDDLEWARE = [ 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'awx.main.middleware.DisableLocalAuthMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'awx.sso.middleware.SocialAuthMiddleware', 'crum.CurrentRequestUserMiddleware', From 6f4c41a8d33bb2292115c6fa792402425319ffe4 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Tue, 11 May 2021 11:34:14 -0400 Subject: [PATCH 5/7] Add validation checks that prevent the setting from being turned on if remote auth systems and users are not already present. --- awx/api/conf.py | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/awx/api/conf.py b/awx/api/conf.py index 5616842fe0..1a47d46e63 100644 --- a/awx/api/conf.py +++ b/awx/api/conf.py @@ -1,8 +1,12 @@ # Django +from django.conf import settings from django.utils.translation import ugettext_lazy as _ +# Django REST Framework +from rest_framework import serializers + # AWX -from awx.conf import fields, register +from awx.conf import fields, register, register_validate from awx.api.fields import OAuth2ProviderField from oauth2_provider.settings import oauth2_settings @@ -92,3 +96,27 @@ register( category=_('Authentication'), category_slug='authentication', ) + + +def authentication_validate(serializer, attrs): + from django.contrib.auth.models import User + + remote_auth_settings = [ + 'AUTH_LDAP_SERVER_URI', + 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', + 'SOCIAL_AUTH_GITHUB_KEY', + 'SOCIAL_AUTH_GITHUB_ORG_KEY', + 'SOCIAL_AUTH_GITHUB_TEAM_KEY', + 'SOCIAL_AUTH_SAML_ENABLED_IDPS', + 'RADIUS_SERVER', + 'TACACSPLUS_HOST', + ] + if attrs.get('DISABLE_LOCAL_AUTH', False): + if not any(getattr(settings, s, None) for s in remote_auth_settings): + raise serializers.ValidationError(_("There are no remote authentication systems configured.")) + if not User.objects.exclude(profile__ldap_dn='', enterprise_auth__isnull=True, social_auth__isnull=True).exists(): + raise serializers.ValidationError(_("There are no remote users in the system.")) + return attrs + + +register_validate('authentication', authentication_validate) From 2aa3fe756e6dd10af4baaa1b40599b468d4ef000 Mon Sep 17 00:00:00 2001 From: Jeff Bradberry Date: Wed, 19 May 2021 10:27:51 -0400 Subject: [PATCH 6/7] Remove the remote user existence validation since we are going to do a confirmation modal dialog instead. --- awx/api/conf.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/awx/api/conf.py b/awx/api/conf.py index 1a47d46e63..00c712a064 100644 --- a/awx/api/conf.py +++ b/awx/api/conf.py @@ -99,8 +99,6 @@ register( def authentication_validate(serializer, attrs): - from django.contrib.auth.models import User - remote_auth_settings = [ 'AUTH_LDAP_SERVER_URI', 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY', @@ -114,8 +112,6 @@ def authentication_validate(serializer, attrs): if attrs.get('DISABLE_LOCAL_AUTH', False): if not any(getattr(settings, s, None) for s in remote_auth_settings): raise serializers.ValidationError(_("There are no remote authentication systems configured.")) - if not User.objects.exclude(profile__ldap_dn='', enterprise_auth__isnull=True, social_auth__isnull=True).exists(): - raise serializers.ValidationError(_("There are no remote users in the system.")) return attrs From 3b5641c41b44198ee459f3bd5c4353aa59735a9d Mon Sep 17 00:00:00 2001 From: Alex Corey Date: Thu, 20 May 2021 09:31:31 -0400 Subject: [PATCH 7/7] adds confirmation modal to switch --- .../MiscSystemEdit/MiscSystemEdit.jsx | 2 + .../screens/Setting/shared/SharedFields.jsx | 58 +++++++++++- .../Setting/shared/SharedFields.test.jsx | 92 +++++++++++++++++++ 3 files changed, 150 insertions(+), 2 deletions(-) diff --git a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx index 312b4dbf96..f72b12b794 100644 --- a/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx +++ b/awx/ui_next/src/screens/Setting/MiscSystem/MiscSystemEdit/MiscSystemEdit.jsx @@ -264,6 +264,8 @@ function MiscSystemEdit() { /> ); }; -const BooleanField = ({ ariaLabel = '', name, config, disabled = false }) => { +const BooleanField = ({ + ariaLabel = '', + name, + config, + disabled = false, + needsConfirmationModal, + modalTitle, +}) => { const [field, meta, helpers] = useField(name); + const [isModalOpen, setIsModalOpen] = useState(false); + + if (isModalOpen) { + return ( + { + helpers.setValue(false); + }} + actions={[ + , + , + ]} + >{t`Are you sure you want to disable local authentication? Doing so could impact users' ability to log in and the system administrator's ability to reverse this change.`} + ); + } return config ? ( { isDisabled={disabled} label={t`On`} labelOff={t`Off`} - onChange={() => helpers.setValue(!field.value)} + onChange={() => + needsConfirmationModal + ? setIsModalOpen(true) + : helpers.setValue(!field.value) + } aria-label={ariaLabel || config.label} /> diff --git a/awx/ui_next/src/screens/Setting/shared/SharedFields.test.jsx b/awx/ui_next/src/screens/Setting/shared/SharedFields.test.jsx index 3ac561e3d2..35c494fe83 100644 --- a/awx/ui_next/src/screens/Setting/shared/SharedFields.test.jsx +++ b/awx/ui_next/src/screens/Setting/shared/SharedFields.test.jsx @@ -265,4 +265,96 @@ describe('Setting form fields', () => { wrapper.update(); expect(wrapper.find('input#mock_file-filename').prop('value')).toEqual(''); }); + test('should render confirmation modal when toggle on for disable local auth', async () => { + const wrapper = mountWithContexts( + + {() => ( + + )} + + ); + expect(wrapper.find('Switch')).toHaveLength(1); + expect(wrapper.find('Switch').prop('isChecked')).toBe(false); + expect(wrapper.find('Switch').prop('isDisabled')).toBe(false); + await act(async () => { + wrapper.find('Switch').invoke('onChange')(); + }); + wrapper.update(); + + expect(wrapper.find('AlertModal')).toHaveLength(1); + await act(async () => + wrapper + .find('Button[ouiaId="confirm-misc-settings-modal"]') + .prop('onClick')() + ); + wrapper.update(); + expect(wrapper.find('AlertModal')).toHaveLength(0); + expect(wrapper.find('Switch').prop('isChecked')).toBe(true); + }); + + test('shold not toggle disable local auth', async () => { + const wrapper = mountWithContexts( + + {() => ( + + )} + + ); + expect(wrapper.find('Switch')).toHaveLength(1); + expect(wrapper.find('Switch').prop('isChecked')).toBe(false); + expect(wrapper.find('Switch').prop('isDisabled')).toBe(false); + await act(async () => { + wrapper.find('Switch').invoke('onChange')(); + }); + wrapper.update(); + + expect(wrapper.find('AlertModal')).toHaveLength(1); + await act(async () => + wrapper + .find('Button[ouiaId="cancel-misc-settings-modal"]') + .prop('onClick')() + ); + wrapper.update(); + + expect(wrapper.find('AlertModal')).toHaveLength(0); + expect(wrapper.find('Switch').prop('isChecked')).toBe(false); + }); });