src/pyams_security/interfaces/__init__.py
changeset 0 f04e1d0a0723
child 2 94e76f8e9828
equal deleted inserted replaced
-1:000000000000 0:f04e1d0a0723
       
     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 import re
       
    18 
       
    19 # import interfaces
       
    20 from pyams_skin.interfaces import IContentSearch
       
    21 from zope.annotation.interfaces import IAttributeAnnotatable
       
    22 from zope.container.interfaces import IContainer
       
    23 from zope.location.interfaces import IContained
       
    24 
       
    25 # import packages
       
    26 from pyams_utils.schema import EncodedPassword
       
    27 from zope.configuration.fields import GlobalObject
       
    28 from zope.container.constraints import contains, containers
       
    29 from zope.interface import implementer, Interface, Attribute, invariant, Invalid
       
    30 from zope.schema import TextLine, Text, Int, Bool, List, Tuple, Set, Dict, Choice, Datetime
       
    31 
       
    32 from pyams_security import _
       
    33 
       
    34 
       
    35 class IPermission(Interface):
       
    36     """Permission utility class"""
       
    37 
       
    38     id = TextLine(title="Unique ID",
       
    39                   required=True)
       
    40 
       
    41     title = TextLine(title="Title",
       
    42                      required=True)
       
    43 
       
    44     description = Text(title="Description",
       
    45                        required=False)
       
    46 
       
    47 
       
    48 class IRole(Interface):
       
    49     """Role utility class"""
       
    50 
       
    51     id = TextLine(title="Unique ID",
       
    52                   required=True)
       
    53 
       
    54     title = TextLine(title="Title",
       
    55                      required=True)
       
    56 
       
    57     description = Text(title="Description",
       
    58                        required=False)
       
    59 
       
    60     permissions = Set(title="Permissions",
       
    61                       description="ID of role's permissions",
       
    62                       value_type=TextLine(),
       
    63                       required=False)
       
    64 
       
    65 
       
    66 class IPrincipalInfo(Interface):
       
    67     """Principal info class
       
    68 
       
    69     This is the generic interface of objects defined in request 'principal' attribute
       
    70     """
       
    71 
       
    72     id = TextLine(title="Globally unique ID",
       
    73                   required=True)
       
    74 
       
    75     title = TextLine(title="Principal name",
       
    76                      required=True)
       
    77 
       
    78     groups = Set(title="Principal groups",
       
    79                  description="IDs of principals to which this principal directly belongs",
       
    80                  value_type=TextLine())
       
    81 
       
    82 
       
    83 class ICredentials(Interface):
       
    84     """Credentials interface"""
       
    85 
       
    86     prefix = TextLine(title="Credentials plug-in prefix",
       
    87                       description="Prefix of plug-in which extracted credentials")
       
    88 
       
    89     id = TextLine(title="Credentials ID")
       
    90 
       
    91     attributes = Dict(title="Credentials attributes",
       
    92                       description="Attributes dictionary defined by each credentials plug-in",
       
    93                       required=False,
       
    94                       default={})
       
    95 
       
    96 
       
    97 #
       
    98 # Credentials, authentication and directory plug-ins
       
    99 #
       
   100 
       
   101 class IPluginEvent(Interface):
       
   102     """Plug-in event interface"""
       
   103 
       
   104     plugin = Attribute("Event plug-in name")
       
   105 
       
   106 
       
   107 class IAuthenticatedPrincipalEvent(IPluginEvent):
       
   108     """Authenticated principal event interface"""
       
   109 
       
   110     principal_id = Attribute("Authenticated principal ID")
       
   111 
       
   112     infos = Attribute("Event custom infos")
       
   113 
       
   114 
       
   115 @implementer(IAuthenticatedPrincipalEvent)
       
   116 class AuthenticatedPrincipalEvent(object):
       
   117     """Authenticated principal event"""
       
   118 
       
   119     def __init__(self, plugin, principal_id, **infos):
       
   120         self.plugin = plugin
       
   121         self.principal_id = principal_id
       
   122         self.infos = infos
       
   123 
       
   124 
       
   125 class IPlugin(IContained, IAttributeAnnotatable):
       
   126     """Basic authentication plug-in interface"""
       
   127 
       
   128     containers('pyams_security.interfaces.IAuthentication')
       
   129 
       
   130     prefix = TextLine(title=_("Plug-in prefix"),
       
   131                       description=_("This prefix is mainly used by authentication plug-ins to mark principals"))
       
   132 
       
   133     title = TextLine(title=_("Plug-in title"),
       
   134                      required=False)
       
   135 
       
   136     enabled = Bool(title=_("Enabled plug-in?"),
       
   137                    description=_("You can choose to disable any plug-in..."),
       
   138                    required=True,
       
   139                    default=True)
       
   140 
       
   141 
       
   142 class ICredentialsInfo(Interface):
       
   143     """Credentials extraction plug-in base interface"""
       
   144 
       
   145     def extract_credentials(self, request):
       
   146         """Extract user credentials from given request
       
   147 
       
   148         Result of 'extract_credentials' call should be an ICredentials object for which
       
   149         id is the 'raw' principal ID (without prefix); only authentication plug-ins should
       
   150         add a prefix to principal IDs to distinguish principals
       
   151         """
       
   152 
       
   153 
       
   154 class ICredentialsPlugin(ICredentialsInfo, IPlugin):
       
   155     """Credentials extraction plug-in interface"""
       
   156 
       
   157 
       
   158 class IAuthenticationInfo(Interface):
       
   159     """Principal authentication plug-in base interface"""
       
   160 
       
   161     def authenticate(self, credentials, request):
       
   162         """Authenticate given credentials and returns a principal ID or None"""
       
   163 
       
   164 
       
   165 class IAuthenticationPlugin(IAuthenticationInfo, IPlugin):
       
   166     """Principal authentication plug-in interface"""
       
   167 
       
   168 
       
   169 class IAdminAuthenticationPlugin(IAuthenticationPlugin):
       
   170     """Admin authentication plug-in base interface"""
       
   171 
       
   172     login = TextLine(title=_("Admin. login"))
       
   173 
       
   174     password = EncodedPassword(title=_("Admin. password"))
       
   175 
       
   176 
       
   177 class IDirectoryInfo(Interface):
       
   178     """Principal directory plug-in interface"""
       
   179 
       
   180     def get_principal(self, principal_id):
       
   181         """Returns real principal matching given ID, or None"""
       
   182 
       
   183     def get_all_principals(self, principal_id):
       
   184         """Returns all principals matching given principal ID"""
       
   185 
       
   186     def find_principals(self, query):
       
   187         """Find principals matching given query"""
       
   188 
       
   189 
       
   190 class IDirectoryPlugin(IDirectoryInfo, IPlugin):
       
   191     """Principal directory plug-in info"""
       
   192 
       
   193 
       
   194 class IDirectorySearchPlugin(IDirectoryPlugin, IContentSearch):
       
   195     """Principal directory plug-in supporting search"""
       
   196 
       
   197 
       
   198 class IUsersFolderPlugin(IAuthenticationPlugin, IDirectorySearchPlugin):
       
   199     """Local users folder interface"""
       
   200 
       
   201     def check_login(self, login):
       
   202         """Check for existence of given login"""
       
   203 
       
   204 
       
   205 #
       
   206 # User registration
       
   207 #
       
   208 
       
   209 def check_password(password):
       
   210     """Check validity of a given password"""
       
   211     nbmaj = 0
       
   212     nbmin = 0
       
   213     nbn = 0
       
   214     nbo = 0
       
   215     for car in password:
       
   216         if ord(car) in range(ord('A'), ord('Z') + 1):
       
   217             nbmaj += 1
       
   218         elif ord(car) in range(ord('a'), ord('z') + 1):
       
   219             nbmin += 1
       
   220         elif ord(car) in range(ord('0'), ord('9') + 1):
       
   221             nbn += 1
       
   222         else:
       
   223             nbo += 1
       
   224     if [nbmin, nbmaj, nbn, nbo].count(0) > 1:
       
   225         raise Invalid(_("Your password must contain at least three of these kinds of characters: "
       
   226                         "lowercase letters, uppercase letters, numbers and special characters"))
       
   227 
       
   228 
       
   229 EMAIL_REGEX = re.compile("[^@]+@[^@]+\.[^@]+")
       
   230 
       
   231 
       
   232 class IUserRegistrationInfo(Interface):
       
   233     """User registration info"""
       
   234 
       
   235     login = TextLine(title=_("User login"),
       
   236                      description=_("If you don't provide a custom login, your login will be your email address..."),
       
   237                      required=False)
       
   238 
       
   239     @invariant
       
   240     def check_login(self):
       
   241         if not self.login:
       
   242             self.login = self.email
       
   243 
       
   244     email = TextLine(title=_("E-mail address"),
       
   245                      description=_("An email will be sent to this address to validate account activation; "
       
   246                                    "it will be used as your future user login"),
       
   247                      required=True)
       
   248 
       
   249     @invariant
       
   250     def check_email(self):
       
   251         if not EMAIL_REGEX.match(self.email):
       
   252             raise Invalid(_("Your email address is not valid!"))
       
   253 
       
   254     firstname = TextLine(title=_("First name"),
       
   255                          required=True)
       
   256 
       
   257     lastname = TextLine(title=_("Last name"),
       
   258                         required=True)
       
   259 
       
   260     company_name = TextLine(title=_("Company name"),
       
   261                             required=False)
       
   262 
       
   263     password = EncodedPassword(title=_("Password"),
       
   264                                description=_("Password must be at least 8 characters long, and contain at least "
       
   265                                              "three kins of characters between lowercase letters, uppercase "
       
   266                                              "letters, numbers and special characters"),
       
   267                                min_length=8,
       
   268                                required=True)
       
   269 
       
   270     confirmed_password = EncodedPassword(title=_("Confirmed password"),
       
   271                                          required=True)
       
   272 
       
   273     @invariant
       
   274     def check_password(self):
       
   275         if self.password != self.confirmed_password:
       
   276             raise Invalid(_("You didn't confirmed your password correctly!"))
       
   277         check_password(self.password)
       
   278 
       
   279 
       
   280 class IUserRegistrationConfirmationInfo(Interface):
       
   281     """User registration confirmation info"""
       
   282 
       
   283     activation_hash = TextLine(title=_("Activation hash"),
       
   284                                required=True)
       
   285 
       
   286     login = TextLine(title=_("User login"),
       
   287                      required=True)
       
   288 
       
   289     password = EncodedPassword(title=_("Password"),
       
   290                                min_length=8,
       
   291                                required=True)
       
   292 
       
   293     confirmed_password = EncodedPassword(title=_("Confirmed password"),
       
   294                                          required=True)
       
   295 
       
   296     @invariant
       
   297     def check_password(self):
       
   298         if self.password != self.confirmed_password:
       
   299             raise Invalid(_("You didn't confirmed your password correctly!"))
       
   300         check_password(self.password)
       
   301 
       
   302 
       
   303 class ILocalUser(IAttributeAnnotatable):
       
   304     """Local user interface"""
       
   305 
       
   306     login = TextLine(title=_("User login"),
       
   307                      required=True,
       
   308                      readonly=True)
       
   309 
       
   310     @invariant
       
   311     def check_login(self):
       
   312         if not self.login:
       
   313             self.login = self.email
       
   314 
       
   315     email = TextLine(title=_("User email address"),
       
   316                      required=True)
       
   317 
       
   318     @invariant
       
   319     def check_email(self):
       
   320         if not EMAIL_REGEX.match(self.email):
       
   321             raise Invalid(_("Given email address is not valid!"))
       
   322 
       
   323     firstname = TextLine(title=_("First name"),
       
   324                          required=True)
       
   325 
       
   326     lastname = TextLine(title=_("Last name"),
       
   327                         required=True)
       
   328 
       
   329     title = Attribute("User full name")
       
   330 
       
   331     company_name = TextLine(title=_("Company name"),
       
   332                             required=False)
       
   333 
       
   334     password_manager = Choice(title=_("Password manager name"),
       
   335                               required=True,
       
   336                               vocabulary='PyAMS password managers',
       
   337                               default='SSHA')
       
   338 
       
   339     password = EncodedPassword(title=_("Password"),
       
   340                                min_length=8,
       
   341                                required=False)
       
   342 
       
   343     wait_confirmation = Bool(title=_("Wait confirmation?"),
       
   344                              description=_("If 'no', user will be activated immediately without waiting email "
       
   345                                            "confirmation"),
       
   346                              required=True,
       
   347                              default=True)
       
   348 
       
   349     self_registered = Bool(title=_("Self-registered profile?"),
       
   350                            required=True,
       
   351                            default=True,
       
   352                            readonly=True)
       
   353 
       
   354     activation_secret = TextLine(title=_("Activation secret key"),
       
   355                                  description=_("This private secret is used to create and check activation hash"),
       
   356                                  readonly=True)
       
   357 
       
   358     activation_hash = TextLine(title=_("Activation hash"),
       
   359                                description=_("This hash is provided into activation message URL. Activation hash "
       
   360                                              "is missing for local users which were registered without waiting "
       
   361                                              "their confirmation."),
       
   362                                readonly=True)
       
   363 
       
   364     activation_date = Datetime(title=_("Activation date"),
       
   365                                required=False)
       
   366 
       
   367     activated = Bool(title=_("Activation date"),
       
   368                      required=True,
       
   369                      default=False)
       
   370 
       
   371     def check_password(self, password):
       
   372         """Check user password against provided one"""
       
   373 
       
   374     def generate_secret(self, login, password):
       
   375         """Generate secret key of this profile"""
       
   376 
       
   377     def check_activation(self, hash, login, password):
       
   378         """Check activation for given settings"""
       
   379 
       
   380 
       
   381 #
       
   382 # Security manager
       
   383 #
       
   384 
       
   385 class ISecurityManager(IContainer, IDirectoryInfo):
       
   386     """Authentication and principals management utility"""
       
   387 
       
   388     contains(IPlugin)
       
   389 
       
   390     enable_social_login = Bool(title=_("Enable social login?"),
       
   391                                description=_("Enable login via social OAuth plug-ins"),
       
   392                                required=True,
       
   393                                default=False)
       
   394 
       
   395     authomatic_secret = TextLine(title=_("Authomatic secret"),
       
   396                                  description=_("This secret phrase is used to encrypt Authomatic cookie"),
       
   397                                  default='this is not a secret',
       
   398                                  required=True)
       
   399 
       
   400     social_login_use_popup = Bool(title=_("Use social popup?"),
       
   401                                   required=True,
       
   402                                   default=False)
       
   403 
       
   404     open_registration = Bool(title=_("Open registration?"),
       
   405                              description=_("If 'Yes', any use will be able to create a new user account"),
       
   406                              required=True,
       
   407                              default=False)
       
   408 
       
   409     users_folder = Choice(title=_("Users folder"),
       
   410                           description=_("Name of users folder used to store registered principals"),
       
   411                           required=False,
       
   412                           vocabulary='PyAMS users folders')
       
   413 
       
   414     @invariant
       
   415     def check_users_folder(self):
       
   416         if self.open_registration and not self.users_folder:
       
   417             raise Invalid(_("You can't activate open registration without selecting a users folder"))
       
   418 
       
   419     credentials_plugins_names = Tuple(title=_("Credentials plug-ins"),
       
   420                                       description=_("These plug-ins can be used to extract request credentials"),
       
   421                                       value_type=TextLine(),
       
   422                                       readonly=True,
       
   423                                       default=())
       
   424 
       
   425     authentication_plugins_names = Tuple(title=_("Authentication plug-ins"),
       
   426                                          description=_("The plug-ins can be used to check extracted credentials "
       
   427                                                        "against a local or remote users database"),
       
   428                                          value_type=TextLine(),
       
   429                                          default=())
       
   430 
       
   431     directory_plugins_names = Tuple(title=_("Directory plug-ins"),
       
   432                                     description=_("The plug-in can be used to extract principals information"),
       
   433                                     value_type=TextLine(),
       
   434                                     default=())
       
   435 
       
   436     def get_plugin(self, name):
       
   437         """Get plug-in matching given name"""
       
   438 
       
   439     def get_credentials_plugins(self):
       
   440         """Extract list of credentials plug-ins"""
       
   441 
       
   442     def order_credentials_plugins(self, names):
       
   443         """Define credentials plug-ins order"""
       
   444 
       
   445     def get_authentication_plugins(self):
       
   446         """Extract list of authentication plug-ins"""
       
   447 
       
   448     def order_authentication_plugins(self, names):
       
   449         """Define authentication plug-ins order"""
       
   450 
       
   451     def get_directory_plugins(self):
       
   452         """Extract list of directory plug-ins"""
       
   453 
       
   454     def order_directory_plugins(self, names):
       
   455         """Define directory plug-ins order"""
       
   456 
       
   457 
       
   458 LOGIN_REFERER_KEY = 'pyams_security.login.referer'
       
   459 
       
   460 
       
   461 class ILoginView(Interface):
       
   462     """Login view marker interface"""
       
   463 
       
   464 
       
   465 #
       
   466 # Social login configuration
       
   467 #
       
   468 
       
   469 class ISocialLoginConfiguration(Interface):
       
   470     """Social login configuration interface"""
       
   471 
       
   472     configuration = Dict(title=_("Social login configuration"),
       
   473                          key_type=TextLine(title=_("Provider name")),
       
   474                          value_type=Dict(title=_("Provider configuration")))
       
   475 
       
   476 
       
   477 class ISocialLoginProviderInfo(Interface):
       
   478     """Social login provider info"""
       
   479 
       
   480     provider_name = TextLine(title=_("Provider name"),
       
   481                              required=True)
       
   482 
       
   483     provider_id = Int(title=_("Provider ID"),
       
   484                       description=_("This value should be unique between all providers"),
       
   485                       required=True,
       
   486                       min=0)
       
   487 
       
   488     klass = GlobalObject(title=_("Provider class"),
       
   489                          required=True)
       
   490 
       
   491     consumer_key = TextLine(title=_("Provider consumer key"),
       
   492                             required=True)
       
   493 
       
   494     consumer_secret = TextLine(title=_("Provider secret"),
       
   495                                required=True)
       
   496 
       
   497     scope = List(title=_("Provider scope"),
       
   498                  required=True,
       
   499                  value_type=TextLine(),
       
   500                  default=['email'])