src/pyams_security/plugin/social.py
changeset 2 94e76f8e9828
child 42 07229ac2497b
equal deleted inserted replaced
1:5595823c66f1 2:94e76f8e9828
       
     1 #
       
     2 # Copyright (c) 2008-2015 Thierry Florac <tflorac AT ulthar.net>
       
     3 # All Rights Reserved.
       
     4 #
       
     5 # This software is subject to the provisions of the Zope Public License,
       
     6 # Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
       
     7 # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
       
     8 # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
       
     9 # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
       
    10 # FOR A PARTICULAR PURPOSE.
       
    11 #
       
    12 
       
    13 __docformat__ = 'restructuredtext'
       
    14 
       
    15 
       
    16 # import standard library
       
    17 from datetime import datetime
       
    18 
       
    19 # import interfaces
       
    20 from pyams_security.interfaces import ISocialUser, IPrincipalInfo, ISocialUsersFolderPlugin, ISecurityManager, \
       
    21     IAuthenticatedPrincipalEvent, ISocialLoginConfiguration, ISocialLoginProviderInfo, ISocialLoginProviderConnection
       
    22 from zope.annotation.interfaces import IAnnotations
       
    23 from zope.schema.interfaces import IVocabularyRegistry, IVocabularyFactory
       
    24 from zope.traversing.interfaces import ITraversable
       
    25 
       
    26 # import packages
       
    27 from authomatic.providers import oauth1, oauth2
       
    28 from persistent import Persistent
       
    29 from pyams_security.principal import PrincipalInfo
       
    30 from pyams_utils.adapter import adapter_config, ContextAdapter
       
    31 from pyams_utils.registry import query_utility
       
    32 from pyams_utils.request import check_request
       
    33 from pyramid.events import subscriber
       
    34 from zope.container.contained import Contained
       
    35 from zope.container.folder import Folder
       
    36 from zope.interface import implementer, provider
       
    37 from zope.lifecycleevent import ObjectCreatedEvent
       
    38 from zope.location import locate
       
    39 from zope.schema.fieldproperty import FieldProperty
       
    40 from zope.schema.vocabulary import SimpleVocabulary, SimpleTerm, getVocabularyRegistry
       
    41 
       
    42 
       
    43 @implementer(ISocialUser)
       
    44 class SocialUser(Persistent, Contained):
       
    45     """Social user persistent class"""
       
    46 
       
    47     user_id = FieldProperty(ISocialUser['user_id'])
       
    48     provider_name = FieldProperty(ISocialUser['provider_name'])
       
    49     username = FieldProperty(ISocialUser['username'])
       
    50     name = FieldProperty(ISocialUser['name'])
       
    51     first_name = FieldProperty(ISocialUser['first_name'])
       
    52     last_name = FieldProperty(ISocialUser['last_name'])
       
    53     nickname = FieldProperty(ISocialUser['nickname'])
       
    54     email = FieldProperty(ISocialUser['email'])
       
    55     timezone = FieldProperty(ISocialUser['timezone'])
       
    56     country = FieldProperty(ISocialUser['country'])
       
    57     city = FieldProperty(ISocialUser['city'])
       
    58     postal_code = FieldProperty(ISocialUser['postal_code'])
       
    59     locale = FieldProperty(ISocialUser['locale'])
       
    60     picture = FieldProperty(ISocialUser['picture'])
       
    61     birth_date = FieldProperty(ISocialUser['birth_date'])
       
    62     registration_date = FieldProperty(ISocialUser['registration_date'])
       
    63 
       
    64     @property
       
    65     def title(self):
       
    66         if self.name:
       
    67             result = self.name
       
    68         elif self.first_name:
       
    69             result = '{first} {last}'.format(self.first_name, self.last_name or '')
       
    70         elif self.username:
       
    71             result = self.username
       
    72         else:
       
    73             result = self.nickname or self.user_id
       
    74         return result
       
    75 
       
    76     @property
       
    77     def title_with_source(self):
       
    78         return '{title} ({provider})'.format(title=self.title,
       
    79                                              provider=self.provider_name.capitalize())
       
    80 
       
    81 
       
    82 @adapter_config(context=ISocialUser, provides=IPrincipalInfo)
       
    83 def SocialUserPrincipalInfoAdapter(user):
       
    84     """Social user principal info adapter"""
       
    85     return PrincipalInfo(id="{0}:{1}".format(user.__parent__.prefix, user.user_id),
       
    86                          title=user.name)
       
    87 
       
    88 
       
    89 @implementer(ISocialUsersFolderPlugin)
       
    90 class SocialUsersFolder(Folder):
       
    91     """Social users folder"""
       
    92 
       
    93     prefix = FieldProperty(ISocialUsersFolderPlugin['prefix'])
       
    94     title = FieldProperty(ISocialUsersFolderPlugin['title'])
       
    95     enabled = FieldProperty(ISocialUsersFolderPlugin['enabled'])
       
    96 
       
    97     def get_principal(self, principal_id):
       
    98         if not self.enabled:
       
    99             return None
       
   100         if not principal_id.startswith(self.prefix + ':'):
       
   101             return None
       
   102         prefix, login = principal_id.split(':', 1)
       
   103         user = self.get(login)
       
   104         if user is not None:
       
   105             return PrincipalInfo(id='{prefix}:{user_id}'.format(prefix=self.prefix,
       
   106                                                                 user_id=user.user_id),
       
   107                                  title=user.title)
       
   108 
       
   109     def get_all_principals(self, principal_id):
       
   110         if not self.enabled:
       
   111             return set()
       
   112         if self.get_principal(principal_id) is not None:
       
   113             return {principal_id}
       
   114         return set()
       
   115 
       
   116     def find_principals(self, query):
       
   117         if not self.enabled:
       
   118             raise StopIteration
       
   119         # TODO: use inner text catalog for more efficient search?
       
   120         if not query:
       
   121             return None
       
   122         query = query.lower()
       
   123         for user in self.values():
       
   124             if (query == user.user_id or
       
   125                     query in (user.name or '').lower() or
       
   126                     query in (user.email or '').lower()):
       
   127                 yield PrincipalInfo(id='{0}:{1}'.format(self.prefix, user.user_id),
       
   128                                     title=user.title_with_source)
       
   129 
       
   130     def get_search_results(self, data):
       
   131         # TODO: use inner text catalog for more efficient search?
       
   132         query = data.get('query')
       
   133         if not query:
       
   134             return ()
       
   135         query = query.lower()
       
   136         for user in self.values():
       
   137             if (query == user.user_id or
       
   138                     query in (user.name or '').lower() or
       
   139                     query in (user.email or '').lower()):
       
   140                 yield user
       
   141 
       
   142 
       
   143 @provider(IVocabularyRegistry)
       
   144 class SocialUsersFolderVocabulary(SimpleVocabulary):
       
   145     """'PyAMS users folders' vocabulary"""
       
   146 
       
   147     def __init__(self, *args, **kwargs):
       
   148         terms = []
       
   149         manager = query_utility(ISecurityManager)
       
   150         if manager is not None:
       
   151             for name, plugin in manager.items():
       
   152                 if ISocialUsersFolderPlugin.providedBy(plugin):
       
   153                     terms.append(SimpleTerm(name, title=plugin.title))
       
   154         super(SocialUsersFolderVocabulary, self).__init__(terms)
       
   155 
       
   156 getVocabularyRegistry().register('PyAMS social users folders', SocialUsersFolderVocabulary)
       
   157 
       
   158 
       
   159 @subscriber(IAuthenticatedPrincipalEvent, plugin_selector='oauth')
       
   160 def handle_authenticated_principal(event):
       
   161     """Handle authenticated social principal"""
       
   162     manager = query_utility(ISecurityManager)
       
   163     social_folder = manager.get(manager.social_users_folder)
       
   164     if social_folder is not None:
       
   165         infos = event.infos
       
   166         if not (infos and
       
   167                 'provider_name' in infos and
       
   168                 'user' in infos):
       
   169             return
       
   170         user = infos['user']
       
   171         principal_id = event.principal_id
       
   172         if principal_id not in social_folder:
       
   173             social_user = SocialUser()
       
   174             check_request().registry.notify(ObjectCreatedEvent(social_user))
       
   175             social_user.user_id = principal_id
       
   176             social_user.provider_name = infos['provider_name']
       
   177             social_user.username = user.username
       
   178             social_user.name = user.name
       
   179             social_user.first_name = user.first_name
       
   180             social_user.last_name = user.last_name
       
   181             social_user.nickname = user.nickname
       
   182             social_user.email = user.email
       
   183             social_user.timezone = str(user.timezone)
       
   184             social_user.country = user.country
       
   185             social_user.city = user.city
       
   186             social_user.postal_code = user.postal_code
       
   187             social_user.locale = user.locale
       
   188             social_user.picture = user.picture
       
   189             if isinstance(user.birth_date, datetime):
       
   190                 social_user.birth_date = user.birth_date
       
   191             social_user.registration_date = datetime.utcnow()
       
   192             social_folder[principal_id] = social_user
       
   193 
       
   194 
       
   195 #
       
   196 # OAuth providers configuration
       
   197 #
       
   198 
       
   199 @implementer(ISocialLoginProviderInfo)
       
   200 class SocialLoginProviderInfo(object):
       
   201     """Social login provider info"""
       
   202 
       
   203     name = FieldProperty(ISocialLoginProviderInfo['name'])
       
   204     provider = None
       
   205     icon_class = FieldProperty(ISocialLoginProviderInfo['icon_class'])
       
   206     icon_filename = FieldProperty(ISocialLoginProviderInfo['icon_filename'])
       
   207     scope = FieldProperty(ISocialLoginProviderInfo['scope'])
       
   208 
       
   209     def __init__(self, name, provider, **kwargs):
       
   210         self.name = name
       
   211         self.provider = provider
       
   212         for k, v in kwargs.items():
       
   213             setattr(self, k, v)
       
   214 
       
   215 
       
   216 PROVIDERS_INFO = {'behance': SocialLoginProviderInfo(name=oauth2.Behance.__name__,
       
   217                                                      provider=oauth2.Behance,
       
   218                                                      icon_class='fa fa-fw fa-behance-square',
       
   219                                                      icon_filename='behance.ico',
       
   220                                                      scope=oauth2.Behance.user_info_scope),
       
   221                   'bitbucket': SocialLoginProviderInfo(name=oauth1.Bitbucket.__name__,
       
   222                                                        provider=oauth1.Bitbucket,
       
   223                                                        icon_class='fa fa-fw fa-bitbucket',
       
   224                                                        icon_filename='bitbucket.ico'),
       
   225                   'bitly': SocialLoginProviderInfo(name=oauth2.Bitly.__name__,
       
   226                                                    provider=oauth2.Bitly,
       
   227                                                    icon_class='fa fa-fw fa-share-alt',
       
   228                                                    icon_filename='bitly.ico',
       
   229                                                    scope=oauth2.Bitly.user_info_scope),
       
   230                   'cosm': SocialLoginProviderInfo(name=oauth2.Cosm.__name__,
       
   231                                                   provider=oauth2.Cosm,
       
   232                                                   icon_class='fa fa-fw fa-share-alt',
       
   233                                                   icon_filename='cosm.ico',
       
   234                                                   scope=oauth2.Cosm.user_info_scope),
       
   235                   'devianart': SocialLoginProviderInfo(name=oauth2.DeviantART.__name__,
       
   236                                                        provider=oauth2.DeviantART,
       
   237                                                        icon_class='fa fa-fw fa-deviantart',
       
   238                                                        icon_filename='deviantart.ico',
       
   239                                                        scope=oauth2.DeviantART.user_info_scope),
       
   240                   'facebook': SocialLoginProviderInfo(name=oauth2.Facebook.__name__,
       
   241                                                       provider=oauth2.Facebook,
       
   242                                                       icon_class='fa fa-fw fa-facebook-square',
       
   243                                                       icon_filename='facebook.ico',
       
   244                                                       scope=oauth2.Facebook.user_info_scope),
       
   245                   'foursquare': SocialLoginProviderInfo(name=oauth2.Foursquare.__name__,
       
   246                                                         provider=oauth2.Foursquare,
       
   247                                                         icon_class='fa fa-fw fa-foursquare',
       
   248                                                         icon_filename='foursquare.ico',
       
   249                                                         scope=oauth2.Foursquare.user_info_scope),
       
   250                   'flickr': SocialLoginProviderInfo(name=oauth1.Flickr.__name__,
       
   251                                                     provider=oauth1.Flickr,
       
   252                                                     icon_class='fa fa-fw fa-flickr',
       
   253                                                     icon_filename='flickr.ico'),
       
   254                   'github': SocialLoginProviderInfo(name=oauth2.GitHub.__name__,
       
   255                                                     provider=oauth2.GitHub,
       
   256                                                     icon_class='fa fa-fw fa-github',
       
   257                                                     icon_filename='github.ico',
       
   258                                                     scope=oauth2.GitHub.user_info_scope),
       
   259                   'google': SocialLoginProviderInfo(name=oauth2.Google.__name__,
       
   260                                                     provider=oauth2.Google,
       
   261                                                     icon_class='fa fa-fw fa-google-plus',
       
   262                                                     icon_filename='google.ico',
       
   263                                                     scope=oauth2.Google.user_info_scope),
       
   264                   'linkedin': SocialLoginProviderInfo(name=oauth2.LinkedIn.__name__,
       
   265                                                       provider=oauth2.LinkedIn,
       
   266                                                       icon_class='fa fa-fw fa-linkedin-square',
       
   267                                                       icon_filename='linkedin.ico',
       
   268                                                       scope=oauth2.LinkedIn.user_info_scope),
       
   269                   'meetup': SocialLoginProviderInfo(name=oauth1.Meetup.__name__,
       
   270                                                     provider=oauth1.Meetup,
       
   271                                                     icon_class='fa fa-fw fa-share-alt',
       
   272                                                     icon_filename='meetup.ico'),
       
   273                   'paypal': SocialLoginProviderInfo(name=oauth2.PayPal.__name__,
       
   274                                                     provider=oauth2.PayPal,
       
   275                                                     icon_class='fa fa-fw fa-paypal',
       
   276                                                     icon_filename='paypal.ico',
       
   277                                                     scope=oauth2.PayPal.user_info_scope),
       
   278                   'plurk': SocialLoginProviderInfo(name=oauth1.Plurk.__name__,
       
   279                                                    provider=oauth1.Plurk,
       
   280                                                    icon_class='fa fa-fw fa-share-alt',
       
   281                                                    icon_filename='plurk.ico'),
       
   282                   'reddit': SocialLoginProviderInfo(name=oauth2.Reddit.__name__,
       
   283                                                     provider=oauth2.Reddit,
       
   284                                                     icon_class='fa fa-fw fa-reddit',
       
   285                                                     icon_filename='reddit.ico',
       
   286                                                     scope=oauth2.Reddit.user_info_scope),
       
   287                   'twitter': SocialLoginProviderInfo(name=oauth1.Twitter.__name__,
       
   288                                                      provider=oauth1.Twitter,
       
   289                                                      icon_class='fa fa-fw fa-twitter',
       
   290                                                      icon_filename='twitter.ico'),
       
   291                   'tumblr': SocialLoginProviderInfo(name=oauth1.Tumblr.__name__,
       
   292                                                     provider=oauth1.Tumblr,
       
   293                                                     icon_class='fa fa-fw fa-tumblr-square',
       
   294                                                     icon_filename='tumblr.ico'),
       
   295                   'ubuntuone': SocialLoginProviderInfo(name=oauth1.UbuntuOne.__name__,
       
   296                                                        provider=oauth1.UbuntuOne,
       
   297                                                        icon_class='fa fa-fw fa-share-alt',
       
   298                                                        icon_filename='ubuntuone.ico'),
       
   299                   'viadeo': SocialLoginProviderInfo(name=oauth2.Viadeo.__name__,
       
   300                                                     provider=oauth2.Viadeo,
       
   301                                                     icon_class='fa fa-fw fa-share-alt',
       
   302                                                     icon_filename='viadeo.ico',
       
   303                                                     scope=oauth2.Viadeo.user_info_scope),
       
   304                   'vimeo': SocialLoginProviderInfo(name=oauth1.Vimeo.__name__,
       
   305                                                    provider=oauth1.Vimeo,
       
   306                                                    icon_class='fa fa-fw fa-vimeo-square',
       
   307                                                    icon_filename='vimeo.ico'),
       
   308                   'vk': SocialLoginProviderInfo(name=oauth2.VK.__name__,
       
   309                                                 provider=oauth2.VK,
       
   310                                                 icon_class='fa fa-fw fa-vk',
       
   311                                                 icon_filename='vk.ico',
       
   312                                                 scope=oauth2.VK.user_info_scope),
       
   313                   'windowlive': SocialLoginProviderInfo(name=oauth2.WindowsLive.__name__,
       
   314                                                         provider=oauth2.WindowsLive,
       
   315                                                         icon_class='fa fa-fw fa-windows',
       
   316                                                         icon_filename='windows_live.ico',
       
   317                                                         scope=oauth2.WindowsLive.user_info_scope),
       
   318                   'xero': SocialLoginProviderInfo(name=oauth1.Xero.__name__,
       
   319                                                   provider=oauth1.Xero,
       
   320                                                   icon_class='fa fa-fw fa-share-alt',
       
   321                                                   icon_filename='xero.ico'),
       
   322                   'xing': SocialLoginProviderInfo(name=oauth1.Xing.__name__,
       
   323                                                   provider=oauth1.Xing,
       
   324                                                   icon_class='fa fa-fw fa-xing',
       
   325                                                   icon_filename='xing.ico'),
       
   326                   'yahoo': SocialLoginProviderInfo(name=oauth1.Yahoo.__name__,
       
   327                                                    provider=oauth1.Yahoo,
       
   328                                                    icon_class='fa fa-fw fa-yahoo',
       
   329                                                    icon_filename='yahoo.ico'),
       
   330                   'yammer': SocialLoginProviderInfo(name=oauth2.Yammer.__name__,
       
   331                                                     provider=oauth2.Yammer,
       
   332                                                     icon_class='fa fa-fw fa-share-alt',
       
   333                                                     icon_filename='yammer.ico',
       
   334                                                     scope=oauth2.Yammer.user_info_scope),
       
   335                   'yandex': SocialLoginProviderInfo(name=oauth2.Yandex.__name__,
       
   336                                                     provider=oauth2.Yandex,
       
   337                                                     icon_class='fa fa-fw fa-share-alt',
       
   338                                                     icon_filename='yandex.ico',
       
   339                                                     scope=oauth2.Yandex.user_info_scope)}
       
   340 
       
   341 
       
   342 def get_provider_info(provider_name):
       
   343     """Get provider info matching given provider name"""
       
   344     return PROVIDERS_INFO.get(provider_name)
       
   345 
       
   346 
       
   347 @provider(IVocabularyFactory)
       
   348 class OAuthProvidersVocabulary(SimpleVocabulary):
       
   349     """OAuth providers vocabulary"""
       
   350 
       
   351     def __init__(self, *args, **kwargs):
       
   352         terms = []
       
   353         for key, provider in PROVIDERS_INFO.items():
       
   354             terms.append(SimpleTerm(key, title=provider.name))
       
   355         terms.sort(key=lambda x: x.title)
       
   356         super(OAuthProvidersVocabulary, self).__init__(terms)
       
   357 
       
   358 getVocabularyRegistry().register('PyAMS OAuth providers', OAuthProvidersVocabulary)
       
   359 
       
   360 
       
   361 @implementer(ISocialLoginConfiguration)
       
   362 class SocialLoginConfiguration(Folder):
       
   363     """Social login configuration"""
       
   364 
       
   365     def get_oauth_configuration(self):
       
   366         result = {}
       
   367         for provider in self.values():
       
   368             provider_info = get_provider_info(provider.provider_name)
       
   369             result[provider.provider_name] = {'id': provider.provider_id,
       
   370                                               'class_': provider_info.provider,
       
   371                                               'consumer_key': provider.consumer_key,
       
   372                                               'consumer_secret': provider.consumer_secret,
       
   373                                               'scope': provider_info.scope}
       
   374         return result
       
   375 
       
   376 
       
   377 SOCIAL_LOGIN_CONFIGURATION_KEY = 'pyams_security.plugin.social'
       
   378 
       
   379 
       
   380 @adapter_config(context=ISecurityManager, provides=ISocialLoginConfiguration)
       
   381 def SocialLoginConfigurationAdapter(context):
       
   382     """Social login configuration adapter"""
       
   383     annotations = IAnnotations(context)
       
   384     configuration = annotations.get(SOCIAL_LOGIN_CONFIGURATION_KEY)
       
   385     if configuration is None:
       
   386         configuration = annotations[SOCIAL_LOGIN_CONFIGURATION_KEY] = SocialLoginConfiguration()
       
   387         check_request().registry.notify(ObjectCreatedEvent(configuration))
       
   388         locate(configuration, context, '++social-configuration++')
       
   389     return configuration
       
   390 
       
   391 
       
   392 @adapter_config(name='social-configuration', context=ISecurityManager, provides=ITraversable)
       
   393 class SecurityManagerSocialTraverser(ContextAdapter):
       
   394     """++social-configuration++ namespace traverser"""
       
   395 
       
   396     def traverse(self, name, furtherpath=None):
       
   397         return ISocialLoginConfiguration(self.context)
       
   398 
       
   399 
       
   400 @implementer(ISocialLoginProviderConnection)
       
   401 class SocialLoginProviderConnection(Persistent):
       
   402     """Social login provider connection"""
       
   403 
       
   404     provider_name = FieldProperty(ISocialLoginProviderConnection['provider_name'])
       
   405     provider_id = FieldProperty(ISocialLoginProviderConnection['provider_id'])
       
   406     consumer_key = FieldProperty(ISocialLoginProviderConnection['consumer_key'])
       
   407     consumer_secret = FieldProperty(ISocialLoginProviderConnection['consumer_secret'])
       
   408 
       
   409     def get_configuration(self):
       
   410         return get_provider_info(self.provider_name)