跳转到主要内容

可插拔认证工具

项目描述

zope.pluggableauth

https://github.com/zopefoundation/zope.pluggableauth/actions/workflows/tests.yml/badge.svg

基于 zope.authentication,此软件包提供了一种灵活的可插拔认证工具,并提供了一些常见插件。

可插拔认证工具

可插拔认证工具(PAU)提供了一个用于认证主体并将信息与之关联的框架。它使用插件和订阅者来完成其工作。

要使用可插拔认证工具,它应注册为提供 zope.authentication.interfaces.IAuthentication 接口的工具。

认证

PAU的主要任务是认证主体。它在工作中使用两种类型的插件:

  • 凭据插件

  • 认证器插件

凭据插件负责从请求中提取用户凭据。在某些情况下,凭据插件可能会发出“挑战”以获取凭据。例如,“会话”凭据插件从会话中读取凭据(“提取”)。如果它找不到凭据,它将重定向用户到登录表单以提供凭据(“挑战”)。

认证器插件负责认证由凭据插件提取的凭据。它们通常也能够为成功认证的凭据创建主体对象。

给定一个请求对象,PAU(Principal Authority Unit)如果可能,则返回一个主体对象。PAU通过首先遍历其证书插件以获取一组证书来实现这一点。如果它获取到证书,它将遍历其验证器插件以验证它们。

如果验证器成功验证一组证书,PAU将使用验证器创建与证书对应的主体。验证器会在创建经过验证的主体时通知订阅者。订阅者负责向主体添加数据,特别是组。通常,如果订阅者添加数据,它也应该添加相应的接口声明。

简单证书插件

为了说明,我们将创建一个简单的证书插件

>>> from zope import interface
>>> from zope.pluggableauth.authentication import interfaces
>>> @interface.implementer(interfaces.ICredentialsPlugin)
... class MyCredentialsPlugin(object):
...
...     def extractCredentials(self, request):
...         return request.get('credentials')
...
...     def challenge(self, request):
...         pass # challenge is a no-op for this plugin
...
...     def logout(self, request):
...         pass # logout is a no-op for this plugin

作为一个插件,MyCredentialsPlugin需要注册为一个命名实用工具

>>> myCredentialsPlugin = MyCredentialsPlugin()
>>> provideUtility(myCredentialsPlugin, name='My Credentials Plugin')

简单验证器插件

接下来,我们将创建一个简单的验证器插件。对于我们的插件,我们需要实现IPrincipalInfo

>>> @interface.implementer(interfaces.IPrincipalInfo)
... class PrincipalInfo(object):
...
...     def __init__(self, id, title, description):
...         self.id = id
...         self.title = title
...         self.description = description
...
...     def __repr__(self):
...         return 'PrincipalInfo(%r)' % self.id

我们的验证器在创建主体信息时使用此类型

>>> @interface.implementer(interfaces.IAuthenticatorPlugin)
... class MyAuthenticatorPlugin(object):
...
...     def authenticateCredentials(self, credentials):
...         if credentials == 'secretcode':
...             return PrincipalInfo('bob', 'Bob', '')
...
...     def principalInfo(self, id):
...         pass # plugin not currently supporting search

与证书插件一样,验证器插件必须注册为一个命名实用工具

>>> myAuthenticatorPlugin = MyAuthenticatorPlugin()
>>> provideUtility(myAuthenticatorPlugin, name='My Authenticator Plugin')

配置PAU

最后,我们将创建PAU本身

>>> from zope.pluggableauth import authentication
>>> pau = authentication.PluggableAuthentication('xyz_')

并用这两个插件进行配置

>>> pau.credentialsPlugins = ('My Credentials Plugin', )
>>> pau.authenticatorPlugins = ('My Authenticator Plugin', )

使用PAU进行验证

>>> from zope.pluggableauth.factories import AuthenticatedPrincipalFactory
>>> provideAdapter(AuthenticatedPrincipalFactory)

现在我们可以使用PAU验证一个示例请求

>>> from zope.publisher.browser import TestRequest
>>> print(pau.authenticate(TestRequest()))
None

在这种情况下,我们无法验证一个空请求。同样,我们也不能验证带有错误证书的请求

>>> print(pau.authenticate(TestRequest(credentials='let me in!')))
None

然而,如果我们提供正确的证书

>>> request = TestRequest(credentials='secretcode')
>>> principal = pau.authenticate(request)
>>> principal
Principal('xyz_bob')

我们就会得到一个经过验证的主体。

多个验证器插件

PAU可以与多个验证器插件一起工作。它按PAU的authenticatorPlugins属性中指定的顺序使用每个插件来验证一组证书。

为了说明,我们将创建另一个验证器

>>> class MyAuthenticatorPlugin2(MyAuthenticatorPlugin):
...
...     def authenticateCredentials(self, credentials):
...         if credentials == 'secretcode':
...             return PrincipalInfo('black', 'Black Spy', '')
...         elif credentials == 'hiddenkey':
...             return PrincipalInfo('white', 'White Spy', '')
>>> provideUtility(MyAuthenticatorPlugin2(), name='My Authenticator Plugin 2')

如果我们将其放在原始验证器之前

>>> pau.authenticatorPlugins = (
...     'My Authenticator Plugin 2',
...     'My Authenticator Plugin')

那么它将被赋予首先验证请求的机会

>>> pau.authenticate(TestRequest(credentials='secretcode'))
Principal('xyz_black')

如果两个插件都不能验证,PAU将返回None

>>> print(pau.authenticate(TestRequest(credentials='let me in!!')))
None

当我们改变验证器插件的顺序时

>>> pau.authenticatorPlugins = (
...     'My Authenticator Plugin',
...     'My Authenticator Plugin 2')

我们看到我们的原始插件现在首先行动

>>> pau.authenticate(TestRequest(credentials='secretcode'))
Principal('xyz_bob')

然而,如果第一个插件没有成功,第二个插件将有机会进行验证

>>> pau.authenticate(TestRequest(credentials='hiddenkey'))
Principal('xyz_white')

多个证书插件

与验证器类似,我们可以指定多个证书插件。为了说明,我们将创建一个从请求表单中提取证书的证书插件

>>> @interface.implementer(interfaces.ICredentialsPlugin)
... class FormCredentialsPlugin:
...
...     def extractCredentials(self, request):
...         return request.form.get('my_credentials')
...
...     def challenge(self, request):
...         pass
...
...     def logout(request):
...         pass
>>> provideUtility(FormCredentialsPlugin(),
...                name='Form Credentials Plugin')

并在现有插件之前插入新的证书插件

>>> pau.credentialsPlugins = (
...     'Form Credentials Plugin',
...     'My Credentials Plugin')

PAU将按顺序使用每个插件,尝试从请求中获取证书

>>> pau.authenticate(TestRequest(credentials='secretcode',
...                              form={'my_credentials': 'hiddenkey'}))
Principal('xyz_white')

在这种情况下,第一个证书插件成功从表单中获取了证书,第二个验证器能够验证这些证书。具体来说,PAU执行了以下步骤

  • 使用“表单证书插件”获取证书

  • 使用“表单证书插件”获取了“hiddenkey”证书,尝试使用“我的验证器插件”进行验证

  • 使用“我的验证器插件”验证“hiddenkey”失败,尝试“我的验证器插件2”

  • 使用“我的验证器插件2”成功进行验证

让我们尝试一个不同的场景

>>> pau.authenticate(TestRequest(credentials='secretcode'))
Principal('xyz_bob')

在这种情况下,PAU执行了以下步骤

- Get credentials using 'Form Credentials Plugin'
  • 使用“表单证书插件”获取证书失败,尝试“我的证书插件”

  • 使用“我的证书插件”获取了“scecretcode”证书,尝试使用“我的验证器插件”进行验证

  • 使用“我的验证器插件”成功进行验证

让我们尝试一个稍微复杂一些的场景

>>> pau.authenticate(TestRequest(credentials='hiddenkey',
...                              form={'my_credentials': 'bogusvalue'}))
Principal('xyz_white')

这突出了PAU使用多个插件进行验证的能力

  • 使用“表单证书插件”获取证书

  • 使用“表单证书插件”获取了“bogusvalue”证书,尝试使用“我的验证器插件”进行验证

  • 使用“我的验证器插件”验证“boguskey”失败,尝试“我的验证器插件2”

  • 使用‘My Authenticator Plugin 2’无法验证‘boguskey’ – 没有更多的验证器可以尝试,所以让我们尝试下一个凭证插件以获取新的凭证

  • 使用‘My Credentials Plugin’获取凭证

  • 使用‘My Credentials Plugin’获取了‘hiddenkey’凭证,尝试使用‘My Authenticator Plugin’进行验证

  • 使用‘My Authenticator Plugin’无法验证‘hiddenkey’,尝试使用‘My Authenticator Plugin 2’

  • 使用‘My Authenticator Plugin 2’成功进行了验证(欢呼!)

多个验证器插件

与其他我们看到的其他操作一样,PAU使用多个插件来查找主体。如果第一个验证器插件找不到请求的主体,则使用下一个插件,依此类推。

>>> @interface.implementer(interfaces.IAuthenticatorPlugin)
... class AnotherAuthenticatorPlugin:
...
...     def __init__(self):
...         self.infos = {}
...         self.ids = {}
...
...     def principalInfo(self, id):
...         return self.infos.get(id)
...
...     def authenticateCredentials(self, credentials):
...         id = self.ids.get(credentials)
...         if id is not None:
...             return self.infos[id]
...
...     def add(self, id, title, description, credentials):
...         self.infos[id] = PrincipalInfo(id, title, description)
...         self.ids[credentials] = id

为了说明,我们将创建和注册两个验证器

>>> authenticator1 = AnotherAuthenticatorPlugin()
>>> provideUtility(authenticator1, name='Authentication Plugin 1')
>>> authenticator2 = AnotherAuthenticatorPlugin()
>>> provideUtility(authenticator2, name='Authentication Plugin 2')

并将一个主体添加到它们中

>>> authenticator1.add('bob', 'Bob', 'A nice guy', 'b0b')
>>> authenticator1.add('white', 'White Spy', 'Sneaky', 'deathtoblack')
>>> authenticator2.add('black', 'Black Spy', 'Also sneaky', 'deathtowhite')

当我们配置PAU使用两个可搜索的验证器(注意顺序)时

>>> pau.authenticatorPlugins = (
...     'Authentication Plugin 2',
...     'Authentication Plugin 1')

我们注册了我们主体的工厂

>>> from zope.pluggableauth.factories import FoundPrincipalFactory
>>> provideAdapter(FoundPrincipalFactory)

我们看到PAU如何使用这两个插件

>>> pau.getPrincipal('xyz_white')
Principal('xyz_white')
>>> pau.getPrincipal('xyz_black')
Principal('xyz_black')

如果多个插件都知道相同的主体ID,则使用第一个插件,其余的不会被委派。为了说明,我们将添加另一个与现有主体ID相同的主体

>>> authenticator2.add('white', 'White Rider', '', 'r1der')
>>> pau.getPrincipal('xyz_white').title
'White Rider'

如果我们改变插件的顺序

>>> pau.authenticatorPlugins = (
...     'Authentication Plugin 1',
...     'Authentication Plugin 2')

我们得到ID为‘white’的不同主体

>>> pau.getPrincipal('xyz_white').title
'White Spy'

发出挑战

PAU的IAuthentication协议的一部分是在其‘unauthorized’方法被调用时要求用户提供凭证。这种功能的需求是由以下用例驱动的

  • 用户尝试执行他没有授权执行的操作。

  • 处理程序通过调用IAuthentication的‘unauthorized’来响应未授权错误。

  • 身份验证组件(在我们的例子中,是一个PAU)通过向用户发出挑战来收集新的凭证(通常是通过作为新用户登录)。

PAU通过将其凭证插件委派来处理凭证挑战。

目前,PAU配置了当要求挑战时不会执行任何操作的凭证插件(见上面的“challenge”方法)。

为了说明挑战,我们将子类化现有的凭证插件并在其“challenge”中做些事情

>>> class LoginFormCredentialsPlugin(FormCredentialsPlugin):
...
...     def __init__(self, loginForm):
...         self.loginForm = loginForm
...
...     def challenge(self, request):
...         request.response.redirect(self.loginForm)
...         return True

此插件通过将响应重定向到登录表单来处理挑战。它返回True以向PAU发出信号,表明它已处理了挑战。

我们现在将创建并注册几个这样的插件

>>> provideUtility(LoginFormCredentialsPlugin('simplelogin.html'),
...                name='Simple Login Form Plugin')
>>> provideUtility(LoginFormCredentialsPlugin('advancedlogin.html'),
...                name='Advanced Login Form Plugin')

并将PAU配置为使用它们

>>> pau.credentialsPlugins = (
...     'Simple Login Form Plugin',
...     'Advanced Login Form Plugin')

现在当我们调用PAU上的‘unauthorized’时

>>> request = TestRequest()
>>> pau.unauthorized(id=None, request=request)

我们看到用户被重定向到简单的登录表单

>>> request.response.getStatus()
302
>>> request.response.getHeader('location')
'simplelogin.html'

我们可以通过重新排列插件的顺序来更改挑战策略

>>> pau.credentialsPlugins = (
...     'Advanced Login Form Plugin',
...     'Simple Login Form Plugin')

现在当我们调用‘unauthorized’时

>>> request = TestRequest()
>>> pau.unauthorized(id=None, request=request)

由于它是第一个,所以使用了高级插件

>>> request.response.getStatus()
302
>>> request.response.getHeader('location')
'advancedlogin.html'

挑战协议

有时,我们希望多个挑战者协同工作。例如,HTTP规范允许在响应中发出多个挑战。挑战插件可以提供一个challengeProtocol属性,有效地将相关的插件组合在一起进行挑战。如果一个插件从其挑战中返回True并提供一个非None的challengeProtocol,则具有相同挑战协议的后续凭证插件也将用于挑战。

如果没有challengeProtocol,则仅使用成功挑战的第一个插件。

让我们看一个例子。我们将定义一个新的插件,指定一个‘X-Challenge’协议

>>> class XChallengeCredentialsPlugin(FormCredentialsPlugin):
...
...     challengeProtocol = 'X-Challenge'
...
...     def __init__(self, challengeValue):
...         self.challengeValue = challengeValue
...
...     def challenge(self, request):
...         value = self.challengeValue
...         existing = request.response.getHeader('X-Challenge', '')
...         if existing:
...             value += ' ' + existing
...         request.response.setHeader('X-Challenge', value)
...         return True

并将几个实例注册为实用程序

>>> provideUtility(XChallengeCredentialsPlugin('basic'),
...                name='Basic X-Challenge Plugin')
>>> provideUtility(XChallengeCredentialsPlugin('advanced'),
...                name='Advanced X-Challenge Plugin')

当我们使用这两个插件与PAU一起使用

>>> pau.credentialsPlugins = (
...     'Basic X-Challenge Plugin',
...     'Advanced X-Challenge Plugin')

并调用‘unauthorized’时

>>> request = TestRequest()
>>> pau.unauthorized(None, request)

我们看到这两个插件都参与了挑战,而不仅仅是第一个插件

>>> request.response.getHeader('X-Challenge')
'advanced basic'

可插拔认证前缀

主体ID需要在全球范围内是唯一的。插件通常会提供提供ID前缀的选项,以便不同的插件集在PAU内提供唯一的ID。如果系统中存在多个可插拔认证实用程序,给每个PAU一个唯一的标识符是个好主意,这样不同的PAU的主体ID就不会冲突。我们可以在创建PAU时提供一个前缀

>>> pau = authentication.PluggableAuthentication('mypau_')
>>> pau.credentialsPlugins = ('My Credentials Plugin', )
>>> pau.authenticatorPlugins = ('My Authenticator Plugin', )

当我们创建一个请求并尝试进行认证时

>>> pau.authenticate(TestRequest(credentials='secretcode'))
Principal('mypau_bob')

请注意,现在我们的主要ID具有可插拔认证实用程序前缀。

只要我们提供前缀,我们仍然可以查找主要实体。

>> pau.getPrincipal('mypas_42')
Principal('mypas_42', "{'domain': 42}")

>> pau.getPrincipal('mypas_41')
OddPrincipal('mypas_41', "{'int': 41}")

主要文件夹

主要文件夹包含包含主要信息的实体信息对象。我们使用< cite>InternalPrincipal类创建内部实体,并将它们添加到主要文件夹中

>>> from zope.pluggableauth.plugins.principalfolder import InternalPrincipal
>>> p1 = InternalPrincipal('login1', '123', "Principal 1",
...     passwordManagerName="SHA1")
>>> p2 = InternalPrincipal('login2', '456', "The Other One")

>>> from zope.pluggableauth.plugins.principalfolder import PrincipalFolder
>>> principals = PrincipalFolder('principal.')
>>> principals['p1'] = p1
>>> principals['p2'] = p2

认证

主要文件夹提供了< cite>IAuthenticatorPlugin接口。当我们提供合适的凭据时

>>> from pprint import pprint
>>> principals.authenticateCredentials({'login': 'login1', 'password': '123'})
PrincipalInfo('principal.p1')

我们返回一个主要ID和一些补充信息,包括主要标题和描述。请注意,主要ID是主要文件夹前缀和文件夹内部主要信息对象名称的连接。

如果凭据无效,则不返回任何内容

>>> principals.authenticateCredentials({'login': 'login1',
...                                     'password': '1234'})
>>> principals.authenticateCredentials(42)

更改凭据

可以通过修改主要信息对象来更改凭据

>>> p1.login = 'bob'
>>> p1.password = 'eek'
>>> principals.authenticateCredentials({'login': 'bob', 'password': 'eek'})
PrincipalInfo('principal.p1')
>>> principals.authenticateCredentials({'login': 'login1',
...                                     'password': 'eek'})
>>> principals.authenticateCredentials({'login': 'bob',
...                                     'password': '123'})

尝试选择已被占用的登录名是错误的

>>> p1.login = 'login2'
Traceback (most recent call last):
...
ValueError: Principal Login already taken!

如果尝试这样做,则数据保持不变

>>> principals.authenticateCredentials({'login': 'bob', 'password': 'eek'})
PrincipalInfo('principal.p1')

删除主要实体

当然,如果删除了主要实体,我们就不能再对其进行认证了

>>> del principals['p1']
>>> principals.authenticateCredentials({'login': 'bob',
...                                     'password': 'eek'})

组文件夹

组文件夹为存储在ZODB中的组信息提供支持。它们是持久的,并且必须包含在使用它们的PAUs中。

与其他实体一样,组在需要时创建。

组文件夹包含包含组信息的组信息对象。我们使用< cite>GroupInformation类创建组信息

>>> import zope.pluggableauth.plugins.groupfolder
>>> g1 = zope.pluggableauth.plugins.groupfolder.GroupInformation("Group 1")
>>> groups = zope.pluggableauth.plugins.groupfolder.GroupFolder('group.')
>>> groups['g1'] = g1

注意,当添加组信息时,会生成一个GroupAdded事件

>>> from zope.pluggableauth import interfaces
>>> from zope.component.eventtesting import getEvents
>>> getEvents(interfaces.IGroupAdded)
[<GroupAdded 'group.g1'>]

组是在认证服务的范围内定义的。组必须可以通过认证服务访问,并且可以包含可以通过认证服务访问的实体。

为了说明组与认证服务的交互,我们将创建一个示例认证服务

>>> from zope import interface
>>> from zope.authentication.interfaces import IAuthentication
>>> from zope.authentication.interfaces import PrincipalLookupError
>>> from zope.security.interfaces import IGroupAwarePrincipal
>>> from zope.pluggableauth.plugins.groupfolder import setGroupsForPrincipal
>>> @interface.implementer(IGroupAwarePrincipal)
... class Principal:
...     def __init__(self, id, title='', description=''):
...         self.id, self.title, self.description = id, title, description
...         self.groups = []
>>> class PrincipalCreatedEvent:
...     def __init__(self, authentication, principal):
...         self.authentication = authentication
...         self.principal = principal
>>> from zope.pluggableauth.plugins import principalfolder
>>> @interface.implementer(IAuthentication)
... class Principals:
...     def __init__(self, groups, prefix='auth.'):
...         self.prefix = prefix
...         self.principals = {
...            'p1': principalfolder.PrincipalInfo('p1', '', '', ''),
...            'p2': principalfolder.PrincipalInfo('p2', '', '', ''),
...            'p3': principalfolder.PrincipalInfo('p3', '', '', ''),
...            'p4': principalfolder.PrincipalInfo('p4', '', '', ''),
...            }
...         self.groups = groups
...         groups.__parent__ = self
...
...     def getAuthenticatorPlugins(self):
...         return [('principals', self.principals), ('groups', self.groups)]
...
...     def getPrincipal(self, id):
...         if not id.startswith(self.prefix):
...             raise PrincipalLookupError(id)
...         id = id[len(self.prefix):]
...         info = self.principals.get(id)
...         if info is None:
...             info = self.groups.principalInfo(id)
...             if info is None:
...                raise PrincipalLookupError(id)
...         principal = Principal(self.prefix+info.id,
...                               info.title, info.description)
...         setGroupsForPrincipal(PrincipalCreatedEvent(self, principal))
...         return principal

这个类实际上并没有实现完整的< cite>IAuthentication接口,但它实现了组使用的< cite>getPrincipal方法。它工作方式非常类似于可插拔认证实用程序。它按需创建实体。它在创建实体时调用< cite>setGroupsForPrincipal,这通常作为事件订阅者调用。为了让< cite>setGroupsForPrincipal能够找到组文件夹,我们必须将其注册为一个实用程序

>>> from zope.pluggableauth.interfaces import IAuthenticatorPlugin
>>> from zope.component import provideUtility
>>> provideUtility(groups, IAuthenticatorPlugin)

我们将创建并注册一个新的主要实体实用程序

>>> principals = Principals(groups)
>>> provideUtility(principals, IAuthentication)

现在我们可以在组上设置主要实体

>>> g1.principals = ['auth.p1', 'auth.p2']
>>> g1.principals
('auth.p1', 'auth.p2')

添加主要实体会引发事件。

>>> getEvents(interfaces.IPrincipalsAddedToGroup)[-1]
<PrincipalsAddedToGroup ['auth.p1', 'auth.p2'] 'auth.group.g1'>

现在我们可以查找主要实体的组

>>> groups.getGroupsForPrincipal('auth.p1')
('group.g1',)

请注意,组ID是组文件夹前缀和文件夹内部组信息对象名称的连接。

如果我们删除一个组

>>> del groups['g1']

那么组文件夹将失去该组的组信息

>>> groups.getGroupsForPrincipal('auth.p1')
()

但组上的实体信息保持不变

>>> g1.principals
('auth.p1', 'auth.p2')

它还会引发一个事件,表明已从组中删除主要实体(g1是组信息,而不是zope.security.interfaces.IGroup)。

>>> getEvents(interfaces.IPrincipalsRemovedFromGroup)[-1]
<PrincipalsRemovedFromGroup ['auth.p1', 'auth.p2'] 'auth.group.g1'>

添加组设置文件夹的主要实体信息。让我们使用不同的组名

>>> groups['G1'] = g1
>>> groups.getGroupsForPrincipal('auth.p1')
('group.G1',)

在这里我们可以看到新的名称已反映在组信息中。

通常,会引发一个事件。

>>> getEvents(interfaces.IPrincipalsAddedToGroup)[-1]
<PrincipalsAddedToGroup ['auth.p1', 'auth.p2'] 'auth.group.G1'>

关于成员事件(添加和从组中删除成员),我们已经看到当组信息对象被添加到组文件夹时以及从组文件夹中删除时,会触发事件;并且我们也看到当成员被添加到已注册的组时,会触发事件。当成员从已注册的组中删除时,也会触发事件。让我们快速看一些更多示例。

>>> g1.principals = ('auth.p1', 'auth.p3', 'auth.p4')
>>> getEvents(interfaces.IPrincipalsAddedToGroup)[-1]
<PrincipalsAddedToGroup ['auth.p3', 'auth.p4'] 'auth.group.G1'>
>>> getEvents(interfaces.IPrincipalsRemovedFromGroup)[-1]
<PrincipalsRemovedFromGroup ['auth.p2'] 'auth.group.G1'>
>>> g1.principals = ('auth.p1', 'auth.p2')
>>> getEvents(interfaces.IPrincipalsAddedToGroup)[-1]
<PrincipalsAddedToGroup ['auth.p2'] 'auth.group.G1'>
>>> getEvents(interfaces.IPrincipalsRemovedFromGroup)[-1]
<PrincipalsRemovedFromGroup ['auth.p3', 'auth.p4'] 'auth.group.G1'>

组可以包含组

>>> g2 = zope.pluggableauth.plugins.groupfolder.GroupInformation("Group Two")
>>> groups['G2'] = g2
>>> g2.principals = ['auth.group.G1']
>>> groups.getGroupsForPrincipal('auth.group.G1')
('group.G2',)
>>> old = getEvents(interfaces.IPrincipalsAddedToGroup)[-1]
>>> old
<PrincipalsAddedToGroup ['auth.group.G1'] 'auth.group.G2'>

组不能包含循环

>>> g1.principals = ('auth.p1', 'auth.p2', 'auth.group.G2')
... # doctest: +NORMALIZE_WHITESPACE
Traceback (most recent call last):
...
zope.pluggableauth.plugins.groupfolder.GroupCycle: ('auth.group.G2', ['auth.group.G2', 'auth.group.G1'])

尝试这样做不会触发事件。

>>> getEvents(interfaces.IPrincipalsAddedToGroup)[-1] is old
True

它们不必是层次结构化的

>>> ga = zope.pluggableauth.plugins.groupfolder.GroupInformation("Group A")
>>> groups['GA'] = ga
>>> gb = zope.pluggableauth.plugins.groupfolder.GroupInformation("Group B")
>>> groups['GB'] = gb
>>> gb.principals = ['auth.group.GA']
>>> gc = zope.pluggableauth.plugins.groupfolder.GroupInformation("Group C")
>>> groups['GC'] = gc
>>> gc.principals = ['auth.group.GA']
>>> gd = zope.pluggableauth.plugins.groupfolder.GroupInformation("Group D")
>>> groups['GD'] = gd
>>> gd.principals = ['auth.group.GA', 'auth.group.GB']
>>> ga.principals = ['auth.p1']

组文件夹提供了一个非常简单的搜索界面。它们在组标题和描述上执行简单的字符串搜索。

>>> list(groups.search({'search': 'gro'})) # doctest: +NORMALIZE_WHITESPACE
['group.G1', 'group.G2',
 'group.GA', 'group.GB', 'group.GC', 'group.GD']
>>> list(groups.search({'search': 'two'}))
['group.G2']

它们还支持批处理

>>> list(groups.search({'search': 'gro'}, 2, 3))
['group.GA', 'group.GB', 'group.GC']

如果您不提供搜索关键字,则不会返回任何结果

>>> list(groups.search({}))
[]

识别组

函数 setGroupsForPrincipal 是成员创建事件的订阅者。它将任何由组文件夹定义的组添加到这些组中的用户

>>> principal = principals.getPrincipal('auth.p1')
>>> principal.groups
['auth.group.G1', 'auth.group.GA']

当然,这也适用于组

>>> principal = principals.getPrincipal('auth.group.G1')
>>> principal.id
'auth.group.G1'
>>> principal.groups
['auth.group.G2']

除了设置成员组外,setGroupsForPrincipal 函数还在组上声明了 IGroup 接口

>>> [iface.__name__ for iface in interface.providedBy(principal)]
['IGroup', 'IGroupAwarePrincipal']
>>> [iface.__name__
...  for iface in interface.providedBy(principals.getPrincipal('auth.p1'))]
['IGroupAwarePrincipal']

特殊组

两个特殊组,认证用户和所有人,可能适用于由插件认证工具创建的用户。存在一个名为 specialGroups 的订阅者,如果提供了 IAuthenticatedGroup 或 IEveryoneGroup 工具,它将在任何非组成员上设置这些组。

让我们定义一个组感知成员

>>> import zope.security.interfaces
>>> @interface.implementer(zope.security.interfaces.IGroupAwarePrincipal)
... class GroupAwarePrincipal(Principal):
...     def __init__(self, id):
...         Principal.__init__(self, id)
...         self.groups = []

如果我们用这个成员通知订阅者,什么也不会发生,因为组还没有被定义

>>> prin = GroupAwarePrincipal('x')
>>> event = interfaces.FoundPrincipalCreated(42, prin, {})
>>> zope.pluggableauth.plugins.groupfolder.specialGroups(event)
>>> prin.groups
[]

现在,如果我们定义 Everyone 组

>>> import zope.authentication.interfaces
>>> @interface.implementer(zope.authentication.interfaces.IEveryoneGroup)
... class EverybodyGroup(Principal):
...     pass
>>> everybody = EverybodyGroup('all')
>>> provideUtility(everybody, zope.authentication.interfaces.IEveryoneGroup)

然后这个组将被添加到成员中

>>> zope.pluggableauth.plugins.groupfolder.specialGroups(event)
>>> prin.groups
['all']

同样,对于认证组

>>> @interface.implementer(
...         zope.authentication.interfaces.IAuthenticatedGroup)
... class AuthenticatedGroup(Principal):
...     pass
>>> authenticated = AuthenticatedGroup('auth')
>>> provideUtility(authenticated, zope.authentication.interfaces.IAuthenticatedGroup)

然后这个组将被添加到成员中

>>> prin.groups = []
>>> zope.pluggableauth.plugins.groupfolder.specialGroups(event)
>>> prin.groups.sort()
>>> prin.groups
['all', 'auth']

这些组仅添加到非组成员

>>> prin.groups = []
>>> interface.directlyProvides(prin, zope.security.interfaces.IGroup)
>>> zope.pluggableauth.plugins.groupfolder.specialGroups(event)
>>> prin.groups
[]

并且它们仅添加到组感知成员

>>> @interface.implementer(zope.security.interfaces.IPrincipal)
... class SolitaryPrincipal:
...     id = title = description = ''
>>> event = interfaces.FoundPrincipalCreated(42, SolitaryPrincipal(), {})
>>> zope.pluggableauth.plugins.groupfolder.specialGroups(event)
>>> prin.groups
[]

成员感知组

组文件夹包括一个订阅者,它给组成员提供 zope.security.interfaces.IGroupAware 接口及其实现。这允许组能够获取和设置其成员。

给定一个信息对象和一个组……

>>> @interface.implementer(
...         zope.pluggableauth.plugins.groupfolder.IGroupInformation)
... class DemoGroupInformation(object):
...     def __init__(self, title, description, principals):
...         self.title = title
...         self.description = description
...         self.principals = principals
...
>>> i = DemoGroupInformation(
...     'Managers', 'Taskmasters', ('joe', 'jane'))
...
>>> info = zope.pluggableauth.plugins.groupfolder.GroupInfo(
...     'groups.managers', i)
>>> @interface.implementer(IGroupAwarePrincipal)
... class DummyGroup(object):
...     def __init__(self, id, title='', description=''):
...         self.id = id
...         self.title = title
...         self.description = description
...         self.groups = []
...
>>> principal = DummyGroup('foo')
>>> zope.security.interfaces.IMemberAwareGroup.providedBy(principal)
False

……当您调用订阅者时,它将两个伪方法添加到成员中,并使成员提供 IMemberAwareGroup 接口。

>>> zope.pluggableauth.plugins.groupfolder.setMemberSubscriber(
...     interfaces.FoundPrincipalCreated(
...         'dummy auth (ignored)', principal, info))
>>> principal.getMembers()
('joe', 'jane')
>>> principal.setMembers(('joe', 'jane', 'jaimie'))
>>> principal.getMembers()
('joe', 'jane', 'jaimie')
>>> zope.security.interfaces.IMemberAwareGroup.providedBy(principal)
True

这两个方法与 IGroupInformation 对象上的值一起工作。

>>> i.principals == principal.getMembers()
True

限制

当前组文件夹设计有一个重要的限制!

除非成员来自同一插件认证工具,否则将成员分配到组文件夹没有意义。

  • 如果成员来自更高的认证工具,用户将不会收到组定义。为什么?因为在成员认证时设置了成员的组分配。在那个时刻,当前站点是包含成员定义的站点。定义在较低站点中的组将不会咨询

  • 由于在包含组站点的管理中看不到它们,因此无法从较低认证工具分配用户

可能的设计方案是将用户角色分配独立于组定义存储,并在(URL)遍历期间查找分配。但这可能相当复杂。

虽然可以在 URL 路径上具有多个认证工具,但通常最好坚持一个更简单的模型,即在一个 URL 路径上只有一个认证工具(除了用于引导目的的全局工具)。

变更

3.0 (2023-02-14)

  • 添加对 Python 3.8、3.9、3.10、3.11 的支持。

  • 放弃对 Python 2.7、3.5、3.6 的支持。

  • 放弃对已弃用的 python setup.py test 的支持。

2.3.1 (2021-03-19)

  • 放弃对 Python 3.4 的支持。

  • 添加对 Python 3.7 的支持。

  • 从 zope.interface.interfaces 导入以避免弃用警告。

2.3.0 (2017-11-12)

  • 放弃对 Python 3.3 的支持。

2.2.0 (2017-05-02)

  • 添加对 Python 3.6 的支持。

  • 修复在 Python 3.6 下的 idpicker 中的 NameError。见 问题 7

2.1.0 (2016-07-04)

  • 添加对 Python 3.5 的支持。

  • 放弃对 Python 2.6 的支持。

2.0.0 (2014-12-24)

  • 添加对 Python 3.4 的支持。

  • zope.pluggableauth.plugins.session.redirectWithComeFrom 重构为一个可重用的函数。

  • 修复:允许在HTTP基本认证凭据提取插件中包含冒号的密码,以符合RFC2617。

2.0.0a1 (2013-02-21)

  • 添加 tox.iniMANIFEST.in

  • 添加对Python 3.3的支持。

  • 将已弃用的 zope.component.adapts 用法替换为等效的 zope.component.adapter 装饰器。

  • 将已弃用的 zope.interface.implements 用法替换为等效的 zope.interface.implementer 装饰器。

  • 停止对Python 2.4和2.5的支持。

1.3 (2011-02-08)

1.2 (2010-12-16)

  • SessionCredentialsPlugin (_makeCredentials) 添加一个钩子,该钩子可以在子类中重写以以不同的方式在会话中存储凭据。

    例如,您可以使用 keas.kmi 对当前登录用户的密码进行加密,以便它们不会以纯文本形式出现在ZODB中。

1.1 (2010-10-18)

  • 将具体的 IAuthenticatorPlugin 实现从 zope.app.authentication 移动到 zope.pluggableauth.plugins

    因此,希望使用 IAuthenticator 插件(以前在 zope.app.authentication 中找到)的项目不会自动拉入注册ZMI视图所需的 zope.app.* 依赖项。

1.0.3 (2010-07-09)

  • 修复依赖声明。

1.0.2 (2010-07-90)

1.0.1 (2010-02-11)

  • 在新的ZCML文件中声明适配器: principalfactories.zcml。避免了在 zope.app.authentication 中的重复错误。

1.0 (2010-02-05)

  • 从 zope.app.authentication 分离出来

项目详情


下载文件

下载适合您平台的文件。如果您不确定选择哪个,请了解更多关于 安装软件包 的信息。

源代码发行版

zope.pluggableauth-3.0.tar.gz (54.8 kB 查看哈希值)

上传时间 源代码

构建发行版

zope.pluggableauth-3.0-py3-none-any.whl (53.5 kB 查看哈希值)

上传时间 Python 3

由以下支持