"""Third-party auth provider definitions. Loaded by Django's settings mechanism. Consequently, this module must not invoke the Django armature. """ from social.backends import google, linkedin, facebook _DEFAULT_ICON_CLASS = 'fa-signin' class BaseProvider(object): """Abstract base class for third-party auth providers. All providers must subclass BaseProvider -- otherwise, they cannot be put in the provider Registry. """ # Class. The provider's backing social.backends.base.BaseAuth child. BACKEND_CLASS = None # String. Name of the FontAwesome glyph to use for sign in buttons (or the # name of a user-supplied custom glyph that is present at runtime). ICON_CLASS = _DEFAULT_ICON_CLASS # String. User-facing name of the provider. Must be unique across all # enabled providers. Will be presented in the UI. NAME = None # Dict of string -> object. Settings that will be merged into Django's # settings instance. In most cases the value will be None, since real # values are merged from .json files (foo.auth.json; foo.env.json) onto the # settings instance during application initialization. SETTINGS = {} @classmethod def get_authentication_backend(cls): """Gets associated Django settings.AUTHENTICATION_BACKEND string.""" return '%s.%s' % (cls.BACKEND_CLASS.__module__, cls.BACKEND_CLASS.__name__) @classmethod def get_email(cls, unused_provider_details): """Gets user's email address. Provider responses can contain arbitrary data. This method can be overridden to extract an email address from the provider details extracted by the social_details pipeline step. Args: unused_provider_details: dict of string -> string. Data about the user passed back by the provider. Returns: String or None. The user's email address, if any. """ return None @classmethod def get_name(cls, unused_provider_details): """Gets user's name. Provider responses can contain arbitrary data. This method can be overridden to extract a full name for a user from the provider details extracted by the social_details pipeline step. Args: unused_provider_details: dict of string -> string. Data about the user passed back by the provider. Returns: String or None. The user's full name, if any. """ return None @classmethod def get_register_form_data(cls, pipeline_kwargs): """Gets dict of data to display on the register form. common.djangoapps.student.views.register_user uses this to populate the new account creation form with values supplied by the user's chosen provider, preventing duplicate data entry. Args: pipeline_kwargs: dict of string -> object. Keyword arguments accumulated by the pipeline thus far. Returns: Dict of string -> string. Keys are names of form fields; values are values for that field. Where there is no value, the empty string must be used. """ # Details about the user sent back from the provider. details = pipeline_kwargs.get('details') # Get the username separately to take advantage of the de-duping logic # built into the pipeline. The provider cannot de-dupe because it can't # check the state of taken usernames in our system. Note that there is # technically a data race between the creation of this value and the # creation of the user object, so it is still possible for users to get # an error on submit. suggested_username = pipeline_kwargs.get('username') return { 'email': cls.get_email(details) or '', 'name': cls.get_name(details) or '', 'username': suggested_username, } @classmethod def merge_onto(cls, settings): """Merge class-level settings onto a django settings module.""" for key, value in cls.SETTINGS.iteritems(): setattr(settings, key, value) class GoogleOauth2(BaseProvider): """Provider for Google's Oauth2 auth system.""" BACKEND_CLASS = google.GoogleOAuth2 ICON_CLASS = 'fa-google-plus' NAME = 'Google' SETTINGS = { 'SOCIAL_AUTH_GOOGLE_OAUTH2_KEY': None, 'SOCIAL_AUTH_GOOGLE_OAUTH2_SECRET': None, } @classmethod def get_email(cls, provider_details): return provider_details.get('email') @classmethod def get_name(cls, provider_details): return provider_details.get('fullname') class LinkedInOauth2(BaseProvider): """Provider for LinkedIn's Oauth2 auth system.""" BACKEND_CLASS = linkedin.LinkedinOAuth2 ICON_CLASS = 'fa-linkedin' NAME = 'LinkedIn' SETTINGS = { 'SOCIAL_AUTH_LINKEDIN_OAUTH2_KEY': None, 'SOCIAL_AUTH_LINKEDIN_OAUTH2_SECRET': None, } @classmethod def get_email(cls, provider_details): return provider_details.get('email') @classmethod def get_name(cls, provider_details): return provider_details.get('fullname') class FacebookOauth2(BaseProvider): """Provider for LinkedIn's Oauth2 auth system.""" BACKEND_CLASS = facebook.FacebookOAuth2 ICON_CLASS = 'fa-facebook' NAME = 'Facebook' SETTINGS = { 'SOCIAL_AUTH_FACEBOOK_KEY': None, 'SOCIAL_AUTH_FACEBOOK_SECRET': None, } @classmethod def get_email(cls, provider_details): return provider_details.get('email') @classmethod def get_name(cls, provider_details): return provider_details.get('fullname') class Registry(object): """Singleton registry of third-party auth providers. Providers must subclass BaseProvider in order to be usable in the registry. """ _CONFIGURED = False _ENABLED = {} @classmethod def _check_configured(cls): """Ensures registry is configured.""" if not cls._CONFIGURED: raise RuntimeError('Registry not configured') @classmethod def _get_all(cls): """Gets all provider implementations loaded into the Python runtime.""" # BaseProvider does so have __subclassess__. pylint: disable-msg=no-member return {klass.NAME: klass for klass in BaseProvider.__subclasses__()} @classmethod def _enable(cls, provider): """Enables a single provider.""" if provider.NAME in cls._ENABLED: raise ValueError('Provider %s already enabled' % provider.NAME) cls._ENABLED[provider.NAME] = provider @classmethod def configure_once(cls, provider_names): """Configures providers. Args: provider_names: list of string. The providers to configure. Raises: ValueError: if the registry has already been configured, or if any of the passed provider_names does not have a corresponding BaseProvider child implementation. """ if cls._CONFIGURED: raise ValueError('Provider registry already configured') # Flip the bit eagerly -- configure() should not be re-callable if one # _enable call fails. cls._CONFIGURED = True for name in provider_names: all_providers = cls._get_all() if name not in all_providers: raise ValueError('No implementation found for provider ' + name) cls._enable(all_providers.get(name)) @classmethod def enabled(cls): """Returns list of enabled providers.""" cls._check_configured() return sorted(cls._ENABLED.values(), key=lambda provider: provider.NAME) @classmethod def get(cls, provider_name): """Gets provider named provider_name string if enabled, else None.""" cls._check_configured() return cls._ENABLED.get(provider_name) @classmethod def get_by_backend_name(cls, backend_name): """Gets provider (or None) by backend name. Args: backend_name: string. The python-social-auth backends.base.BaseAuth.name (for example, 'google-oauth2') to try and get a provider for. Raises: RuntimeError: if the registry has not been configured. """ cls._check_configured() for enabled in cls._ENABLED.values(): if enabled.BACKEND_CLASS.name == backend_name: return enabled @classmethod def _reset(cls): """Returns the registry to an unconfigured state; for tests only.""" cls._CONFIGURED = False cls._ENABLED = {}