src/pyams_security/plugin/social.py
changeset 2 94e76f8e9828
child 42 07229ac2497b
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/plugin/social.py	Mon Feb 23 17:55:05 2015 +0100
@@ -0,0 +1,410 @@
+#
+# Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+
+__docformat__ = 'restructuredtext'
+
+
+# import standard library
+from datetime import datetime
+
+# import interfaces
+from pyams_security.interfaces import ISocialUser, IPrincipalInfo, ISocialUsersFolderPlugin, ISecurityManager, \
+    IAuthenticatedPrincipalEvent, ISocialLoginConfiguration, ISocialLoginProviderInfo, ISocialLoginProviderConnection
+from zope.annotation.interfaces import IAnnotations
+from zope.schema.interfaces import IVocabularyRegistry, IVocabularyFactory
+from zope.traversing.interfaces import ITraversable
+
+# import packages
+from authomatic.providers import oauth1, oauth2
+from persistent import Persistent
+from pyams_security.principal import PrincipalInfo
+from pyams_utils.adapter import adapter_config, ContextAdapter
+from pyams_utils.registry import query_utility
+from pyams_utils.request import check_request
+from pyramid.events import subscriber
+from zope.container.contained import Contained
+from zope.container.folder import Folder
+from zope.interface import implementer, provider
+from zope.lifecycleevent import ObjectCreatedEvent
+from zope.location import locate
+from zope.schema.fieldproperty import FieldProperty
+from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry
+
+
+@implementer(ISocialUser)
+class SocialUser(Persistent, Contained):
+    """Social user persistent class"""
+
+    user_id = FieldProperty(ISocialUser['user_id'])
+    provider_name = FieldProperty(ISocialUser['provider_name'])
+    username = FieldProperty(ISocialUser['username'])
+    name = FieldProperty(ISocialUser['name'])
+    first_name = FieldProperty(ISocialUser['first_name'])
+    last_name = FieldProperty(ISocialUser['last_name'])
+    nickname = FieldProperty(ISocialUser['nickname'])
+    email = FieldProperty(ISocialUser['email'])
+    timezone = FieldProperty(ISocialUser['timezone'])
+    country = FieldProperty(ISocialUser['country'])
+    city = FieldProperty(ISocialUser['city'])
+    postal_code = FieldProperty(ISocialUser['postal_code'])
+    locale = FieldProperty(ISocialUser['locale'])
+    picture = FieldProperty(ISocialUser['picture'])
+    birth_date = FieldProperty(ISocialUser['birth_date'])
+    registration_date = FieldProperty(ISocialUser['registration_date'])
+
+    @property
+    def title(self):
+        if self.name:
+            result = self.name
+        elif self.first_name:
+            result = '{first} {last}'.format(self.first_name, self.last_name or '')
+        elif self.username:
+            result = self.username
+        else:
+            result = self.nickname or self.user_id
+        return result
+
+    @property
+    def title_with_source(self):
+        return '{title} ({provider})'.format(title=self.title,
+                                             provider=self.provider_name.capitalize())
+
+
+@adapter_config(context=ISocialUser, provides=IPrincipalInfo)
+def SocialUserPrincipalInfoAdapter(user):
+    """Social user principal info adapter"""
+    return PrincipalInfo(id="{0}:{1}".format(user.__parent__.prefix, user.user_id),
+                         title=user.name)
+
+
+@implementer(ISocialUsersFolderPlugin)
+class SocialUsersFolder(Folder):
+    """Social users folder"""
+
+    prefix = FieldProperty(ISocialUsersFolderPlugin['prefix'])
+    title = FieldProperty(ISocialUsersFolderPlugin['title'])
+    enabled = FieldProperty(ISocialUsersFolderPlugin['enabled'])
+
+    def get_principal(self, principal_id):
+        if not self.enabled:
+            return None
+        if not principal_id.startswith(self.prefix + ':'):
+            return None
+        prefix, login = principal_id.split(':', 1)
+        user = self.get(login)
+        if user is not None:
+            return PrincipalInfo(id='{prefix}:{user_id}'.format(prefix=self.prefix,
+                                                                user_id=user.user_id),
+                                 title=user.title)
+
+    def get_all_principals(self, principal_id):
+        if not self.enabled:
+            return set()
+        if self.get_principal(principal_id) is not None:
+            return {principal_id}
+        return set()
+
+    def find_principals(self, query):
+        if not self.enabled:
+            raise StopIteration
+        # TODO: use inner text catalog for more efficient search?
+        if not query:
+            return None
+        query = query.lower()
+        for user in self.values():
+            if (query == user.user_id or
+                    query in (user.name or '').lower() or
+                    query in (user.email or '').lower()):
+                yield PrincipalInfo(id='{0}:{1}'.format(self.prefix, user.user_id),
+                                    title=user.title_with_source)
+
+    def get_search_results(self, data):
+        # TODO: use inner text catalog for more efficient search?
+        query = data.get('query')
+        if not query:
+            return ()
+        query = query.lower()
+        for user in self.values():
+            if (query == user.user_id or
+                    query in (user.name or '').lower() or
+                    query in (user.email or '').lower()):
+                yield user
+
+
+@provider(IVocabularyRegistry)
+class SocialUsersFolderVocabulary(SimpleVocabulary):
+    """'PyAMS users folders' vocabulary"""
+
+    def __init__(self, *args, **kwargs):
+        terms = []
+        manager = query_utility(ISecurityManager)
+        if manager is not None:
+            for name, plugin in manager.items():
+                if ISocialUsersFolderPlugin.providedBy(plugin):
+                    terms.append(SimpleTerm(name, title=plugin.title))
+        super(SocialUsersFolderVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS social users folders', SocialUsersFolderVocabulary)
+
+
+@subscriber(IAuthenticatedPrincipalEvent, plugin_selector='oauth')
+def handle_authenticated_principal(event):
+    """Handle authenticated social principal"""
+    manager = query_utility(ISecurityManager)
+    social_folder = manager.get(manager.social_users_folder)
+    if social_folder is not None:
+        infos = event.infos
+        if not (infos and
+                'provider_name' in infos and
+                'user' in infos):
+            return
+        user = infos['user']
+        principal_id = event.principal_id
+        if principal_id not in social_folder:
+            social_user = SocialUser()
+            check_request().registry.notify(ObjectCreatedEvent(social_user))
+            social_user.user_id = principal_id
+            social_user.provider_name = infos['provider_name']
+            social_user.username = user.username
+            social_user.name = user.name
+            social_user.first_name = user.first_name
+            social_user.last_name = user.last_name
+            social_user.nickname = user.nickname
+            social_user.email = user.email
+            social_user.timezone = str(user.timezone)
+            social_user.country = user.country
+            social_user.city = user.city
+            social_user.postal_code = user.postal_code
+            social_user.locale = user.locale
+            social_user.picture = user.picture
+            if isinstance(user.birth_date, datetime):
+                social_user.birth_date = user.birth_date
+            social_user.registration_date = datetime.utcnow()
+            social_folder[principal_id] = social_user
+
+
+#
+# OAuth providers configuration
+#
+
+@implementer(ISocialLoginProviderInfo)
+class SocialLoginProviderInfo(object):
+    """Social login provider info"""
+
+    name = FieldProperty(ISocialLoginProviderInfo['name'])
+    provider = None
+    icon_class = FieldProperty(ISocialLoginProviderInfo['icon_class'])
+    icon_filename = FieldProperty(ISocialLoginProviderInfo['icon_filename'])
+    scope = FieldProperty(ISocialLoginProviderInfo['scope'])
+
+    def __init__(self, name, provider, **kwargs):
+        self.name = name
+        self.provider = provider
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+
+
+PROVIDERS_INFO = {'behance': SocialLoginProviderInfo(name=oauth2.Behance.__name__,
+                                                     provider=oauth2.Behance,
+                                                     icon_class='fa fa-fw fa-behance-square',
+                                                     icon_filename='behance.ico',
+                                                     scope=oauth2.Behance.user_info_scope),
+                  'bitbucket': SocialLoginProviderInfo(name=oauth1.Bitbucket.__name__,
+                                                       provider=oauth1.Bitbucket,
+                                                       icon_class='fa fa-fw fa-bitbucket',
+                                                       icon_filename='bitbucket.ico'),
+                  'bitly': SocialLoginProviderInfo(name=oauth2.Bitly.__name__,
+                                                   provider=oauth2.Bitly,
+                                                   icon_class='fa fa-fw fa-share-alt',
+                                                   icon_filename='bitly.ico',
+                                                   scope=oauth2.Bitly.user_info_scope),
+                  'cosm': SocialLoginProviderInfo(name=oauth2.Cosm.__name__,
+                                                  provider=oauth2.Cosm,
+                                                  icon_class='fa fa-fw fa-share-alt',
+                                                  icon_filename='cosm.ico',
+                                                  scope=oauth2.Cosm.user_info_scope),
+                  'devianart': SocialLoginProviderInfo(name=oauth2.DeviantART.__name__,
+                                                       provider=oauth2.DeviantART,
+                                                       icon_class='fa fa-fw fa-deviantart',
+                                                       icon_filename='deviantart.ico',
+                                                       scope=oauth2.DeviantART.user_info_scope),
+                  'facebook': SocialLoginProviderInfo(name=oauth2.Facebook.__name__,
+                                                      provider=oauth2.Facebook,
+                                                      icon_class='fa fa-fw fa-facebook-square',
+                                                      icon_filename='facebook.ico',
+                                                      scope=oauth2.Facebook.user_info_scope),
+                  'foursquare': SocialLoginProviderInfo(name=oauth2.Foursquare.__name__,
+                                                        provider=oauth2.Foursquare,
+                                                        icon_class='fa fa-fw fa-foursquare',
+                                                        icon_filename='foursquare.ico',
+                                                        scope=oauth2.Foursquare.user_info_scope),
+                  'flickr': SocialLoginProviderInfo(name=oauth1.Flickr.__name__,
+                                                    provider=oauth1.Flickr,
+                                                    icon_class='fa fa-fw fa-flickr',
+                                                    icon_filename='flickr.ico'),
+                  'github': SocialLoginProviderInfo(name=oauth2.GitHub.__name__,
+                                                    provider=oauth2.GitHub,
+                                                    icon_class='fa fa-fw fa-github',
+                                                    icon_filename='github.ico',
+                                                    scope=oauth2.GitHub.user_info_scope),
+                  'google': SocialLoginProviderInfo(name=oauth2.Google.__name__,
+                                                    provider=oauth2.Google,
+                                                    icon_class='fa fa-fw fa-google-plus',
+                                                    icon_filename='google.ico',
+                                                    scope=oauth2.Google.user_info_scope),
+                  'linkedin': SocialLoginProviderInfo(name=oauth2.LinkedIn.__name__,
+                                                      provider=oauth2.LinkedIn,
+                                                      icon_class='fa fa-fw fa-linkedin-square',
+                                                      icon_filename='linkedin.ico',
+                                                      scope=oauth2.LinkedIn.user_info_scope),
+                  'meetup': SocialLoginProviderInfo(name=oauth1.Meetup.__name__,
+                                                    provider=oauth1.Meetup,
+                                                    icon_class='fa fa-fw fa-share-alt',
+                                                    icon_filename='meetup.ico'),
+                  'paypal': SocialLoginProviderInfo(name=oauth2.PayPal.__name__,
+                                                    provider=oauth2.PayPal,
+                                                    icon_class='fa fa-fw fa-paypal',
+                                                    icon_filename='paypal.ico',
+                                                    scope=oauth2.PayPal.user_info_scope),
+                  'plurk': SocialLoginProviderInfo(name=oauth1.Plurk.__name__,
+                                                   provider=oauth1.Plurk,
+                                                   icon_class='fa fa-fw fa-share-alt',
+                                                   icon_filename='plurk.ico'),
+                  'reddit': SocialLoginProviderInfo(name=oauth2.Reddit.__name__,
+                                                    provider=oauth2.Reddit,
+                                                    icon_class='fa fa-fw fa-reddit',
+                                                    icon_filename='reddit.ico',
+                                                    scope=oauth2.Reddit.user_info_scope),
+                  'twitter': SocialLoginProviderInfo(name=oauth1.Twitter.__name__,
+                                                     provider=oauth1.Twitter,
+                                                     icon_class='fa fa-fw fa-twitter',
+                                                     icon_filename='twitter.ico'),
+                  'tumblr': SocialLoginProviderInfo(name=oauth1.Tumblr.__name__,
+                                                    provider=oauth1.Tumblr,
+                                                    icon_class='fa fa-fw fa-tumblr-square',
+                                                    icon_filename='tumblr.ico'),
+                  'ubuntuone': SocialLoginProviderInfo(name=oauth1.UbuntuOne.__name__,
+                                                       provider=oauth1.UbuntuOne,
+                                                       icon_class='fa fa-fw fa-share-alt',
+                                                       icon_filename='ubuntuone.ico'),
+                  'viadeo': SocialLoginProviderInfo(name=oauth2.Viadeo.__name__,
+                                                    provider=oauth2.Viadeo,
+                                                    icon_class='fa fa-fw fa-share-alt',
+                                                    icon_filename='viadeo.ico',
+                                                    scope=oauth2.Viadeo.user_info_scope),
+                  'vimeo': SocialLoginProviderInfo(name=oauth1.Vimeo.__name__,
+                                                   provider=oauth1.Vimeo,
+                                                   icon_class='fa fa-fw fa-vimeo-square',
+                                                   icon_filename='vimeo.ico'),
+                  'vk': SocialLoginProviderInfo(name=oauth2.VK.__name__,
+                                                provider=oauth2.VK,
+                                                icon_class='fa fa-fw fa-vk',
+                                                icon_filename='vk.ico',
+                                                scope=oauth2.VK.user_info_scope),
+                  'windowlive': SocialLoginProviderInfo(name=oauth2.WindowsLive.__name__,
+                                                        provider=oauth2.WindowsLive,
+                                                        icon_class='fa fa-fw fa-windows',
+                                                        icon_filename='windows_live.ico',
+                                                        scope=oauth2.WindowsLive.user_info_scope),
+                  'xero': SocialLoginProviderInfo(name=oauth1.Xero.__name__,
+                                                  provider=oauth1.Xero,
+                                                  icon_class='fa fa-fw fa-share-alt',
+                                                  icon_filename='xero.ico'),
+                  'xing': SocialLoginProviderInfo(name=oauth1.Xing.__name__,
+                                                  provider=oauth1.Xing,
+                                                  icon_class='fa fa-fw fa-xing',
+                                                  icon_filename='xing.ico'),
+                  'yahoo': SocialLoginProviderInfo(name=oauth1.Yahoo.__name__,
+                                                   provider=oauth1.Yahoo,
+                                                   icon_class='fa fa-fw fa-yahoo',
+                                                   icon_filename='yahoo.ico'),
+                  'yammer': SocialLoginProviderInfo(name=oauth2.Yammer.__name__,
+                                                    provider=oauth2.Yammer,
+                                                    icon_class='fa fa-fw fa-share-alt',
+                                                    icon_filename='yammer.ico',
+                                                    scope=oauth2.Yammer.user_info_scope),
+                  'yandex': SocialLoginProviderInfo(name=oauth2.Yandex.__name__,
+                                                    provider=oauth2.Yandex,
+                                                    icon_class='fa fa-fw fa-share-alt',
+                                                    icon_filename='yandex.ico',
+                                                    scope=oauth2.Yandex.user_info_scope)}
+
+
+def get_provider_info(provider_name):
+    """Get provider info matching given provider name"""
+    return PROVIDERS_INFO.get(provider_name)
+
+
+@provider(IVocabularyFactory)
+class OAuthProvidersVocabulary(SimpleVocabulary):
+    """OAuth providers vocabulary"""
+
+    def __init__(self, *args, **kwargs):
+        terms = []
+        for key, provider in PROVIDERS_INFO.items():
+            terms.append(SimpleTerm(key, title=provider.name))
+        terms.sort(key=lambda x: x.title)
+        super(OAuthProvidersVocabulary, self).__init__(terms)
+
+getVocabularyRegistry().register('PyAMS OAuth providers', OAuthProvidersVocabulary)
+
+
+@implementer(ISocialLoginConfiguration)
+class SocialLoginConfiguration(Folder):
+    """Social login configuration"""
+
+    def get_oauth_configuration(self):
+        result = {}
+        for provider in self.values():
+            provider_info = get_provider_info(provider.provider_name)
+            result[provider.provider_name] = {'id': provider.provider_id,
+                                              'class_': provider_info.provider,
+                                              'consumer_key': provider.consumer_key,
+                                              'consumer_secret': provider.consumer_secret,
+                                              'scope': provider_info.scope}
+        return result
+
+
+SOCIAL_LOGIN_CONFIGURATION_KEY = 'pyams_security.plugin.social'
+
+
+@adapter_config(context=ISecurityManager, provides=ISocialLoginConfiguration)
+def SocialLoginConfigurationAdapter(context):
+    """Social login configuration adapter"""
+    annotations = IAnnotations(context)
+    configuration = annotations.get(SOCIAL_LOGIN_CONFIGURATION_KEY)
+    if configuration is None:
+        configuration = annotations[SOCIAL_LOGIN_CONFIGURATION_KEY] = SocialLoginConfiguration()
+        check_request().registry.notify(ObjectCreatedEvent(configuration))
+        locate(configuration, context, '++social-configuration++')
+    return configuration
+
+
+@adapter_config(name='social-configuration', context=ISecurityManager, provides=ITraversable)
+class SecurityManagerSocialTraverser(ContextAdapter):
+    """++social-configuration++ namespace traverser"""
+
+    def traverse(self, name, furtherpath=None):
+        return ISocialLoginConfiguration(self.context)
+
+
+@implementer(ISocialLoginProviderConnection)
+class SocialLoginProviderConnection(Persistent):
+    """Social login provider connection"""
+
+    provider_name = FieldProperty(ISocialLoginProviderConnection['provider_name'])
+    provider_id = FieldProperty(ISocialLoginProviderConnection['provider_id'])
+    consumer_key = FieldProperty(ISocialLoginProviderConnection['consumer_key'])
+    consumer_secret = FieldProperty(ISocialLoginProviderConnection['consumer_secret'])
+
+    def get_configuration(self):
+        return get_provider_info(self.provider_name)