Merge pull request #10102 from jbradberry/disable-local-users

Add the ability to disable local authentication

SUMMARY
When an external authentication system is enabled, users would like the ability to disable local authentication for enhanced security.
related #4553
TODO

 create a configure-Tower-in-Tower setting,  DISABLE_LOCAL_AUTH
 expose the setting in the settings UI
 be able to query out all local-only users

User.objects.filter(Q(profile__isnull=True) | Q(profile__ldap_dn=''), enterprise_auth__isnull=True, social_auth__isnull=True)
see: awx/main/utils/common.py, get_external_account


 write a thin wrapper around the Django model-based auth backend
 update the UI tests to include the new setting
 be able to trigger a side-effect when this setting changes
 revoke all OAuth2 tokens for users that do not have a remote
auth backend associated with them
 revoke sessions for local-only users

ultimately I did this by adding a new middleware that checks the value of this new setting and force-logouts any local-only user making a request after it is enabled


 settings API endpoint raises a validation error if there are no external users or auth sources configured

The remote user existence validation has been removed, since ultimately we can't know for sure if a sysadmin-level user will still have access to the UI.  This is being dealt with by using a confirmation modal, see below.


 add a modal asking the user to confirm that they want to turn this setting on

ISSUE TYPE


Feature Pull Request

COMPONENT NAME


API
UI

AWX VERSION

Reviewed-by: Jeff Bradberry <None>
Reviewed-by: Bianca Henderson <beeankha@gmail.com>
Reviewed-by: Mat Wilson <mawilson@redhat.com>
Reviewed-by: Michael Abashian <None>
Reviewed-by: Chris Meyers <None>
This commit is contained in:
softwarefactory-project-zuul[bot]
2021-05-27 18:37:47 +00:00
committed by GitHub
16 changed files with 273 additions and 16 deletions

View File

@@ -1,8 +1,12 @@
# Django # Django
from django.conf import settings
from django.utils.translation import ugettext_lazy as _ from django.utils.translation import ugettext_lazy as _
# Django REST Framework
from rest_framework import serializers
# AWX # AWX
from awx.conf import fields, register from awx.conf import fields, register, register_validate
from awx.api.fields import OAuth2ProviderField from awx.api.fields import OAuth2ProviderField
from oauth2_provider.settings import oauth2_settings from oauth2_provider.settings import oauth2_settings
@@ -27,6 +31,17 @@ register(
category=_('Authentication'), category=_('Authentication'),
category_slug='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( register(
'AUTH_BASIC_ENABLED', 'AUTH_BASIC_ENABLED',
field_class=fields.BooleanField, field_class=fields.BooleanField,
@@ -81,3 +96,23 @@ register(
category=_('Authentication'), category=_('Authentication'),
category_slug='authentication', category_slug='authentication',
) )
def authentication_validate(serializer, attrs):
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."))
return attrs
register_validate('authentication', authentication_validate)

View File

@@ -3,9 +3,9 @@ import logging
# Django # Django
from django.conf import settings from django.conf import settings
from django.core.cache import cache
from django.core.signals import setting_changed from django.core.signals import setting_changed
from django.db.models.signals import post_save, pre_delete, post_delete 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.dispatch import receiver
# AWX # AWX
@@ -25,7 +25,7 @@ def handle_setting_change(key, for_delete=False):
# Note: Doesn't handle multiple levels of dependencies! # Note: Doesn't handle multiple levels of dependencies!
setting_keys.append(dependent_key) setting_keys.append(dependent_key)
# NOTE: This block is probably duplicated. # 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) cache.delete_many(cache_keys)
# Send setting_changed signal with new value for each setting. # Send setting_changed signal with new value for each setting.
@@ -58,3 +58,18 @@ def on_post_delete_setting(sender, **kwargs):
key = getattr(instance, '_saved_key_', None) key = getattr(instance, '_saved_key_', None)
if key: if key:
handle_setting_change(key, True) 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))

14
awx/main/backends.py Normal file
View File

@@ -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)

View File

@@ -36,7 +36,7 @@ register(
'ORG_ADMINS_CAN_SEE_ALL_USERS', 'ORG_ADMINS_CAN_SEE_ALL_USERS',
field_class=fields.BooleanField, field_class=fields.BooleanField,
label=_('All Users Visible to Organization Admins'), 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=_('System'),
category_slug='system', category_slug='system',
) )
@@ -59,7 +59,7 @@ register(
schemes=('http', 'https'), schemes=('http', 'https'),
allow_plain_hostname=True, # Allow hostname only without TLD. allow_plain_hostname=True, # Allow hostname only without TLD.
label=_('Base URL of the service'), 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=_('System'),
category_slug='system', category_slug='system',
) )
@@ -94,13 +94,12 @@ register(
category_slug='system', category_slug='system',
) )
register( register(
'LICENSE', 'LICENSE',
field_class=fields.DictField, field_class=fields.DictField,
default=lambda: {}, default=lambda: {},
label=_('License'), 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=_('System'),
category_slug='system', category_slug='system',
) )
@@ -194,7 +193,7 @@ register(
'CUSTOM_VENV_PATHS', 'CUSTOM_VENV_PATHS',
field_class=fields.StringListPathField, field_class=fields.StringListPathField,
label=_('Custom virtual environment paths'), 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=_('System'),
category_slug='system', category_slug='system',
default=[], default=[],
@@ -318,7 +317,7 @@ register(
field_class=fields.BooleanField, field_class=fields.BooleanField,
default=False, default=False,
label=_('Ignore Ansible Galaxy SSL Certificate Verification'), 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=_('Jobs'),
category_slug='jobs', category_slug='jobs',
) )
@@ -433,7 +432,7 @@ register(
allow_null=False, allow_null=False,
default=200, default=200,
label=_('Maximum number of forks per job'), 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=_('Jobs'),
category_slug='jobs', category_slug='jobs',
) )
@@ -454,7 +453,7 @@ register(
allow_null=True, allow_null=True,
default=None, default=None,
label=_('Logging Aggregator Port'), 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=_('Logging'),
category_slug='logging', category_slug='logging',
required=False, required=False,
@@ -561,7 +560,7 @@ register(
field_class=fields.IntegerField, field_class=fields.IntegerField,
default=5, default=5,
label=_('TCP Connection Timeout'), 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=_('Logging'),
category_slug='logging', category_slug='logging',
unit=_('seconds'), unit=_('seconds'),
@@ -627,7 +626,7 @@ register(
field_class=fields.BooleanField, field_class=fields.BooleanField,
default=False, default=False,
label=_('Enable rsyslogd debugging'), 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=_('Logging'),
category_slug='logging', category_slug='logging',
) )

View File

@@ -31,6 +31,7 @@ class Command(BaseCommand):
for session in sessions: for session in sessions:
user_id = session.get_decoded().get('_auth_user_id') user_id = session.get_decoded().get('_auth_user_id')
if (user is None) or (user_id and user.id == int(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) session = import_module(settings.SESSION_ENGINE).SessionStore(session.session_key)
# Log out the session, but without the need for a request object. # Log out the session, but without the need for a request object.
session.flush() session.flush()

View File

@@ -7,6 +7,7 @@ import time
import urllib.parse import urllib.parse
from django.conf import settings from django.conf import settings
from django.contrib.auth import logout
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.migrations.executor import MigrationExecutor from django.db.migrations.executor import MigrationExecutor
from django.db import connection from django.db import connection
@@ -71,6 +72,21 @@ class SessionTimeoutMiddleware(MiddlewareMixin):
return response 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(): def _customize_graph():
from awx.main.models import Instance, Schedule, UnifiedJobTemplate from awx.main.models import Instance, Schedule, UnifiedJobTemplate

View File

@@ -364,7 +364,7 @@ AUTHENTICATION_BACKENDS = (
'social_core.backends.github_enterprise.GithubEnterpriseTeamOAuth2', 'social_core.backends.github_enterprise.GithubEnterpriseTeamOAuth2',
'social_core.backends.azuread.AzureADOAuth2', 'social_core.backends.azuread.AzureADOAuth2',
'awx.sso.backends.SAMLAuth', 'awx.sso.backends.SAMLAuth',
'django.contrib.auth.backends.ModelBackend', 'awx.main.backends.AWXModelBackend',
) )
@@ -716,6 +716,7 @@ CALLBACK_QUEUE = "callback_tasks"
# Note: This setting may be overridden by database settings. # Note: This setting may be overridden by database settings.
ORG_ADMINS_CAN_SEE_ALL_USERS = True ORG_ADMINS_CAN_SEE_ALL_USERS = True
MANAGE_ORGANIZATION_AUTH = True MANAGE_ORGANIZATION_AUTH = True
DISABLE_LOCAL_AUTH = False
# Note: This setting may be overridden by database settings. # Note: This setting may be overridden by database settings.
TOWER_URL_BASE = "https://towerhost" TOWER_URL_BASE = "https://towerhost"
@@ -913,6 +914,7 @@ MIDDLEWARE = [
'django.middleware.common.CommonMiddleware', 'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'awx.main.middleware.DisableLocalAuthMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'awx.sso.middleware.SocialAuthMiddleware', 'awx.sso.middleware.SocialAuthMiddleware',
'crum.CurrentRequestUserMiddleware', 'crum.CurrentRequestUserMiddleware',

View File

@@ -196,6 +196,7 @@ class AuthenticationBackendsField(fields.StringListField):
], ],
), ),
('django.contrib.auth.backends.ModelBackend', []), ('django.contrib.auth.backends.ModelBackend', []),
('awx.main.backends.AWXModelBackend', []),
] ]
) )

View File

@@ -48,6 +48,7 @@ function MiscSystemDetail() {
'INSIGHTS_TRACKING_STATE', 'INSIGHTS_TRACKING_STATE',
'LOGIN_REDIRECT_OVERRIDE', 'LOGIN_REDIRECT_OVERRIDE',
'MANAGE_ORGANIZATION_AUTH', 'MANAGE_ORGANIZATION_AUTH',
'DISABLE_LOCAL_AUTH',
'OAUTH2_PROVIDER', 'OAUTH2_PROVIDER',
'ORG_ADMINS_CAN_SEE_ALL_USERS', 'ORG_ADMINS_CAN_SEE_ALL_USERS',
'REDHAT_PASSWORD', 'REDHAT_PASSWORD',

View File

@@ -30,6 +30,7 @@ describe('<MiscSystemDetail />', () => {
INSIGHTS_TRACKING_STATE: false, INSIGHTS_TRACKING_STATE: false,
LOGIN_REDIRECT_OVERRIDE: 'https://redirect.com', LOGIN_REDIRECT_OVERRIDE: 'https://redirect.com',
MANAGE_ORGANIZATION_AUTH: true, MANAGE_ORGANIZATION_AUTH: true,
DISABLE_LOCAL_AUTH: false,
OAUTH2_PROVIDER: { OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS: 1, ACCESS_TOKEN_EXPIRE_SECONDS: 1,
AUTHORIZATION_CODE_EXPIRE_SECONDS: 2, AUTHORIZATION_CODE_EXPIRE_SECONDS: 2,

View File

@@ -48,6 +48,7 @@ function MiscSystemEdit() {
'INSIGHTS_TRACKING_STATE', 'INSIGHTS_TRACKING_STATE',
'LOGIN_REDIRECT_OVERRIDE', 'LOGIN_REDIRECT_OVERRIDE',
'MANAGE_ORGANIZATION_AUTH', 'MANAGE_ORGANIZATION_AUTH',
'DISABLE_LOCAL_AUTH',
'OAUTH2_PROVIDER', 'OAUTH2_PROVIDER',
'ORG_ADMINS_CAN_SEE_ALL_USERS', 'ORG_ADMINS_CAN_SEE_ALL_USERS',
'REDHAT_PASSWORD', 'REDHAT_PASSWORD',
@@ -261,6 +262,12 @@ function MiscSystemEdit() {
name="MANAGE_ORGANIZATION_AUTH" name="MANAGE_ORGANIZATION_AUTH"
config={system.MANAGE_ORGANIZATION_AUTH} config={system.MANAGE_ORGANIZATION_AUTH}
/> />
<BooleanField
name="DISABLE_LOCAL_AUTH"
needsConfirmationModal
modalTitle={t`Confirm Disable Local Authorization`}
config={system.DISABLE_LOCAL_AUTH}
/>
<InputField <InputField
name="SESSION_COOKIE_AGE" name="SESSION_COOKIE_AGE"
config={system.SESSION_COOKIE_AGE} config={system.SESSION_COOKIE_AGE}

View File

@@ -31,6 +31,7 @@ const systemData = {
INSIGHTS_TRACKING_STATE: false, INSIGHTS_TRACKING_STATE: false,
LOGIN_REDIRECT_OVERRIDE: '', LOGIN_REDIRECT_OVERRIDE: '',
MANAGE_ORGANIZATION_AUTH: true, MANAGE_ORGANIZATION_AUTH: true,
DISABLE_LOCAL_AUTH: false,
OAUTH2_PROVIDER: { OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS: 31536000000, ACCESS_TOKEN_EXPIRE_SECONDS: 31536000000,
AUTHORIZATION_CODE_EXPIRE_SECONDS: 600, AUTHORIZATION_CODE_EXPIRE_SECONDS: 600,

View File

@@ -3,6 +3,7 @@ import { shape, string } from 'prop-types';
import { t } from '@lingui/macro'; import { t } from '@lingui/macro';
import { useField } from 'formik'; import { useField } from 'formik';
import { import {
Button,
FileUpload, FileUpload,
FormGroup as PFFormGroup, FormGroup as PFFormGroup,
InputGroup, InputGroup,
@@ -25,6 +26,7 @@ import {
url, url,
} from '../../../util/validators'; } from '../../../util/validators';
import RevertButton from './RevertButton'; import RevertButton from './RevertButton';
import AlertModal from '../../../components/AlertModal';
const FormGroup = styled(PFFormGroup)` const FormGroup = styled(PFFormGroup)`
.pf-c-form__group-label { .pf-c-form__group-label {
@@ -73,8 +75,56 @@ const SettingGroup = ({
</FormGroup> </FormGroup>
); );
}; };
const BooleanField = ({ ariaLabel = '', name, config, disabled = false }) => { const BooleanField = ({
ariaLabel = '',
name,
config,
disabled = false,
needsConfirmationModal,
modalTitle,
}) => {
const [field, meta, helpers] = useField(name); const [field, meta, helpers] = useField(name);
const [isModalOpen, setIsModalOpen] = useState(false);
if (isModalOpen) {
return (
<AlertModal
isOpen
title={modalTitle}
variant="danger"
aria-label={modalTitle}
onClose={() => {
helpers.setValue(false);
}}
actions={[
<Button
ouiaId="confirm-misc-settings-modal"
key="confirm"
variant="danger"
aria-label={t`Confirm`}
onClick={() => {
helpers.setValue(true);
setIsModalOpen(false);
}}
>
{t`Confirm`}
</Button>,
<Button
ouiaId="cancel-misc-settings-modal"
key="cancel"
variant="link"
aria-label={t`Cancel`}
onClick={() => {
helpers.setValue(false);
setIsModalOpen(false);
}}
>
{t`Cancel`}
</Button>,
]}
>{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.`}</AlertModal>
);
}
return config ? ( return config ? (
<SettingGroup <SettingGroup
@@ -92,7 +142,11 @@ const BooleanField = ({ ariaLabel = '', name, config, disabled = false }) => {
isDisabled={disabled} isDisabled={disabled}
label={t`On`} label={t`On`}
labelOff={t`Off`} labelOff={t`Off`}
onChange={() => helpers.setValue(!field.value)} onChange={() =>
needsConfirmationModal
? setIsModalOpen(true)
: helpers.setValue(!field.value)
}
aria-label={ariaLabel || config.label} aria-label={ariaLabel || config.label}
/> />
</SettingGroup> </SettingGroup>

View File

@@ -265,4 +265,96 @@ describe('Setting form fields', () => {
wrapper.update(); wrapper.update();
expect(wrapper.find('input#mock_file-filename').prop('value')).toEqual(''); 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(
<Formik
initialValues={{
DISABLE_LOCAL_AUTH: false,
}}
>
{() => (
<BooleanField
name="DISABLE_LOCAL_AUTH"
needsConfirmationModal
modalTitle="Confirm Disable Local Authorization"
config={{
category: 'Authentication',
category_slug: 'authentication',
default: false,
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.',
label: 'Disable the built-in authentication system',
required: true,
type: 'boolean',
value: false,
}}
/>
)}
</Formik>
);
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(
<Formik
initialValues={{
DISABLE_LOCAL_AUTH: false,
}}
>
{() => (
<BooleanField
name="DISABLE_LOCAL_AUTH"
needsConfirmationModal
modalTitle="Confirm Disable Local Authorization"
config={{
category: 'Authentication',
category_slug: 'authentication',
default: false,
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.',
label: 'Disable the built-in authentication system',
required: true,
type: 'boolean',
value: false,
}}
/>
)}
</Formik>
);
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);
});
}); });

View File

@@ -34,6 +34,14 @@
"category_slug": "system", "category_slug": "system",
"defined_in_file": false "defined_in_file": false
}, },
"DISABLE_LOCAL_AUTH": {
"type": "boolean",
"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",
"defined_in_file": false
},
"TOWER_URL_BASE": { "TOWER_URL_BASE": {
"type": "string", "type": "string",
"label": "Base URL of the service", "label": "Base URL of the service",
@@ -2897,6 +2905,15 @@
"category_slug": "system", "category_slug": "system",
"default": true "default": true
}, },
"DISABLE_LOCAL_AUTH": {
"type": "boolean",
"required": true,
"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",
"default": false
},
"TOWER_URL_BASE": { "TOWER_URL_BASE": {
"type": "string", "type": "string",
"required": true, "required": true,

View File

@@ -3,6 +3,7 @@
"ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC":false, "ACTIVITY_STREAM_ENABLED_FOR_INVENTORY_SYNC":false,
"ORG_ADMINS_CAN_SEE_ALL_USERS":true, "ORG_ADMINS_CAN_SEE_ALL_USERS":true,
"MANAGE_ORGANIZATION_AUTH":true, "MANAGE_ORGANIZATION_AUTH":true,
"DISABLE_LOCAL_AUTH":false,
"TOWER_URL_BASE":"https://localhost:3000", "TOWER_URL_BASE":"https://localhost:3000",
"REMOTE_HOST_HEADERS":["REMOTE_ADDR","REMOTE_HOST"], "REMOTE_HOST_HEADERS":["REMOTE_ADDR","REMOTE_HOST"],
"PROXY_IP_ALLOWED_LIST":[], "PROXY_IP_ALLOWED_LIST":[],