--- /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'])