Commit 05c8fa82 by Jesse Shapiro Committed by GitHub

Merge pull request #13360 from open-craft/haikuginger/sso-visible-field

[ENT-13] Make SSO providers optionally hidden
parents 6f0e0971 ce2ab5b1
...@@ -194,7 +194,7 @@ def auth_pipeline_urls(auth_entry, redirect_url=None): ...@@ -194,7 +194,7 @@ def auth_pipeline_urls(auth_entry, redirect_url=None):
return { return {
provider.provider_id: third_party_auth.pipeline.get_login_url( provider.provider_id: third_party_auth.pipeline.get_login_url(
provider.provider_id, auth_entry, redirect_url=redirect_url provider.provider_id, auth_entry, redirect_url=redirect_url
) for provider in third_party_auth.provider.Registry.accepting_logins() ) for provider in third_party_auth.provider.Registry.displayed_for_login()
} }
......
...@@ -54,8 +54,8 @@ class LoginFormTest(ThirdPartyAuthTestMixin, UrlResetMixin, SharedModuleStoreTes ...@@ -54,8 +54,8 @@ class LoginFormTest(ThirdPartyAuthTestMixin, UrlResetMixin, SharedModuleStoreTes
self.url = reverse("signin_user") self.url = reverse("signin_user")
self.course_id = unicode(self.course.id) self.course_id = unicode(self.course.id)
self.courseware_url = reverse("courseware", args=[self.course_id]) self.courseware_url = reverse("courseware", args=[self.course_id])
self.configure_google_provider(enabled=True) self.configure_google_provider(enabled=True, visible=True)
self.configure_facebook_provider(enabled=True) self.configure_facebook_provider(enabled=True, visible=True)
@patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False}) @patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False})
@ddt.data(THIRD_PARTY_AUTH_PROVIDERS) @ddt.data(THIRD_PARTY_AUTH_PROVIDERS)
...@@ -170,8 +170,8 @@ class RegisterFormTest(ThirdPartyAuthTestMixin, UrlResetMixin, SharedModuleStore ...@@ -170,8 +170,8 @@ class RegisterFormTest(ThirdPartyAuthTestMixin, UrlResetMixin, SharedModuleStore
self.url = reverse("register_user") self.url = reverse("register_user")
self.course_id = unicode(self.course.id) self.course_id = unicode(self.course.id)
self.configure_google_provider(enabled=True) self.configure_google_provider(enabled=True, visible=True)
self.configure_facebook_provider(enabled=True) self.configure_facebook_provider(enabled=True, visible=True)
@patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False}) @patch.dict(settings.FEATURES, {"ENABLE_THIRD_PARTY_AUTH": False})
@ddt.data(*THIRD_PARTY_AUTH_PROVIDERS) @ddt.data(*THIRD_PARTY_AUTH_PROVIDERS)
......
...@@ -51,7 +51,11 @@ class TpaAPITestCase(ThirdPartyAuthTestMixin, APITestCase): ...@@ -51,7 +51,11 @@ class TpaAPITestCase(ThirdPartyAuthTestMixin, APITestCase):
self.configure_facebook_provider(enabled=True) self.configure_facebook_provider(enabled=True)
self.configure_linkedin_provider(enabled=False) self.configure_linkedin_provider(enabled=False)
self.enable_saml() self.enable_saml()
testshib = self.configure_saml_provider(name='TestShib', enabled=True, idp_slug=IDP_SLUG_TESTSHIB) testshib = self.configure_saml_provider(
name='TestShib',
enabled=True,
idp_slug=IDP_SLUG_TESTSHIB
)
# Create several users and link each user to Google and TestShib # Create several users and link each user to Google and TestShib
for username in LINKED_USERS: for username in LINKED_USERS:
......
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('third_party_auth', '0001_initial'),
('third_party_auth', '0002_schema__provider_icon_image'),
('third_party_auth', '0003_samlproviderconfig_debug_mode'),
]
operations = [
migrations.AddField(
model_name='LTIProviderConfig',
name='visible',
field=models.BooleanField(
help_text=b'If this option is not selected, users will not be presented with the provider as an option to authenticate with on the login screen, but manual authentication using the correct link is still possible.',
default=True
),
preserve_default=False
),
migrations.AlterField(
model_name='LTIProviderConfig',
name='visible',
field=models.BooleanField(
help_text=b'If this option is not selected, users will not be presented with the provider as an option to authenticate with on the login screen, but manual authentication using the correct link is still possible.',
default=False
)
),
migrations.AddField(
model_name='OAuth2ProviderConfig',
name='visible',
field=models.BooleanField(
help_text=b'If this option is not selected, users will not be presented with the provider as an option to authenticate with on the login screen, but manual authentication using the correct link is still possible.',
default=True
),
preserve_default=False
),
migrations.AlterField(
model_name='OAuth2ProviderConfig',
name='visible',
field=models.BooleanField(
help_text=b'If this option is not selected, users will not be presented with the provider as an option to authenticate with on the login screen, but manual authentication using the correct link is still possible.',
default=False
)
),
migrations.AddField(
model_name='SAMLProviderConfig',
name='visible',
field=models.BooleanField(
help_text=b'If this option is not selected, users will not be presented with the provider as an option to authenticate with on the login screen, but manual authentication using the correct link is still possible.',
default=True
),
preserve_default=False
),
migrations.AlterField(
model_name='SAMLProviderConfig',
name='visible',
field=models.BooleanField(
help_text=b'If this option is not selected, users will not be presented with the provider as an option to authenticate with on the login screen, but manual authentication using the correct link is still possible.',
default=False
)
),
]
...@@ -121,6 +121,14 @@ class ProviderConfig(ConfigurationModel): ...@@ -121,6 +121,14 @@ class ProviderConfig(ConfigurationModel):
"email, and their account will be activated immediately upon registration." "email, and their account will be activated immediately upon registration."
), ),
) )
visible = models.BooleanField(
default=False,
help_text=_(
"If this option is not selected, users will not be presented with the provider "
"as an option to authenticate with on the login screen, but manual "
"authentication using the correct link is still possible."
),
)
prefix = None # used for provider_id. Set to a string value in subclass prefix = None # used for provider_id. Set to a string value in subclass
backend_name = None # Set to a field or fixed value in subclass backend_name = None # Set to a field or fixed value in subclass
accepts_logins = True # Whether to display a sign-in button when the provider is enabled accepts_logins = True # Whether to display a sign-in button when the provider is enabled
...@@ -212,6 +220,14 @@ class ProviderConfig(ConfigurationModel): ...@@ -212,6 +220,14 @@ class ProviderConfig(ConfigurationModel):
"""Gets associated Django settings.AUTHENTICATION_BACKEND string.""" """Gets associated Django settings.AUTHENTICATION_BACKEND string."""
return '{}.{}'.format(self.backend_class.__module__, self.backend_class.__name__) return '{}.{}'.format(self.backend_class.__module__, self.backend_class.__name__)
@property
def display_for_login(self):
"""
Determines whether the provider ought to be shown as an option with
which to authenticate on the login screen, registration screen, and elsewhere.
"""
return bool(self.enabled and self.accepts_logins and self.visible)
class OAuth2ProviderConfig(ProviderConfig): class OAuth2ProviderConfig(ProviderConfig):
""" """
......
...@@ -37,9 +37,9 @@ class Registry(object): ...@@ -37,9 +37,9 @@ class Registry(object):
return sorted(cls._enabled_providers(), key=lambda provider: provider.name) return sorted(cls._enabled_providers(), key=lambda provider: provider.name)
@classmethod @classmethod
def accepting_logins(cls): def displayed_for_login(cls):
"""Returns list of providers that can be used to initiate logins currently""" """Returns list of providers that can be used to initiate logins in the UI"""
return [provider for provider in cls.enabled() if provider.accepts_logins] return [provider for provider in cls.enabled() if provider.display_for_login]
@classmethod @classmethod
def get(cls, provider_id): def get(cls, provider_id):
......
...@@ -11,6 +11,7 @@ class AzureADOauth2IntegrationTest(base.Oauth2IntegrationTest): ...@@ -11,6 +11,7 @@ class AzureADOauth2IntegrationTest(base.Oauth2IntegrationTest):
super(AzureADOauth2IntegrationTest, self).setUp() super(AzureADOauth2IntegrationTest, self).setUp()
self.provider = self.configure_azure_ad_provider( self.provider = self.configure_azure_ad_provider(
enabled=True, enabled=True,
visible=True,
key='azure_ad_oauth2_key', key='azure_ad_oauth2_key',
secret='azure_ad_oauth2_secret', secret='azure_ad_oauth2_secret',
) )
......
...@@ -22,7 +22,7 @@ class GenericIntegrationTest(IntegrationTestMixin, testutil.TestCase): ...@@ -22,7 +22,7 @@ class GenericIntegrationTest(IntegrationTestMixin, testutil.TestCase):
def setUp(self): def setUp(self):
super(GenericIntegrationTest, self).setUp() super(GenericIntegrationTest, self).setUp()
self.configure_dummy_provider(enabled=True) self.configure_dummy_provider(enabled=True, visible=True)
def do_provider_login(self, provider_redirect_url): def do_provider_login(self, provider_redirect_url):
""" """
......
...@@ -19,6 +19,7 @@ class GoogleOauth2IntegrationTest(base.Oauth2IntegrationTest): ...@@ -19,6 +19,7 @@ class GoogleOauth2IntegrationTest(base.Oauth2IntegrationTest):
super(GoogleOauth2IntegrationTest, self).setUp() super(GoogleOauth2IntegrationTest, self).setUp()
self.provider = self.configure_google_provider( self.provider = self.configure_google_provider(
enabled=True, enabled=True,
visible=True,
key='google_oauth2_key', key='google_oauth2_key',
secret='google_oauth2_secret', secret='google_oauth2_secret',
) )
......
...@@ -10,6 +10,7 @@ class LinkedInOauth2IntegrationTest(base.Oauth2IntegrationTest): ...@@ -10,6 +10,7 @@ class LinkedInOauth2IntegrationTest(base.Oauth2IntegrationTest):
super(LinkedInOauth2IntegrationTest, self).setUp() super(LinkedInOauth2IntegrationTest, self).setUp()
self.provider = self.configure_linkedin_provider( self.provider = self.configure_linkedin_provider(
enabled=True, enabled=True,
visible=True,
key='linkedin_oauth2_key', key='linkedin_oauth2_key',
secret='linkedin_oauth2_secret', secret='linkedin_oauth2_secret',
) )
......
...@@ -134,6 +134,7 @@ class TestShibIntegrationTest(IntegrationTestMixin, testutil.SAMLTestCase): ...@@ -134,6 +134,7 @@ class TestShibIntegrationTest(IntegrationTestMixin, testutil.SAMLTestCase):
fetch_metadata = kwargs.pop('fetch_metadata', True) fetch_metadata = kwargs.pop('fetch_metadata', True)
kwargs.setdefault('name', self.PROVIDER_NAME) kwargs.setdefault('name', self.PROVIDER_NAME)
kwargs.setdefault('enabled', True) kwargs.setdefault('enabled', True)
kwargs.setdefault('visible', True)
kwargs.setdefault('idp_slug', self.PROVIDER_IDP_SLUG) kwargs.setdefault('idp_slug', self.PROVIDER_IDP_SLUG)
kwargs.setdefault('entity_id', TESTSHIB_ENTITY_ID) kwargs.setdefault('entity_id', TESTSHIB_ENTITY_ID)
kwargs.setdefault('metadata_source', TESTSHIB_METADATA_URL) kwargs.setdefault('metadata_source', TESTSHIB_METADATA_URL)
......
...@@ -13,6 +13,7 @@ class TwitterIntegrationTest(base.Oauth2IntegrationTest): ...@@ -13,6 +13,7 @@ class TwitterIntegrationTest(base.Oauth2IntegrationTest):
super(TwitterIntegrationTest, self).setUp() super(TwitterIntegrationTest, self).setUp()
self.provider = self.configure_twitter_provider( self.provider = self.configure_twitter_provider(
enabled=True, enabled=True,
visible=True,
key='twitter_oauth1_key', key='twitter_oauth1_key',
secret='twitter_oauth1_secret', secret='twitter_oauth1_secret',
) )
......
...@@ -48,7 +48,12 @@ class RegistryTest(testutil.TestCase): ...@@ -48,7 +48,12 @@ class RegistryTest(testutil.TestCase):
""" Test that only backend_names listed in settings.AUTHENTICATION_BACKENDS can be used """ """ Test that only backend_names listed in settings.AUTHENTICATION_BACKENDS can be used """
self.configure_oauth_provider(enabled=True, name="Disallowed", backend_name="disallowed") self.configure_oauth_provider(enabled=True, name="Disallowed", backend_name="disallowed")
self.enable_saml() self.enable_saml()
self.configure_saml_provider(enabled=True, name="Disallowed", idp_slug="test", backend_name="disallowed") self.configure_saml_provider(
enabled=True,
name="Disallowed",
idp_slug="test",
backend_name="disallowed"
)
self.assertEqual(len(provider.Registry.enabled()), 0) self.assertEqual(len(provider.Registry.enabled()), 0)
def test_enabled_returns_list_of_enabled_providers_sorted_by_name(self): def test_enabled_returns_list_of_enabled_providers_sorted_by_name(self):
...@@ -62,6 +67,23 @@ class RegistryTest(testutil.TestCase): ...@@ -62,6 +67,23 @@ class RegistryTest(testutil.TestCase):
with patch('third_party_auth.provider._PSA_OAUTH2_BACKENDS', backend_names): with patch('third_party_auth.provider._PSA_OAUTH2_BACKENDS', backend_names):
self.assertEqual(sorted(provider_names), [prov.name for prov in provider.Registry.enabled()]) self.assertEqual(sorted(provider_names), [prov.name for prov in provider.Registry.enabled()])
def test_providers_displayed_for_login(self):
"""
Tests to ensure that only providers that we can use to log in are presented
for rendering in the UI.
"""
hidden_provider = self.configure_google_provider(visible=False, enabled=True)
normal_provider = self.configure_facebook_provider(visible=True, enabled=True)
implicitly_hidden_provider = self.configure_linkedin_provider(enabled=True)
disabled_provider = self.configure_twitter_provider(visible=True, enabled=False)
no_log_in_provider = self.configure_lti_provider()
provider_ids = [idp.provider_id for idp in provider.Registry.displayed_for_login()]
self.assertNotIn(hidden_provider.provider_id, provider_ids)
self.assertNotIn(implicitly_hidden_provider.provider_id, provider_ids)
self.assertNotIn(disabled_provider.provider_id, provider_ids)
self.assertNotIn(no_log_in_provider.provider_id, provider_ids)
self.assertIn(normal_provider.provider_id, provider_ids)
def test_get_returns_enabled_provider(self): def test_get_returns_enabled_provider(self):
google_provider = self.configure_google_provider(enabled=True) google_provider = self.configure_google_provider(enabled=True)
self.assertEqual(google_provider.id, provider.Registry.get(google_provider.provider_id).id) self.assertEqual(google_provider.id, provider.Registry.get(google_provider.provider_id).id)
......
...@@ -35,9 +35,9 @@ class ThirdPartyOAuthTestMixin(ThirdPartyAuthTestMixin): ...@@ -35,9 +35,9 @@ class ThirdPartyOAuthTestMixin(ThirdPartyAuthTestMixin):
UserSocialAuth.objects.create(user=self.user, provider=self.BACKEND, uid=self.social_uid) UserSocialAuth.objects.create(user=self.user, provider=self.BACKEND, uid=self.social_uid)
self.oauth_client = self._create_client() self.oauth_client = self._create_client()
if self.BACKEND == 'google-oauth2': if self.BACKEND == 'google-oauth2':
self.configure_google_provider(enabled=True) self.configure_google_provider(enabled=True, visible=True)
elif self.BACKEND == 'facebook': elif self.BACKEND == 'facebook':
self.configure_facebook_provider(enabled=True) self.configure_facebook_provider(enabled=True, visible=True)
def _create_client(self): def _create_client(self):
""" """
......
...@@ -12,7 +12,8 @@ ...@@ -12,7 +12,8 @@
"backend_name": "google-oauth2", "backend_name": "google-oauth2",
"key": "test", "key": "test",
"secret": "test", "secret": "test",
"other_settings": "{}" "other_settings": "{}",
"visible": true
} }
}, },
{ {
...@@ -28,7 +29,8 @@ ...@@ -28,7 +29,8 @@
"backend_name": "facebook", "backend_name": "facebook",
"key": "test", "key": "test",
"secret": "test", "secret": "test",
"other_settings": "{}" "other_settings": "{}",
"visible": true
} }
}, },
{ {
...@@ -44,7 +46,8 @@ ...@@ -44,7 +46,8 @@
"backend_name": "dummy", "backend_name": "dummy",
"key": "", "key": "",
"secret": "", "secret": "",
"other_settings": "{}" "other_settings": "{}",
"visible": true
} }
} }
] ]
...@@ -237,9 +237,10 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi ...@@ -237,9 +237,10 @@ class StudentAccountLoginAndRegistrationTest(ThirdPartyAuthTestMixin, UrlResetMi
super(StudentAccountLoginAndRegistrationTest, self).setUp() super(StudentAccountLoginAndRegistrationTest, self).setUp()
# For these tests, three third party auth providers are enabled by default: # For these tests, three third party auth providers are enabled by default:
self.configure_google_provider(enabled=True) self.configure_google_provider(enabled=True, visible=True)
self.configure_facebook_provider(enabled=True) self.configure_facebook_provider(enabled=True, visible=True)
self.configure_dummy_provider( self.configure_dummy_provider(
visible=True,
enabled=True, enabled=True,
icon_class='', icon_class='',
icon_image=SimpleUploadedFile('icon.svg', '<svg><rect width="50" height="100"/></svg>'), icon_image=SimpleUploadedFile('icon.svg', '<svg><rect width="50" height="100"/></svg>'),
...@@ -482,8 +483,8 @@ class AccountSettingsViewTest(ThirdPartyAuthTestMixin, TestCase, ProgramsApiConf ...@@ -482,8 +483,8 @@ class AccountSettingsViewTest(ThirdPartyAuthTestMixin, TestCase, ProgramsApiConf
self.request.user = self.user self.request.user = self.user
# For these tests, two third party auth providers are enabled by default: # For these tests, two third party auth providers are enabled by default:
self.configure_google_provider(enabled=True) self.configure_google_provider(enabled=True, visible=True)
self.configure_facebook_provider(enabled=True) self.configure_facebook_provider(enabled=True, visible=True)
# Python-social saves auth failure notifcations in Django messages. # Python-social saves auth failure notifcations in Django messages.
# See pipeline.get_duplicate_provider() for details. # See pipeline.get_duplicate_provider() for details.
......
...@@ -203,7 +203,7 @@ def _third_party_auth_context(request, redirect_to): ...@@ -203,7 +203,7 @@ def _third_party_auth_context(request, redirect_to):
} }
if third_party_auth.is_enabled(): if third_party_auth.is_enabled():
for enabled in third_party_auth.provider.Registry.accepting_logins(): for enabled in third_party_auth.provider.Registry.displayed_for_login():
info = { info = {
"id": enabled.provider_id, "id": enabled.provider_id,
"name": enabled.name, "name": enabled.name,
...@@ -487,6 +487,8 @@ def account_settings_context(request): ...@@ -487,6 +487,8 @@ def account_settings_context(request):
# If the user is connected, sending a POST request to this url removes the connection # If the user is connected, sending a POST request to this url removes the connection
# information for this provider from their edX account. # information for this provider from their edX account.
'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id), 'disconnect_url': pipeline.get_disconnect_url(state.provider.provider_id, state.association_id),
} for state in auth_states] # We only want to include providers if they are either currently available to be logged
# in with, or if the user is already authenticated with them.
} for state in auth_states if state.provider.display_for_login or state.has_account]
return context return context
...@@ -218,7 +218,7 @@ from third_party_auth import provider, pipeline ...@@ -218,7 +218,7 @@ from third_party_auth import provider, pipeline
<div class="form-actions form-third-party-auth"> <div class="form-actions form-third-party-auth">
% for enabled in provider.Registry.accepting_logins(): % for enabled in provider.Registry.displayed_for_login():
## Translators: provider_name is the name of an external, third-party user authentication provider (like Google or LinkedIn). ## Translators: provider_name is the name of an external, third-party user authentication provider (like Google or LinkedIn).
<button type="submit" class="button button-primary button-${enabled.provider_id} login-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_url[enabled.provider_id]}');"> <button type="submit" class="button button-primary button-${enabled.provider_id} login-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_url[enabled.provider_id]}');">
% if enabled.icon_class: % if enabled.icon_class:
......
...@@ -24,7 +24,7 @@ from student.models import UserProfile ...@@ -24,7 +24,7 @@ from student.models import UserProfile
<div class="form-actions form-third-party-auth"> <div class="form-actions form-third-party-auth">
% for enabled in provider.Registry.accepting_logins(): % for enabled in provider.Registry.displayed_for_login():
## Translators: provider_name is the name of an external, third-party user authentication service (like Google or LinkedIn). ## Translators: provider_name is the name of an external, third-party user authentication service (like Google or LinkedIn).
<button type="submit" class="button button-primary button-${enabled.provider_id} register-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_urls[enabled.provider_id]}');"> <button type="submit" class="button button-primary button-${enabled.provider_id} register-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_urls[enabled.provider_id]}');">
% if enabled.icon_class: % if enabled.icon_class:
......
...@@ -33,7 +33,7 @@ from third_party_auth import pipeline ...@@ -33,7 +33,7 @@ from third_party_auth import pipeline
## Translators: clicking on this removes the link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn). ## Translators: clicking on this removes the link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
${_("Unlink")} ${_("Unlink")}
</button> </button>
% elif state.provider.accepts_logins: % elif state.provider.display_for_login:
<a href="${pipeline.get_login_url(state.provider.provider_id, pipeline.AUTH_ENTRY_PROFILE)}"> <a href="${pipeline.get_login_url(state.provider.provider_id, pipeline.AUTH_ENTRY_PROFILE)}">
## Translators: clicking on this creates a link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn). ## Translators: clicking on this creates a link between a user's edX account and their account with an external authentication provider (like Google or LinkedIn).
${_("Link")} ${_("Link")}
......
...@@ -24,7 +24,7 @@ from student.models import UserProfile ...@@ -24,7 +24,7 @@ from student.models import UserProfile
<div class="form-actions form-third-party-auth"> <div class="form-actions form-third-party-auth">
% for enabled in provider.Registry.accepting_logins(): % for enabled in provider.Registry.displayed_for_login():
## Translators: provider_name is the name of an external, third-party user authentication service (like Google or LinkedIn). ## Translators: provider_name is the name of an external, third-party user authentication service (like Google or LinkedIn).
<button type="submit" class="button button-primary button-${enabled.provider_id} register-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_urls[enabled.provider_id]}');"> <button type="submit" class="button button-primary button-${enabled.provider_id} register-${enabled.provider_id}" onclick="thirdPartySignin(event, '${pipeline_urls[enabled.provider_id]}');">
% if enabled.icon_class: % if enabled.icon_class:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment