Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the ability to disable local authentication #10102

Merged
merged 7 commits into from
May 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 36 additions & 1 deletion awx/api/conf.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -27,6 +31,17 @@
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,
Expand Down Expand Up @@ -81,3 +96,23 @@
category=_('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)
19 changes: 17 additions & 2 deletions awx/conf/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@

# 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

# AWX
Expand All @@ -25,7 +25,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.
Expand Down Expand Up @@ -58,3 +58,18 @@ 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))
14 changes: 14 additions & 0 deletions awx/main/backends.py
Original file line number Diff line number Diff line change
@@ -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)
19 changes: 9 additions & 10 deletions awx/main/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
'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',
)
Expand All @@ -59,7 +59,7 @@
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',
)
Expand Down Expand Up @@ -94,13 +94,12 @@
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',
)
Expand Down Expand Up @@ -194,7 +193,7 @@
'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=[],
Expand Down Expand Up @@ -318,7 +317,7 @@
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',
)
Expand Down Expand Up @@ -433,7 +432,7 @@
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',
)
Expand All @@ -454,7 +453,7 @@
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,
Expand Down Expand Up @@ -561,7 +560,7 @@
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'),
Expand Down Expand Up @@ -627,7 +626,7 @@
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',
)
Expand Down
1 change: 1 addition & 0 deletions awx/main/management/commands/expire_sessions.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ def handle(self, *args, **options):
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()
16 changes: 16 additions & 0 deletions awx/main/middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
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
Expand Down Expand Up @@ -71,6 +72,21 @@ def process_response(self, request, 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():
from awx.main.models import Instance, Schedule, UnifiedJobTemplate

Expand Down
4 changes: 3 additions & 1 deletion awx/settings/defaults.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,7 +364,7 @@ def IS_TESTING(argv=None):
'social_core.backends.github_enterprise.GithubEnterpriseTeamOAuth2',
'social_core.backends.azuread.AzureADOAuth2',
'awx.sso.backends.SAMLAuth',
'django.contrib.auth.backends.ModelBackend',
'awx.main.backends.AWXModelBackend',
)


Expand Down Expand Up @@ -716,6 +716,7 @@ def IS_TESTING(argv=None):
# 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"
Expand Down Expand Up @@ -913,6 +914,7 @@ def IS_TESTING(argv=None):
'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',
Expand Down
1 change: 1 addition & 0 deletions awx/sso/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,7 @@ class AuthenticationBackendsField(fields.StringListField):
],
),
('django.contrib.auth.backends.ModelBackend', []),
('awx.main.backends.AWXModelBackend', []),
]
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('<MiscSystemDetail />', () => {
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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -261,6 +262,12 @@ function MiscSystemEdit() {
name="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
name="SESSION_COOKIE_AGE"
config={system.SESSION_COOKIE_AGE}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ const systemData = {
INSIGHTS_TRACKING_STATE: false,
LOGIN_REDIRECT_OVERRIDE: '',
MANAGE_ORGANIZATION_AUTH: true,
DISABLE_LOCAL_AUTH: false,
OAUTH2_PROVIDER: {
ACCESS_TOKEN_EXPIRE_SECONDS: 31536000000,
AUTHORIZATION_CODE_EXPIRE_SECONDS: 600,
Expand Down
58 changes: 56 additions & 2 deletions awx/ui_next/src/screens/Setting/shared/SharedFields.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { shape, string } from 'prop-types';
import { t } from '@lingui/macro';
import { useField } from 'formik';
import {
Button,
FileUpload,
FormGroup as PFFormGroup,
InputGroup,
Expand All @@ -25,6 +26,7 @@ import {
url,
} from '../../../util/validators';
import RevertButton from './RevertButton';
import AlertModal from '../../../components/AlertModal';

const FormGroup = styled(PFFormGroup)`
.pf-c-form__group-label {
Expand Down Expand Up @@ -73,8 +75,56 @@ const SettingGroup = ({
</FormGroup>
);
};
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 (
<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 ? (
<SettingGroup
Expand All @@ -92,7 +142,11 @@ const BooleanField = ({ ariaLabel = '', name, config, disabled = false }) => {
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}
/>
</SettingGroup>
Expand Down
Loading