mirror of
https://github.com/ansible/awx.git
synced 2026-02-26 23:46:05 -03:30
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:
@@ -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)
|
||||||
|
|||||||
@@ -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
14
awx/main/backends.py
Normal 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)
|
||||||
@@ -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',
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -196,6 +196,7 @@ class AuthenticationBackendsField(fields.StringListField):
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
('django.contrib.auth.backends.ModelBackend', []),
|
('django.contrib.auth.backends.ModelBackend', []),
|
||||||
|
('awx.main.backends.AWXModelBackend', []),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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":[],
|
||||||
|
|||||||
Reference in New Issue
Block a user