src/pyams_security/interfaces/__init__.py
changeset 0 f04e1d0a0723
child 2 94e76f8e9828
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/pyams_security/interfaces/__init__.py	Thu Feb 19 10:53:29 2015 +0100
@@ -0,0 +1,500 @@
+#
+# 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
+import re
+
+# import interfaces
+from pyams_skin.interfaces import IContentSearch
+from zope.annotation.interfaces import IAttributeAnnotatable
+from zope.container.interfaces import IContainer
+from zope.location.interfaces import IContained
+
+# import packages
+from pyams_utils.schema import EncodedPassword
+from zope.configuration.fields import GlobalObject
+from zope.container.constraints import contains, containers
+from zope.interface import implementer, Interface, Attribute, invariant, Invalid
+from zope.schema import TextLine, Text, Int, Bool, List, Tuple, Set, Dict, Choice, Datetime
+
+from pyams_security import _
+
+
+class IPermission(Interface):
+    """Permission utility class"""
+
+    id = TextLine(title="Unique ID",
+                  required=True)
+
+    title = TextLine(title="Title",
+                     required=True)
+
+    description = Text(title="Description",
+                       required=False)
+
+
+class IRole(Interface):
+    """Role utility class"""
+
+    id = TextLine(title="Unique ID",
+                  required=True)
+
+    title = TextLine(title="Title",
+                     required=True)
+
+    description = Text(title="Description",
+                       required=False)
+
+    permissions = Set(title="Permissions",
+                      description="ID of role's permissions",
+                      value_type=TextLine(),
+                      required=False)
+
+
+class IPrincipalInfo(Interface):
+    """Principal info class
+
+    This is the generic interface of objects defined in request 'principal' attribute
+    """
+
+    id = TextLine(title="Globally unique ID",
+                  required=True)
+
+    title = TextLine(title="Principal name",
+                     required=True)
+
+    groups = Set(title="Principal groups",
+                 description="IDs of principals to which this principal directly belongs",
+                 value_type=TextLine())
+
+
+class ICredentials(Interface):
+    """Credentials interface"""
+
+    prefix = TextLine(title="Credentials plug-in prefix",
+                      description="Prefix of plug-in which extracted credentials")
+
+    id = TextLine(title="Credentials ID")
+
+    attributes = Dict(title="Credentials attributes",
+                      description="Attributes dictionary defined by each credentials plug-in",
+                      required=False,
+                      default={})
+
+
+#
+# Credentials, authentication and directory plug-ins
+#
+
+class IPluginEvent(Interface):
+    """Plug-in event interface"""
+
+    plugin = Attribute("Event plug-in name")
+
+
+class IAuthenticatedPrincipalEvent(IPluginEvent):
+    """Authenticated principal event interface"""
+
+    principal_id = Attribute("Authenticated principal ID")
+
+    infos = Attribute("Event custom infos")
+
+
+@implementer(IAuthenticatedPrincipalEvent)
+class AuthenticatedPrincipalEvent(object):
+    """Authenticated principal event"""
+
+    def __init__(self, plugin, principal_id, **infos):
+        self.plugin = plugin
+        self.principal_id = principal_id
+        self.infos = infos
+
+
+class IPlugin(IContained, IAttributeAnnotatable):
+    """Basic authentication plug-in interface"""
+
+    containers('pyams_security.interfaces.IAuthentication')
+
+    prefix = TextLine(title=_("Plug-in prefix"),
+                      description=_("This prefix is mainly used by authentication plug-ins to mark principals"))
+
+    title = TextLine(title=_("Plug-in title"),
+                     required=False)
+
+    enabled = Bool(title=_("Enabled plug-in?"),
+                   description=_("You can choose to disable any plug-in..."),
+                   required=True,
+                   default=True)
+
+
+class ICredentialsInfo(Interface):
+    """Credentials extraction plug-in base interface"""
+
+    def extract_credentials(self, request):
+        """Extract user credentials from given request
+
+        Result of 'extract_credentials' call should be an ICredentials object for which
+        id is the 'raw' principal ID (without prefix); only authentication plug-ins should
+        add a prefix to principal IDs to distinguish principals
+        """
+
+
+class ICredentialsPlugin(ICredentialsInfo, IPlugin):
+    """Credentials extraction plug-in interface"""
+
+
+class IAuthenticationInfo(Interface):
+    """Principal authentication plug-in base interface"""
+
+    def authenticate(self, credentials, request):
+        """Authenticate given credentials and returns a principal ID or None"""
+
+
+class IAuthenticationPlugin(IAuthenticationInfo, IPlugin):
+    """Principal authentication plug-in interface"""
+
+
+class IAdminAuthenticationPlugin(IAuthenticationPlugin):
+    """Admin authentication plug-in base interface"""
+
+    login = TextLine(title=_("Admin. login"))
+
+    password = EncodedPassword(title=_("Admin. password"))
+
+
+class IDirectoryInfo(Interface):
+    """Principal directory plug-in interface"""
+
+    def get_principal(self, principal_id):
+        """Returns real principal matching given ID, or None"""
+
+    def get_all_principals(self, principal_id):
+        """Returns all principals matching given principal ID"""
+
+    def find_principals(self, query):
+        """Find principals matching given query"""
+
+
+class IDirectoryPlugin(IDirectoryInfo, IPlugin):
+    """Principal directory plug-in info"""
+
+
+class IDirectorySearchPlugin(IDirectoryPlugin, IContentSearch):
+    """Principal directory plug-in supporting search"""
+
+
+class IUsersFolderPlugin(IAuthenticationPlugin, IDirectorySearchPlugin):
+    """Local users folder interface"""
+
+    def check_login(self, login):
+        """Check for existence of given login"""
+
+
+#
+# User registration
+#
+
+def check_password(password):
+    """Check validity of a given password"""
+    nbmaj = 0
+    nbmin = 0
+    nbn = 0
+    nbo = 0
+    for car in password:
+        if ord(car) in range(ord('A'), ord('Z') + 1):
+            nbmaj += 1
+        elif ord(car) in range(ord('a'), ord('z') + 1):
+            nbmin += 1
+        elif ord(car) in range(ord('0'), ord('9') + 1):
+            nbn += 1
+        else:
+            nbo += 1
+    if [nbmin, nbmaj, nbn, nbo].count(0) > 1:
+        raise Invalid(_("Your password must contain at least three of these kinds of characters: "
+                        "lowercase letters, uppercase letters, numbers and special characters"))
+
+
+EMAIL_REGEX = re.compile("[^@]+@[^@]+\.[^@]+")
+
+
+class IUserRegistrationInfo(Interface):
+    """User registration info"""
+
+    login = TextLine(title=_("User login"),
+                     description=_("If you don't provide a custom login, your login will be your email address..."),
+                     required=False)
+
+    @invariant
+    def check_login(self):
+        if not self.login:
+            self.login = self.email
+
+    email = TextLine(title=_("E-mail address"),
+                     description=_("An email will be sent to this address to validate account activation; "
+                                   "it will be used as your future user login"),
+                     required=True)
+
+    @invariant
+    def check_email(self):
+        if not EMAIL_REGEX.match(self.email):
+            raise Invalid(_("Your email address is not valid!"))
+
+    firstname = TextLine(title=_("First name"),
+                         required=True)
+
+    lastname = TextLine(title=_("Last name"),
+                        required=True)
+
+    company_name = TextLine(title=_("Company name"),
+                            required=False)
+
+    password = EncodedPassword(title=_("Password"),
+                               description=_("Password must be at least 8 characters long, and contain at least "
+                                             "three kins of characters between lowercase letters, uppercase "
+                                             "letters, numbers and special characters"),
+                               min_length=8,
+                               required=True)
+
+    confirmed_password = EncodedPassword(title=_("Confirmed password"),
+                                         required=True)
+
+    @invariant
+    def check_password(self):
+        if self.password != self.confirmed_password:
+            raise Invalid(_("You didn't confirmed your password correctly!"))
+        check_password(self.password)
+
+
+class IUserRegistrationConfirmationInfo(Interface):
+    """User registration confirmation info"""
+
+    activation_hash = TextLine(title=_("Activation hash"),
+                               required=True)
+
+    login = TextLine(title=_("User login"),
+                     required=True)
+
+    password = EncodedPassword(title=_("Password"),
+                               min_length=8,
+                               required=True)
+
+    confirmed_password = EncodedPassword(title=_("Confirmed password"),
+                                         required=True)
+
+    @invariant
+    def check_password(self):
+        if self.password != self.confirmed_password:
+            raise Invalid(_("You didn't confirmed your password correctly!"))
+        check_password(self.password)
+
+
+class ILocalUser(IAttributeAnnotatable):
+    """Local user interface"""
+
+    login = TextLine(title=_("User login"),
+                     required=True,
+                     readonly=True)
+
+    @invariant
+    def check_login(self):
+        if not self.login:
+            self.login = self.email
+
+    email = TextLine(title=_("User email address"),
+                     required=True)
+
+    @invariant
+    def check_email(self):
+        if not EMAIL_REGEX.match(self.email):
+            raise Invalid(_("Given email address is not valid!"))
+
+    firstname = TextLine(title=_("First name"),
+                         required=True)
+
+    lastname = TextLine(title=_("Last name"),
+                        required=True)
+
+    title = Attribute("User full name")
+
+    company_name = TextLine(title=_("Company name"),
+                            required=False)
+
+    password_manager = Choice(title=_("Password manager name"),
+                              required=True,
+                              vocabulary='PyAMS password managers',
+                              default='SSHA')
+
+    password = EncodedPassword(title=_("Password"),
+                               min_length=8,
+                               required=False)
+
+    wait_confirmation = Bool(title=_("Wait confirmation?"),
+                             description=_("If 'no', user will be activated immediately without waiting email "
+                                           "confirmation"),
+                             required=True,
+                             default=True)
+
+    self_registered = Bool(title=_("Self-registered profile?"),
+                           required=True,
+                           default=True,
+                           readonly=True)
+
+    activation_secret = TextLine(title=_("Activation secret key"),
+                                 description=_("This private secret is used to create and check activation hash"),
+                                 readonly=True)
+
+    activation_hash = TextLine(title=_("Activation hash"),
+                               description=_("This hash is provided into activation message URL. Activation hash "
+                                             "is missing for local users which were registered without waiting "
+                                             "their confirmation."),
+                               readonly=True)
+
+    activation_date = Datetime(title=_("Activation date"),
+                               required=False)
+
+    activated = Bool(title=_("Activation date"),
+                     required=True,
+                     default=False)
+
+    def check_password(self, password):
+        """Check user password against provided one"""
+
+    def generate_secret(self, login, password):
+        """Generate secret key of this profile"""
+
+    def check_activation(self, hash, login, password):
+        """Check activation for given settings"""
+
+
+#
+# Security manager
+#
+
+class ISecurityManager(IContainer, IDirectoryInfo):
+    """Authentication and principals management utility"""
+
+    contains(IPlugin)
+
+    enable_social_login = Bool(title=_("Enable social login?"),
+                               description=_("Enable login via social OAuth plug-ins"),
+                               required=True,
+                               default=False)
+
+    authomatic_secret = TextLine(title=_("Authomatic secret"),
+                                 description=_("This secret phrase is used to encrypt Authomatic cookie"),
+                                 default='this is not a secret',
+                                 required=True)
+
+    social_login_use_popup = Bool(title=_("Use social popup?"),
+                                  required=True,
+                                  default=False)
+
+    open_registration = Bool(title=_("Open registration?"),
+                             description=_("If 'Yes', any use will be able to create a new user account"),
+                             required=True,
+                             default=False)
+
+    users_folder = Choice(title=_("Users folder"),
+                          description=_("Name of users folder used to store registered principals"),
+                          required=False,
+                          vocabulary='PyAMS users folders')
+
+    @invariant
+    def check_users_folder(self):
+        if self.open_registration and not self.users_folder:
+            raise Invalid(_("You can't activate open registration without selecting a users folder"))
+
+    credentials_plugins_names = Tuple(title=_("Credentials plug-ins"),
+                                      description=_("These plug-ins can be used to extract request credentials"),
+                                      value_type=TextLine(),
+                                      readonly=True,
+                                      default=())
+
+    authentication_plugins_names = Tuple(title=_("Authentication plug-ins"),
+                                         description=_("The plug-ins can be used to check extracted credentials "
+                                                       "against a local or remote users database"),
+                                         value_type=TextLine(),
+                                         default=())
+
+    directory_plugins_names = Tuple(title=_("Directory plug-ins"),
+                                    description=_("The plug-in can be used to extract principals information"),
+                                    value_type=TextLine(),
+                                    default=())
+
+    def get_plugin(self, name):
+        """Get plug-in matching given name"""
+
+    def get_credentials_plugins(self):
+        """Extract list of credentials plug-ins"""
+
+    def order_credentials_plugins(self, names):
+        """Define credentials plug-ins order"""
+
+    def get_authentication_plugins(self):
+        """Extract list of authentication plug-ins"""
+
+    def order_authentication_plugins(self, names):
+        """Define authentication plug-ins order"""
+
+    def get_directory_plugins(self):
+        """Extract list of directory plug-ins"""
+
+    def order_directory_plugins(self, names):
+        """Define directory plug-ins order"""
+
+
+LOGIN_REFERER_KEY = 'pyams_security.login.referer'
+
+
+class ILoginView(Interface):
+    """Login view marker interface"""
+
+
+#
+# Social login configuration
+#
+
+class ISocialLoginConfiguration(Interface):
+    """Social login configuration interface"""
+
+    configuration = Dict(title=_("Social login configuration"),
+                         key_type=TextLine(title=_("Provider name")),
+                         value_type=Dict(title=_("Provider configuration")))
+
+
+class ISocialLoginProviderInfo(Interface):
+    """Social login provider info"""
+
+    provider_name = TextLine(title=_("Provider name"),
+                             required=True)
+
+    provider_id = Int(title=_("Provider ID"),
+                      description=_("This value should be unique between all providers"),
+                      required=True,
+                      min=0)
+
+    klass = GlobalObject(title=_("Provider class"),
+                         required=True)
+
+    consumer_key = TextLine(title=_("Provider consumer key"),
+                            required=True)
+
+    consumer_secret = TextLine(title=_("Provider secret"),
+                               required=True)
+
+    scope = List(title=_("Provider scope"),
+                 required=True,
+                 value_type=TextLine(),
+                 default=['email'])