diff --git a/awx/main/models/__init__.py b/awx/main/models/__init__.py index 646618d1e8..01bd68cc66 100644 --- a/awx/main/models/__init__.py +++ b/awx/main/models/__init__.py @@ -98,6 +98,20 @@ def user_is_system_auditor(user, tf): User.add_to_class('is_system_auditor', user_is_system_auditor) + +def user_is_in_enterprise_category(user, category): + ret = (category,) in user.enterprise_auth.all().values_list('provider') and not user.has_usable_password() + # NOTE: this if-else block ensures existing enterprise users are still able to + # log in. Remove it in a future release + if category == 'radius': + ret = ret or not user.has_usable_password() + elif category == 'saml': + ret = ret or user.social_auth.all() + return ret + + +User.add_to_class('is_in_enterprise_category', user_is_in_enterprise_category) + # Import signal handlers only after models have been defined. import awx.main.signals # noqa diff --git a/awx/sso/backends.py b/awx/sso/backends.py index 1dd42fcfd9..104fdd2c99 100644 --- a/awx/sso/backends.py +++ b/awx/sso/backends.py @@ -126,23 +126,25 @@ class LDAPBackend(BaseLDAPBackend): return set() +def _decorate_enterprise_user(user, provider): + user.set_unusable_password() + user.save() + enterprise_auth = UserEnterpriseAuth(user=user, provider=provider) + enterprise_auth.save() + return enterprise_auth + + def _get_or_set_enterprise_user(username, password, provider): created = False try: user = User.objects.all().prefetch_related('enterprise_auth').get(username=username) except User.DoesNotExist: user = User(username=username) - user.set_unusable_password() - user.save() - enterprise_auth = UserEnterpriseAuth(user=user, provider=provider) - enterprise_auth.save() + enterprise_auth = _decorate_enterprise_user(user, provider) logger.debug("Created enterprise user %s via %s backend." % (username, enterprise_auth.get_provider_display())) created = True - # NOTE: remove has_usable_password logic in a future release. - if created or\ - not user.has_usable_password() or\ - (provider,) in user.enterprise_auth.all().values_list('provider') and not user.has_usable_password(): + if created or user.is_in_enterprise_category(provider): return user logger.warn("Enterprise user %s already defined in Tower." % username) @@ -258,7 +260,17 @@ class SAMLAuth(BaseSAMLAuth): if not feature_enabled('enterprise_auth'): logger.error("Unable to authenticate, license does not support SAML authentication") return None - return super(SAMLAuth, self).authenticate(*args, **kwargs) + created = False + try: + user = User.objects.get(username=kwargs.get('username', '')) + if user and not user.is_in_enterprise_category('saml'): + return None + except User.DoesNotExist: + created = True + user = super(SAMLAuth, self).authenticate(*args, **kwargs) + if user and created: + _decorate_enterprise_user(user, 'saml') + return user def get_user(self, user_id): if not all([django_settings.SOCIAL_AUTH_SAML_SP_ENTITY_ID, django_settings.SOCIAL_AUTH_SAML_SP_PUBLIC_CERT, diff --git a/awx/sso/migrations/0002_expand_provider_options.py b/awx/sso/migrations/0002_expand_provider_options.py new file mode 100644 index 0000000000..aff1b3d6f1 --- /dev/null +++ b/awx/sso/migrations/0002_expand_provider_options.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('sso', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='userenterpriseauth', + name='provider', + field=models.CharField(max_length=32, choices=[(b'radius', 'RADIUS'), (b'tacacs+', 'TACACS+'), (b'saml', 'SAML')]), + ), + ] diff --git a/awx/sso/models.py b/awx/sso/models.py index d706a2ca5c..474c3d8c57 100644 --- a/awx/sso/models.py +++ b/awx/sso/models.py @@ -13,6 +13,7 @@ class UserEnterpriseAuth(models.Model): PROVIDER_CHOICES = ( ('radius', _('RADIUS')), ('tacacs+', _('TACACS+')), + ('saml', _('SAML')), ) class Meta: diff --git a/docs/auth/README.md b/docs/auth/README.md index 67bcec8e0e..3737bf1823 100644 --- a/docs/auth/README.md +++ b/docs/auth/README.md @@ -16,9 +16,9 @@ On the other hand, the rest of authentication methods use the same types of logi Tower will try authenticating against each enabled authentication method *in the specified order*, meaning if the same username and password is valid in multiple enabled auth methods (For example, both LDAP and TACACS+), Tower will only use the first positive match (In the above example, log a user in via LDAP and skip TACACS+). ## Notes: -* RADIUS users and TACACS+ users are categorized as 'Enterprise' users. The following rules apply to Enterprise users: +* SAML users, RADIUS users and TACACS+ users are categorized as 'Enterprise' users. The following rules apply to Enterprise users: * Enterprise users can only be created via the first successful login attempt from remote authentication backend. * Enterprise users cannot be created/authenticated if non-enterprise users with the same name has already been created in Tower. * Tower passwords of Enterprise users should always be empty and cannot be set by any user if there are enterprise backends enabled. - * If enterprise backends (RADIUS and TACACS+ for now) are disabled, an Enterprise user can be converted to a normal Tower user by setting password field. But this operation is irreversible (The converted Tower user can no longer be treated as Enterprise user) + * If enterprise backends are disabled, an Enterprise user can be converted to a normal Tower user by setting password field. But this operation is irreversible (The converted Tower user can no longer be treated as Enterprise user)