[Zope3-checkins] SVN: Zope3/trunk/src/zope/app/pas/ Restructured principal folders:

Jim Fulton jim at zope.com
Sun Oct 31 14:56:40 EST 2004


Log message for revision 28305:
  Restructured principal folders:
  
  - renamed from zodb to principalfoler
  
  - No longer collect (and ignore) ids in input forms.
  
  - Provided better automatic naming
  
  - Redid tests as documentation
  
  - Provided a functional test
  

Changed:
  U   Zope3/trunk/src/zope/app/pas/authenticationplugins.zcml
  U   Zope3/trunk/src/zope/app/pas/browser/configure.zcml
  A   Zope3/trunk/src/zope/app/pas/browser/ftests.py
  A   Zope3/trunk/src/zope/app/pas/browser/principalfolder.txt
  A   Zope3/trunk/src/zope/app/pas/idpicker.py
  A   Zope3/trunk/src/zope/app/pas/idpicker.txt
  A   Zope3/trunk/src/zope/app/pas/principalfolder.py
  A   Zope3/trunk/src/zope/app/pas/principalfolder.txt
  U   Zope3/trunk/src/zope/app/pas/tests.py
  D   Zope3/trunk/src/zope/app/pas/zodb.py

-=-
Modified: Zope3/trunk/src/zope/app/pas/authenticationplugins.zcml
===================================================================
--- Zope3/trunk/src/zope/app/pas/authenticationplugins.zcml	2004-10-31 17:59:45 UTC (rev 28304)
+++ Zope3/trunk/src/zope/app/pas/authenticationplugins.zcml	2004-10-31 18:01:29 UTC (rev 28305)
@@ -3,15 +3,16 @@
     xmlns:browser="http://namespaces.zope.org/browser"
     i18n_domain="zope"
     >
-  <content class=".zodb.InternalPrincipal">
+
+  <content class=".principalfolder.PrincipalInformation">
     <require
         permission="zope.ManageServices"
-        interface=".zodb.IInternalPrincipal"
-        set_schema=".zodb.IInternalPrincipal" 
+        interface=".principalfolder.IInternalPrincipal"
+        set_schema=".principalfolder.IInternalPrincipal" 
         />
   </content>
 
-  <localUtility class=".zodb.PersistentPrincipalFolder">
+  <localUtility class=".principalfolder.PrincipalFolder">
 
     <implements
         interface="zope.app.annotation.interfaces.IAttributeAnnotatable" />
@@ -24,19 +25,11 @@
 
   <adapter
       provides="zope.app.container.interfaces.INameChooser"
-      for=".zodb.IInternalPrincipalContainer"
-      factory=".zodb.NameChooser"
+      for=".principalfolder.IInternalPrincipalContainer"
+      factory=".idpicker.IdPicker"
       />
-
-  <browser:addMenuItem
-      title="PAS Persistent Authentication Plugin"
-      description="A PAS Persistent Authentication Plugin"
-      class="zope.app.pas.zodb.PersistentPrincipalFolder"
-      permission="zope.ManageServices"
-      />
       
-      
-    <localUtility class=".sql.SQLAuthenticationPlugin">
+  <localUtility class=".sql.SQLAuthenticationPlugin">
 
     <implements
         interface="zope.app.annotation.interfaces.IAttributeAnnotatable" />

Modified: Zope3/trunk/src/zope/app/pas/browser/configure.zcml
===================================================================
--- Zope3/trunk/src/zope/app/pas/browser/configure.zcml	2004-10-31 17:59:45 UTC (rev 28304)
+++ Zope3/trunk/src/zope/app/pas/browser/configure.zcml	2004-10-31 18:01:29 UTC (rev 28305)
@@ -6,28 +6,45 @@
       name="loginForm.html" 
       for="*"
       template="loginform.pt"
-      permission="zope.Public" />
+      permission="zope.Public" 
+      />
 
+  <addform
+      schema="..principalfolder.IInternalPrincipalContainer"
+      label="Add Principal Folder"
+      content_factory="..principalfolder.PrincipalFolder"
+      keyword_arguments="prefix"
+      name="AddPrincipalFolder.html"
+      permission="zope.ManageServices"
+      />
 
+  <addMenuItem
+      title="Principal Folder"
+      description="A PAS Persistent Authentication Plugin"
+      class="zope.app.pas.principalfolder.PrincipalFolder"
+      permission="zope.ManageServices"
+      view="AddPrincipalFolder.html"
+      />
+
   <addform
-      schema="..zodb.IInternalPrincipal"
-      label="Add Internal Principal"
-      content_factory="..zodb.InternalPrincipal"
-      arguments="id login password title"
+      schema="..principalfolder.IInternalPrincipal"
+      label="Add Principal Information"
+      content_factory="..principalfolder.PrincipalInformation"
+      arguments="login password title"
       keyword_arguments="description"
-      name="AddInternalPrincipalForm.html"
+      name="AddPrincipalInformation.html"
       permission="zope.ManageServices"
       />
 
   <addMenuItem
-      title="Persistent Principal" 
-      class="..zodb.InternalPrincipal"
+      title="Principal Information" 
+      class="..principalfolder.PrincipalInformation"
       permission="zope.ManageServices"
-      view="AddInternalPrincipalForm.html"
+      view="AddPrincipalInformation.html"
       />
 
   <editform
-      schema="..zodb.IInternalPrincipal"
+      schema="..principalfolder.IInternalPrincipal"
       label="Change Internal Principal"
       name="edit.html"
       fields="login password title description"
@@ -35,7 +52,7 @@
       menu="zmi_views" title="Edit" />
 
   <containerViews
-      for="..zodb.IInternalPrincipalContainer"
+      for="..principalfolder.IInternalPrincipalContainer"
       add="zope.ManageServices"
       contents="zope.ManageServices"
       index="zope.ManageServices"

Added: Zope3/trunk/src/zope/app/pas/browser/ftests.py
===================================================================
--- Zope3/trunk/src/zope/app/pas/browser/ftests.py	2004-10-31 17:59:45 UTC (rev 28304)
+++ Zope3/trunk/src/zope/app/pas/browser/ftests.py	2004-10-31 18:01:29 UTC (rev 28305)
@@ -0,0 +1,28 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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.
+#
+##############################################################################
+"""Functional tests
+
+$Id$
+"""
+import unittest
+
+def test_suite():
+    from zope.app.tests import functional
+    return unittest.TestSuite((
+        functional.FunctionalDocFileSuite('principalfolder.txt'),
+        ))
+
+if __name__ == '__main__':
+    unittest.main(defaultTest='test_suite')
+


Property changes on: Zope3/trunk/src/zope/app/pas/browser/ftests.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/zope/app/pas/browser/principalfolder.txt
===================================================================
--- Zope3/trunk/src/zope/app/pas/browser/principalfolder.txt	2004-10-31 17:59:45 UTC (rev 28304)
+++ Zope3/trunk/src/zope/app/pas/browser/principalfolder.txt	2004-10-31 18:01:29 UTC (rev 28305)
@@ -0,0 +1,262 @@
+Using Principal Folders
+=======================
+
+Principal folders are PAS plugins that manage principal information,
+especially authentication credentials.  To use a principal folder, you
+need to create a principal folder in a site management folder and then
+configure it in a PAS.  Let's look at an example, in which we'll
+define a new manager named Bob.  Initially, attempts to log in as Bob
+fail:
+
+  >>> print http(r"""
+  ... GET /manage HTTP/1.1
+  ... Authorization: Basic Ym9iOjEyMw==
+  ... """)
+  HTTP/1.1 401 Unauthorized
+  ...
+
+To allow Bob to log in, we'll start by adding a principal folder:
+
+
+(The following request is a bit weird.  It is part of the current
+ tools UI.  It arranges for a tools site-management folder to be
+ created.  We really need to rethink how we manage TTW utilities.)
+
+  >>> print http(r"""
+  ... GET /++etc++site/AddISearchableAuthenticationPluginTool HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... Referer: http://localhost:8081/++etc++site/@@manageISearchableAuthenticationPluginTool.html
+  ... """)
+  HTTP/1.1 200 Ok
+  ...
+
+  >>> print http(r"""
+  ... POST /++etc++site/AddISearchableAuthenticationPluginTool/AddPrincipalFolder.html%3D HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... Content-Length: 434
+  ... Content-Type: multipart/form-data; boundary=---------------------------190685539214643056941988788830
+  ... Referer: http://localhost:8081/++etc++site/AddISearchableAuthenticationPluginTool/AddPrincipalFolder.html=
+  ... 
+  ... -----------------------------190685539214643056941988788830
+  ... Content-Disposition: form-data; name="field.prefix"
+  ... 
+  ... users.
+  ... -----------------------------190685539214643056941988788830
+  ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+  ... 
+  ... Add
+  ... -----------------------------190685539214643056941988788830
+  ... Content-Disposition: form-data; name="add_input_name"
+  ... 
+  ... users
+  ... -----------------------------190685539214643056941988788830--
+  ... """)
+  HTTP/1.1 303 See Other
+  ...
+  Location: ../@@manageISearchableAuthenticationPluginTool.html
+  ...
+
+We specify a prefix, `users.`.  This is used to make sure that ids
+used by this plugin don't conflict with ids of other plugins.  We also
+name ths plugin `users`.  This is the name we'll use when we configure
+the pluggable authentiaction service.
+
+Next we'll view the contents page of the principal folder:
+
+  >>> print http(r"""
+  ... GET /++etc++site/tools/users/@@contents.html HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... Referer: http://localhost:8081/++etc++site/@@manageISearchableAuthenticationPluginTool.html
+  ... """)
+  HTTP/1.1 200 Ok
+  ...
+
+And we'll add a principal, Bob:
+
+  >>> print http(r"""
+  ... POST /++etc++site/tools/users/+/AddPrincipalInformation.html%3D HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... Content-Length: 777
+  ... Content-Type: multipart/form-data; boundary=---------------------------7243003661505678908829226317
+  ... Referer: http://localhost:8081/++etc++site/tools/users/+/AddPrincipalInformation.html=
+  ... 
+  ... -----------------------------7243003661505678908829226317
+  ... Content-Disposition: form-data; name="field.login"
+  ... 
+  ... bob
+  ... -----------------------------7243003661505678908829226317
+  ... Content-Disposition: form-data; name="field.password"
+  ... 
+  ... 123
+  ... -----------------------------7243003661505678908829226317
+  ... Content-Disposition: form-data; name="field.title"
+  ... 
+  ... Bob
+  ... -----------------------------7243003661505678908829226317
+  ... Content-Disposition: form-data; name="field.description"
+  ... 
+  ... 
+  ... -----------------------------7243003661505678908829226317
+  ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+  ... 
+  ... Add
+  ... -----------------------------7243003661505678908829226317
+  ... Content-Disposition: form-data; name="add_input_name"
+  ... 
+  ... 
+  ... -----------------------------7243003661505678908829226317--
+  ... """)
+  HTTP/1.1 303 See Other
+  ...
+  Location: http://localhost/++etc++site/tools/users/@@contents.html
+  ...
+
+Note that we didn't pick a name.  The name, together with the folder
+prefix. If we don't choose a name, a numeric id is chosen.
+
+Now we have a principal folder with a principal. We need to create a
+pluggable authentication service: 
+
+
+  >>> print http(r"""
+  ... POST /++etc++site/default/AddService/action.html HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... Content-Length: 61
+  ... Content-Type: application/x-www-form-urlencoded
+  ... Referer: http://localhost:8081/++etc++site/default/AddService
+  ... 
+  ... type_name=BrowserAdd__zope.app.pas.pas.LocalPAS&id=&add=+Add+""")
+  HTTP/1.1 303 See Other
+  ...
+  Location: http://localhost/++etc++site/default/LocalPAS/@@registration.html
+  ...
+
+and configure it to use the principal folder:
+
+  >>> print http(r"""
+  ... POST /++etc++site/default/LocalPAS/@@edit.html HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... Content-Length: 1818
+  ... Content-Type: multipart/form-data; boundary=---------------------------11831623361211414588608810327
+  ... Referer: http://localhost:8081/++etc++site/default/LocalPAS/@@edit.html
+  ... 
+  ... -----------------------------11831623361211414588608810327
+  ... Content-Disposition: form-data; name="field.extractors.to"
+  ... 
+  ... HTTP Basic
+  ... -----------------------------11831623361211414588608810327
+  ... Content-Disposition: form-data; name="field.authenticators.to"
+  ... 
+  ... users
+  ... -----------------------------11831623361211414588608810327
+  ... Content-Disposition: form-data; name="field.challengers.to"
+  ... 
+  ... No Challenge if Authenticated
+  ... -----------------------------11831623361211414588608810327
+  ... Content-Disposition: form-data; name="field.challengers.to"
+  ... 
+  ... Zope Realm HTTP Basic
+  ... -----------------------------11831623361211414588608810327
+  ... Content-Disposition: form-data; name="field.factories.to"
+  ... 
+  ... Default
+  ... -----------------------------11831623361211414588608810327
+  ... Content-Disposition: form-data; name="field.searchers.to"
+  ... 
+  ... users
+  ... -----------------------------11831623361211414588608810327
+  ... Content-Disposition: form-data; name="UPDATE_SUBMIT"
+  ... 
+  ... Change
+  ... -----------------------------11831623361211414588608810327
+  ... Content-Disposition: form-data; name="field.extractors"
+  ... 
+  ... HTTP Basic
+  ... -----------------------------11831623361211414588608810327
+  ... Content-Disposition: form-data; name="field.authenticators"
+  ... 
+  ... users
+  ... -----------------------------11831623361211414588608810327
+  ... Content-Disposition: form-data; name="field.challengers"
+  ... 
+  ... No Challenge if Authenticated
+  ... -----------------------------11831623361211414588608810327
+  ... Content-Disposition: form-data; name="field.challengers"
+  ... 
+  ... Zope Realm HTTP Basic
+  ... -----------------------------11831623361211414588608810327
+  ... Content-Disposition: form-data; name="field.factories"
+  ... 
+  ... Default
+  ... -----------------------------11831623361211414588608810327
+  ... Content-Disposition: form-data; name="field.searchers"
+  ... 
+  ... users
+  ... -----------------------------11831623361211414588608810327--
+  ... """)
+  HTTP/1.1 200 Ok
+  ...
+
+We also tell it:
+
+  - to use HTTP Basic authentication with the Zope realm,
+
+  - not to challenge authenticated principals, and
+
+  - to use the default principal factory
+
+Now, with this in place, Bob can log in, but he isn't allowed to
+access the management interface:
+
+
+  >>> print http(r"""
+  ... GET /manage HTTP/1.1
+  ... Authorization: Basic Ym9iOjEyMw==
+  ... """)
+  HTTP/1.1 403 Forbidden
+  ...
+
+We go to the granting interface and search for and find a principal named Bob:
+
+  >>> print http(r"""
+  ... POST /@@grant.html HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... Content-Length: 226
+  ... Content-Type: application/x-www-form-urlencoded
+  ... Referer: http://localhost:8081/@@grant.html
+  ... 
+  ... field.principal.displayed=y&field.principal.MC51c2Vycw__.query.field.search=&field.principal.MC51c2Vycw__.query.search=Search&field.principal.MA__.query.searchstring=&field.principal.MA__.selection=em9wZS5zYW1wbGVfbWFuYWdlcg__""")
+  HTTP/1.1 200 Ok
+  ...
+  <select name="field.principal.MC51c2Vycw__.selection">
+  <option value="dXNlcnMuMQ__">Bob</option>
+  </select>
+  ...
+
+We select Bob and grant him the Manager role:
+
+  >>> print http(r"""
+  ... POST /@@grant.html HTTP/1.1
+  ... Authorization: Basic mgr:mgrpw
+  ... Content-Length: 5316
+  ... Content-Type: application/x-www-form-urlencoded
+  ... Referer: http://localhost:8081/@@grant.html
+  ... 
+  ... field.principal=dXNlcnMuMQ__"""
+  ... """&field.principal.displayed=y"""
+  ... """&field.principal.MC51c2Vycw__.query.field.search=bob"""
+  ... """&field.principal.MA__.query.searchstring="""
+  ... """&GRANT_SUBMIT=Change"""
+  ... """&field.dXNlcnMuMQ__.role.zope.Manager=allow"""
+  ... """&field.dXNlcnMuMQ__.role.zope.Manager-empty-marker=1""")
+  HTTP/1.1 200 Ok
+  ...
+
+At which point, Bob can access the management interface:
+
+  >>> print http(r"""
+  ... GET /@@contents.html HTTP/1.1
+  ... Authorization: Basic Ym9iOjEyMw==
+  ... """)
+  HTTP/1.1 200 Ok
+  ...

Added: Zope3/trunk/src/zope/app/pas/idpicker.py
===================================================================
--- Zope3/trunk/src/zope/app/pas/idpicker.py	2004-10-31 17:59:45 UTC (rev 28304)
+++ Zope3/trunk/src/zope/app/pas/idpicker.py	2004-10-31 18:01:29 UTC (rev 28305)
@@ -0,0 +1,38 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.0 (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.
+#
+##############################################################################
+"""Helper base class that picks principal ids
+
+$Id$
+"""
+
+from zope.app.container.contained import NameChooser
+
+
+class IdPicker(NameChooser):
+    """Helper base class that picks principal ids
+
+    Add numbers to ids given by users to make them unique.
+    """
+        
+    def chooseName(self, name, object):
+        i = 0
+        name = unicode(name)
+        orig = name
+        while (not name) or (name in self.context):
+            i += 1
+            name = orig+str(i)
+
+        self.checkName(name, object)
+        return name
+    


Property changes on: Zope3/trunk/src/zope/app/pas/idpicker.py
___________________________________________________________________
Name: svn:keywords
   + Id
Name: svn:eol-style
   + native

Added: Zope3/trunk/src/zope/app/pas/idpicker.txt
===================================================================
--- Zope3/trunk/src/zope/app/pas/idpicker.txt	2004-10-31 17:59:45 UTC (rev 28304)
+++ Zope3/trunk/src/zope/app/pas/idpicker.txt	2004-10-31 18:01:29 UTC (rev 28305)
@@ -0,0 +1,22 @@
+Id Pickler
+----------
+
+The Id pickler is a variation on the name chooser that picks numeric
+ids when no name is given.
+
+  >>> from zope.app.pas.idpicker import IdPicker
+  >>> IdPicker({}).chooseName('', None)
+  u'1'
+
+  >>> IdPicker({'1': 1}).chooseName('', None)
+  u'2'
+
+  >>> IdPicker({'2': 1}).chooseName('', None)
+  u'1'
+
+  >>> IdPicker({'1': 1}).chooseName('bob', None)
+  u'bob'
+
+  >>> IdPicker({'bob': 1}).chooseName('bob', None)
+  u'bob1'
+


Property changes on: Zope3/trunk/src/zope/app/pas/idpicker.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Copied: Zope3/trunk/src/zope/app/pas/principalfolder.py (from rev 28289, Zope3/trunk/src/zope/app/pas/zodb.py)
===================================================================
--- Zope3/trunk/src/zope/app/pas/zodb.py	2004-10-29 21:33:26 UTC (rev 28289)
+++ Zope3/trunk/src/zope/app/pas/principalfolder.py	2004-10-31 18:01:29 UTC (rev 28305)
@@ -0,0 +1,224 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Corporation and Contributors.
+# 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.
+#
+##############################################################################
+"""ZODB-based Authentication Source
+
+$Id$
+"""
+__docformat__ = "reStructuredText"
+
+import persistent
+
+import zope.interface
+import zope.schema
+
+import zope.app.container.btree
+import zope.app.container.contained
+import zope.app.container.constraints
+import zope.app.container.interfaces
+
+from zope.app.i18n import ZopeMessageIDFactory as _
+
+from zope.app.pas import interfaces
+
+class IInternalPrincipal(zope.interface.Interface):
+    """Principal information"""
+
+    login = zope.schema.TextLine(
+        title=_("Login"),
+        description=_("The Login/Username of the principal. "
+                      "This value can change."),
+        required=True)
+
+    password = zope.schema.Password(
+        title=_(u"Password"),
+        description=_("The password for the principal."),
+        required=True)
+
+    title = zope.schema.TextLine(
+        title=_("Title"),
+        description=_("Provides a title for the principal."),
+        required=True)
+
+    description = zope.schema.Text(
+        title=_("Description"),
+        description=_("Provides a description for the principal."),
+        required=False,
+        missing_value='',
+        default=u'',
+        )
+
+
+class IInternalPrincipalContainer(zope.interface.Interface):
+    """A container that contains internal principals."""
+
+    prefix = zope.schema.TextLine(
+        title=_("Prefix"),
+        description=_(
+        "Prefix to be added to all principal ids to assure "
+        "that all ids are unique within the authentication service"
+        ),
+        required=False,
+        missing_value=u"",
+        default=u'',
+        readonly=True,
+        )
+
+    zope.app.container.constraints.contains(IInternalPrincipal)
+
+
+class IInternalPrincipalContained(zope.interface.Interface):
+    """Principal information"""
+
+    zope.app.container.constraints.containers(IInternalPrincipalContainer)
+
+
+class ISearchSchema(zope.interface.Interface):
+    """Search Interface for this Principal Provider"""
+
+    search = zope.schema.TextLine(
+        title=_("Search String"),
+        description=_("A Search String"),
+        required=False,
+        default=u'',
+        missing_value=u'',
+        )
+
+class PrincipalInformation(
+    persistent.Persistent,
+    zope.app.container.contained.Contained,
+    ):
+    """An internal principal for Persistent Principal Folder.
+    """
+    zope.interface.implements(IInternalPrincipal, IInternalPrincipalContained)
+
+    def __init__(self, login, password, title, description=u''):
+        self._login = login
+        self.password = password
+        self.title = title
+        self.description = description
+
+    def getLogin(self):
+        return self._login
+
+    def setLogin(self, login):
+        oldLogin = self._login
+        self._login = login
+        if self.__parent__ is not None:
+            try:
+                self.__parent__.notifyLoginChanged(oldLogin, self)
+            except ValueError:
+                self._login = oldLogin
+                raise
+
+    login = property(getLogin, setLogin)
+
+    def __getitem__(self, attr):
+        if attr in ('title', 'description'):
+            return getattr(self, attr)
+
+class PrincipalFolder(zope.app.container.btree.BTreeContainer):
+    """A Persistent Principal Folder and Authentication plugin
+    """
+    zope.interface.implements(interfaces.ISearchableAuthenticationPlugin,
+                              interfaces.IQuerySchemaSearch,
+                              IInternalPrincipalContainer)
+
+    def __init__(self, prefix=''):
+        self.prefix = unicode(prefix)
+        super(PrincipalFolder, self).__init__()
+        self.__id_by_login = self._newContainerData()
+
+    def notifyLoginChanged(self, oldLogin, principal):
+        """Notify the Container about changed login of a principal.
+
+        We need this, so that our second tree can be kept up-to-date.
+        """
+        # A user with the new login already exists
+        if principal.login in self.__id_by_login:
+            raise ValueError, 'Principal Login already taken!'
+
+        del self.__id_by_login[oldLogin]
+        self.__id_by_login[principal.login] = principal.__name__
+        
+    def __setitem__(self, id, principal):
+        """Add principal information
+        """
+        # A user with the new login already exists
+        if principal.login in self.__id_by_login:
+            raise ValueError, 'Principal Login already taken!'
+
+        super(PrincipalFolder, self).__setitem__(id, principal)
+        self.__id_by_login[principal.login] = id
+
+    def __delitem__(self, id):
+        """Remove principal information
+        """
+        principal = self[id]
+        super(PrincipalFolder, self).__delitem__(id)
+        del self.__id_by_login[principal.login]
+
+
+    def authenticateCredentials(self, credentials):
+        """Return principal info if credentials can be authenticated
+        """
+        if not isinstance(credentials, dict):
+            return None
+
+        if not ('login' in credentials and 'password' in credentials):
+            return None
+
+        id = self.__id_by_login.get(credentials['login'])
+        if id is None:
+            return None
+
+        principal = self[id]
+        if principal.password != credentials['password']:
+            return None
+
+        id = self.prefix+id
+
+        return id, {'login': principal.login, 'title': principal.title,
+                    'description': principal.description}
+
+    def principalInfo(self, principal_id):
+        if principal_id.startswith(self.prefix):
+            principal = self.get(principal_id[len(self.prefix):])
+            if principal is not None:
+                return {
+                    'login': principal.login,
+                    'title': principal.title,
+                    'description': principal.description,
+                    }
+            
+
+    schema = ISearchSchema
+
+    def search(self, query, start=None, batch_size=None):
+        """Search through this principal provider.
+        """
+        search = query.get('search')
+        if search is None:
+            return
+        i = 0
+        n = 1
+        for value in self.values():
+            if (search in value.title or
+                search in value.description or
+                search in value.login):
+                if not ((start is not None and i < start)
+                        or
+                        (batch_size is not None and n > batch_size)):
+                    n += 1
+                    yield self.prefix+value.__name__
+                i += 1

Added: Zope3/trunk/src/zope/app/pas/principalfolder.txt
===================================================================
--- Zope3/trunk/src/zope/app/pas/principalfolder.txt	2004-10-31 17:59:45 UTC (rev 28304)
+++ Zope3/trunk/src/zope/app/pas/principalfolder.txt	2004-10-31 18:01:29 UTC (rev 28305)
@@ -0,0 +1,168 @@
+Principal Folder
+================
+
+Principal folders contain principal-information objects that contain
+principal information.  We create principal information using the
+`PrincipalInformation` class:
+
+  >>> import zope.app.pas.principalfolder
+  >>> p1 = zope.app.pas.principalfolder.PrincipalInformation(
+  ...     'login1', '123', "Principal 1")
+  >>> p2 = zope.app.pas.principalfolder.PrincipalInformation(
+  ...     'login2', '456', "The other one")
+
+  >>> principals = zope.app.pas.principalfolder.PrincipalFolder('principal.')
+  >>> principals['p1'] = p1
+  >>> principals['p2'] = p2
+
+Authentication
+--------------
+
+Principal folders provide the `IAuthenticationPlugin' interface.
+When we provide suitable credentials:
+
+  >>> from zope.testing.doctestunit import pprint
+  >>> pprint(principals.authenticateCredentials({'login': 'login1', 
+  ...                                            'password': '123'}))
+  (u'principal.p1',
+   {'description': u'',
+    'login': 'login1',
+    'title': 'Principal 1'})
+
+we get back a principal id and supplimentary information, including the
+principal title and description.  Note that the principal id is a
+concatination of the principal-folder prefix and the name of the
+principal-information object within the folder.
+
+None is returned if the credentials are invalid:
+
+  >>> principals.authenticateCredentials({'login': 'login1', 
+  ...                                      'password': '1234'})
+  >>> principals.authenticateCredentials(42)
+
+Search
+------
+
+Principal folders also provide the IQuerySchemaSearch interface.  This
+supports both finding principal information based on their ids:
+
+  >>> pprint(principals.principalInfo('principal.p1'))
+  {'description': u'',
+   'login': 'login1',
+   'title': 'Principal 1'}
+
+  >>> principals.principalInfo('p1')
+
+and searching for principals based on a search string:
+
+  >>> list(principals.search({'search': 'other'}))
+  [u'principal.p2']
+
+  >>> list(principals.search({'search': ''}))
+  [u'principal.p1', u'principal.p2']
+
+  >>> list(principals.search({'search': 'eek'}))
+  []
+
+  >>> list(principals.search({}))
+  []
+
+If there are a large number of matches:
+
+  >>> for i in range(20):
+  ...     i = str(i)
+  ...     p = zope.app.pas.principalfolder.PrincipalInformation(
+  ...         'l'+i, i, "Dude "+i)
+  ...     principals[i] = p
+
+  >>> pprint(list(principals.search({'search': 'D'})))
+  [u'principal.0',
+   u'principal.1',
+   u'principal.10',
+   u'principal.11',
+   u'principal.12',
+   u'principal.13',
+   u'principal.14',
+   u'principal.15',
+   u'principal.16',
+   u'principal.17',
+   u'principal.18',
+   u'principal.19',
+   u'principal.2',
+   u'principal.3',
+   u'principal.4',
+   u'principal.5',
+   u'principal.6',
+   u'principal.7',
+   u'principal.8',
+   u'principal.9']
+
+We can use batching parameters to specify a subset of results:
+
+  >>> pprint(list(principals.search({'search': 'D'}, start=17)))
+  [u'principal.7',
+   u'principal.8',
+   u'principal.9']
+
+  >>> pprint(list(principals.search({'search': 'D'}, batch_size=5)))
+  [u'principal.0',
+   u'principal.1',
+   u'principal.10',
+   u'principal.11',
+   u'principal.12']
+
+  >>> pprint(list(principals.search({'search': 'D'}, start=5, batch_size=5)))
+  [u'principal.13',
+   u'principal.14',
+   u'principal.15',
+   u'principal.16',
+   u'principal.17']
+
+Changing credentials
+--------------------
+
+Creedentials can be changed by modifying principal-information
+objects:
+
+  >>> p1.login = 'bob'
+  >>> p1.password = 'eek'
+
+  >>> pprint(principals.authenticateCredentials({'login': 'bob', 
+  ...                                            'password': 'eek'}))
+  (u'principal.p1',
+   {'description': u'',
+    'login': 'bob',
+    'title': 'Principal 1'})
+
+  >>> principals.authenticateCredentials({'login': 'login1', 
+  ...                                     'password': 'eek'})
+
+  >>> principals.authenticateCredentials({'login': 'bob', 
+  ...                                     'password': '123'})
+
+
+It is an error to try to pick a login name that is already taken:
+
+  >>> p1.login = 'login2'
+  Traceback (most recent call last):
+  ...
+  ValueError: Principal Login already taken!
+
+If such an attempt is made, the data are unchanged:
+
+  >>> pprint(principals.authenticateCredentials({'login': 'bob', 
+  ...                                            'password': 'eek'}))
+  (u'principal.p1',
+   {'description': u'',
+    'login': 'bob',
+    'title': 'Principal 1'})
+
+Removing principals
+-------------------
+
+If course, if a principal is removed, we can no-longer authenticate
+it:
+
+  >>> del principals['p1']
+  >>> principals.authenticateCredentials({'login': 'bob', 
+  ...                                     'password': 'eek'})


Property changes on: Zope3/trunk/src/zope/app/pas/principalfolder.txt
___________________________________________________________________
Name: svn:eol-style
   + native

Modified: Zope3/trunk/src/zope/app/pas/tests.py
===================================================================
--- Zope3/trunk/src/zope/app/pas/tests.py	2004-10-31 17:59:45 UTC (rev 28304)
+++ Zope3/trunk/src/zope/app/pas/tests.py	2004-10-31 18:01:29 UTC (rev 28305)
@@ -58,7 +58,8 @@
     return unittest.TestSuite((
         doctest.DocTestSuite('zope.app.pas.generic'),
         doctest.DocTestSuite('zope.app.pas.httpplugins'),
-        doctest.DocTestSuite('zope.app.pas.zodb'),
+        doctest.DocFileSuite('principalfolder.txt'),
+        doctest.DocFileSuite('idpicker.txt'),
         doctest.DocTestSuite('zope.app.pas.principalplugins'),
         doctest.DocTestSuite('zope.app.pas.browserplugins',
                              setUp=formAuthSetUp,

Deleted: Zope3/trunk/src/zope/app/pas/zodb.py
===================================================================
--- Zope3/trunk/src/zope/app/pas/zodb.py	2004-10-31 17:59:45 UTC (rev 28304)
+++ Zope3/trunk/src/zope/app/pas/zodb.py	2004-10-31 18:01:29 UTC (rev 28305)
@@ -1,317 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2004 Zope Corporation and Contributors.
-# 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.
-#
-##############################################################################
-"""ZODB-based Authentication Source
-
-$Id$
-"""
-__docformat__ = "reStructuredText"
-from persistent import Persistent
-from BTrees.OOBTree import OOBTree
-from UserDict import DictMixin
-
-from zope.interface import implements, Interface
-from zope.schema import Text, TextLine, Password, Field
-
-from zope.app.container.btree import BTreeContainer
-from zope.app.container.contained import Contained
-from zope.app.container.constraints import ItemTypePrecondition
-from zope.app.container.constraints import ContainerTypesConstraint
-from zope.app.container.interfaces import IContainer
-from zope.app.container.interfaces import INameChooser
-from zope.app.exception.interfaces import UserError
-from zope.app.i18n import ZopeMessageIDFactory as _
-
-import interfaces
-
-class IInternalPrincipal(Interface):
-    """Principal information"""
-
-    id = TextLine(
-        title=_("Id"),
-        description=_("Id as which this principal will be known and used."),
-        readonly=True,
-        required=True)
-
-    login = TextLine(
-        title=_("Login"),
-        description=_("The Login/Username of the principal. "
-                      "This value can change."),
-        required=True)
-
-    password = Password(
-        title=_(u"Password"),
-        description=_("The password for the principal."),
-        required=True)
-
-    title = TextLine(
-        title=_("Title"),
-        description=_("Provides a title for the principal."),
-        required=True)
-
-    description = Text(
-        title=_("Description"),
-        description=_("Provides a description for the principal."),
-        required=False)
-
-
-class IInternalPrincipalContainer(Interface):
-    """A container that contains internal principals."""
-    
-    def __setitem__(id, principal_source):
-        """Add to object"""
-
-    __setitem__.precondition = ItemTypePrecondition(IInternalPrincipal)
-
-
-class IInternalPrincipalContained(Interface):
-    """Principal information"""
-
-    __parent__= Field(
-        constraint = ContainerTypesConstraint(IInternalPrincipalContainer))
-
-
-class ISearchSchema(Interface):
-    """Search Interface for this Principal Provider"""
-
-    search = TextLine(
-        title=_("Search String"),
-        description=_("A Search String"),
-        required=False,
-        default=u'')
-
-class InternalPrincipal(Persistent, Contained, DictMixin):
-    """An internal principal for Persistent Principal Folder.
-
-    Make sure the folder gets notified for login changes.
-
-    >>> class Folder:
-    ...     def notifyLoginChanged(self, old, principal):
-    ...         self.old = old
-    ...         self.principal = principal
-
-    >>> folder = Folder()
-    >>> principal = InternalPrincipal('1', 'foo', 'bar', 'Foo Bar')
-    >>> principal.__parent__ = folder
-
-    >>> principal.login = 'blah'
-
-    >>> folder.old
-    'foo'
-    >>> folder.principal is principal
-    True
-    """
-    implements(IInternalPrincipal, IInternalPrincipalContained)
-
-    def __init__(self, id, login, password, title, description=u''):
-        self.id = id
-        self._login = login
-        self.password = password
-        self.title = title
-        self.description = description
-
-    def getLogin(self):
-        return self._login
-
-    def setLogin(self, login):
-        oldLogin = self._login
-        self._login = login
-        if self.__parent__ is not None:
-            self.__parent__.notifyLoginChanged(oldLogin, self)
-
-    login = property(getLogin, setLogin)
-
-    def __getitem__(self, attr):
-        if attr in ('title', 'description'):
-            return getattr(self, attr)
-
-    
-        
-
-
-class PersistentPrincipalFolder(BTreeContainer):
-    """A Persistent Principal Folder and Authentication plugin
-
-    Whenever the following code refers to `principal`, we mean a tuple of the
-    form (login, password). Since we try not to expose the password, password
-    is always `None` in any output.
-    """
-    implements(interfaces.IAuthenticationPlugin,
-               interfaces.IQuerySchemaSearch,
-               IInternalPrincipalContainer)
-
-    def __init__(self):
-        super(PersistentPrincipalFolder, self).__init__()
-        self.__id_by_login = self._newContainerData()
-
-    def notifyLoginChanged(self, oldLogin, principal):
-        """Notify the Container about changed login of a principal.
-
-        We need this, so that our second tree can be kept up-to-date.
-        """
-        # A user with the new login already exists
-        if principal.login in self.__id_by_login:
-            raise ValueError, 'Principal Login already taken!'
-
-        del self.__id_by_login[oldLogin]
-        self.__id_by_login[principal.login] = principal.id
-        
-    def __setitem__(self, id, principal):
-        """ See `IContainerNamesContainer`
-
-        >>> pps = PersistentPrincipalFolder()
-        >>> principal = InternalPrincipal('1', 'foo', 'bar', u'Foo Bar')
-        >>> pps['1'] = principal
-        >>> pps['1'] is principal
-        True
-        >>> pps._PersistentPrincipalFolder__id_by_login['foo']
-        '1'
-        """
-        principal.id = id
-        # A user with the new login already exists
-        if principal.login in self.__id_by_login:
-            raise ValueError, 'Principal Login already taken!'
-
-        super(PersistentPrincipalFolder, self).__setitem__(id, principal)
-        self.__id_by_login[principal.login] = id
-
-    def __delitem__(self, id):
-        """ See `IContainer`.
-
-        >>> pps = PersistentPrincipalFolder()
-        >>> pps['1'] = InternalPrincipal('1', 'foo', 'bar', u'Foo Bar')
-        >>> del pps['1']
-        >>> pps['1']
-        Traceback (most recent call last):
-        ...
-        KeyError: '1'
-        """
-        principal = self[id]
-        super(PersistentPrincipalFolder, self).__delitem__(id)
-        del self.__id_by_login[principal.login]
-
-
-    def authenticateCredentials(self, credentials):
-        """See zope.app.pas.interfaces.IAuthenticationPlugin
-
-        Create an authentication plugin and add a principal to it.
-
-        >>> pps = PersistentPrincipalFolder()
-        >>> pps['1'] = InternalPrincipal('1', 'foo', 'bar', u'Foo Bar')
-
-        >>> pps.authenticateCredentials(1) is None
-        True
-        >>> pps.authenticateCredentials({'blah': 2}) is None
-        True
-        >>> pps.authenticateCredentials({'login': 'foo'}) is None
-        True
-        >>> pps.authenticateCredentials({'password': 'bar'}) is None
-        True
-        >>> pps.authenticateCredentials({'login': 'foo1',
-        ...                              'password': 'bar'}) is None
-        True
-        >>> pps.authenticateCredentials({'login': 'foo',
-        ...                              'password': 'bar1'}) is None
-        True
-        >>> res=pps.authenticateCredentials({'login': 'foo', 'password': 'bar'})
-
-        >>> import pprint
-        >>> pp = pprint.PrettyPrinter(width=65)
-        >>> pp.pprint(res)
-        ('1', {'login': 'foo', 'description': u'', 'title': u'Foo Bar'})
-        """
-        if not isinstance(credentials, dict):
-            return None
-
-        if not ('login' in credentials and 'password' in credentials):
-            return None
-
-        id = self.__id_by_login.get(credentials['login'])
-        if id is None:
-            return None
-
-        principal = self[id]
-        if principal.password != credentials['password']:
-            return None
-
-        return id, {'login': principal.login, 'title': principal.title,
-                    'description': principal.description}
-
-    def principalInfo(self, principal_id):
-        return self.get(principal_id)
-
-    schema = ISearchSchema
-
-    def search(self, query, start=None, batch_size=None):
-        """Search through this principal provider.
-
-        >>> pps = PersistentPrincipalFolder()
-        >>> pps['1'] = InternalPrincipal('1', 'foo1', 'bar', u'Foo Bar 1')
-        >>> pps['2'] = InternalPrincipal('2', 'foo2', 'bar', u'Foo Bar 2')
-        >>> pps['3'] = InternalPrincipal('3', 'foo3', 'bar', u'Foo Bar 3')
-
-        >>> list(pps.search({'search': 'foo'}))
-        ['1', '2', '3']
-        >>> list(pps.search({'search': '1'}))
-        ['1']
-
-        >>> list(pps.search({'search': 'foo'}, 1))
-        ['2', '3']
-        >>> list(pps.search({'search': 'foo'}, 1, 1))
-        ['2']
-        """
-        search = query.get('search', u'') or u''
-        i = 0
-        n = 1
-        for value in self.values():
-            if (search in value.title or
-                search in value.description or
-                search in value.login):
-                if not ((start is not None and i < start)
-                        or
-                        (batch_size is not None and n > batch_size)):
-                    n += 1
-                    yield value.id
-                i += 1
-        
-
-class NameChooser(object):
-    """A name chosser for the principal provider.
-
-    >>> folder = PersistentPrincipalFolder()
-    >>> chooser = NameChooser(folder)
-
-    >>> folder['test'] = InternalPrincipal('test', 'foo', 'bar', 'Foo Bar')
-
-    >>> chooser.checkName('test1', None)
-    'test1'
-    >>> chooser.checkName('test', None)
-    Traceback (most recent call last):
-    ...
-    UserError: Name already taken.
-
-    >>> chooser.chooseName('', InternalPrincipal('foo', 'foo', 'bar', ''))
-    'foo'
-    """
-    implements(INameChooser)
-
-    def __init__(self, container):
-        self.container = container
-
-    def checkName(self, name, object):
-        if name in self.container:
-            raise UserError, 'Name already taken.'
-        return name
-
-    def chooseName(self, name, object):
-        return object.id



More information about the Zope3-Checkins mailing list