I do not really get the question. We do not have a guide "how-to write a PAS plugins". This happens not that often.
So, I wrote a bunch of pas plugins already. My latest for a customer some weeks ago was a very customer specific user/groups implementation, but using post handler its the most modern approach. Getting all pieces together needs touches some files:
First you need the plugin with some installation glue:
def manage_addBdaUsersPlugin(context, id, title='', RESPONSE=None, **kw):
"""Create an instance of a BdaSaml2 Plugin.
"""
plugin = BdaUsersPlugin(id, title, **kw)
context._setObject(plugin.getId(), plugin)
if RESPONSE is not None:
RESPONSE.redirect('manage_workspace')
manage_addBdaUsersPluginForm = PageTemplateFile(
'users_add_plugin.pt',
globals(),
__name__='addBdaUsersPlugin'
)
...
@implementer(
IBdaUsersPlugin,
pas_interfaces.IAuthenticationPlugin,
pas_interfaces.IPropertiesPlugin,
pas_interfaces.IUserEnumerationPlugin,
pas_interfaces.IGroupsPlugin,
)
class BdaUsersPlugin(BasePlugin):
"""BdaSaml2 PAS Plugin
"""
def __init__(self, id, title=None, **kw):
self._setId(id)
self.title = title
self.plugin_caching = True
...
And a page template to make it ttw installable (ZMI):
<h1 tal:replace="structure here/manage_page_header">Header</h1>
<h2> Add poi.pas.saml2 Users plugin</h2>
<p class="form-help">
Adds the poi.pas.saml2 User plugin.
</p>
<form action="manage_addPPS2UserPlugin" method="POST">
<table>
<tr>
<td class="form-label"> Id </td>
<td> <input type="text" name="id" /> </td>
</tr>
<tr>
<td class="form-label"> Title </td>
<td> <input type="text" name="title" /> </td>
</tr>
<tr>
<td colspan="2">
<div class="form-element">
<input type="submit" value="submit"/>
</div>
</td>
</tr>
</table>
</form>
<h1 tal:replace="structure here/manage_page_footer">Footer</h1>
in your packages root configure.zcml
do not forget to zope-initialize (unless you in Products.*
namespace):
<five:registerPackage
initialize=".initialize"
package="."
/>
And in the root __init__.py
add the initialize code for the plugin:
# -*- coding: utf-8 -*-
"""Init and utils."""
from AccessControl.Permissions import add_user_folders
from bda.pas.saml2.plugins import users
from Products.PluggableAuthService import registerMultiPlugin
from zope.i18nmessageid import MessageFactory
import os
def initialize(context):
"""Initializer called when used as a Zope 2 product.
"""
registerMultiPlugin(users.BdaUsersPlugin.meta_type)
context.registerClass(
users.BdaUsersPlugin,
permission=add_user_folders,
icon=os.path.join(os.path.dirname(__file__), 'plugins', 'users.png'),
constructors=(
users.manage_addBdaUsersPluginForm,
users.manage_addBdaUsersPlugin
),
visibility=None
)
The GenericSetup post handler:
# -*- coding: utf-8 -*-
from bda.pas.saml2.interfaces import DEFAULT_ID_USERS
from bda.pas.saml2.plugins.users import BdaUsersPlugin
from Products.CMFPlone.interfaces import INonInstallable
from zope.component.hooks import getSite
from zope.interface import implementer
TITLE_USERS = 'CRM to Plone SAML2 plugin - Users (bda.pas.saml2)'
@implementer(INonInstallable)
class HiddenProfiles(object):
def getNonInstallableProfiles(self):
"""Hide uninstall profile from site-creation and quickinstaller"""
return [
'bda.pas.saml2:uninstall',
]
def _add_plugin(pas, pluginid, title, pluginclass):
if pluginid in pas.objectIds():
return title + ' already installed.'
plugin = pluginclass(pluginid, title=title)
pas._setObject(pluginid, plugin)
plugin = pas[plugin.getId()] # get plugin acquisition wrapped!
for info in pas.plugins.listPluginTypeInfo():
interface = info['interface']
if not interface.providedBy(plugin):
continue
pas.plugins.activatePlugin(interface, plugin.getId())
pas.plugins.movePluginsDown(
interface,
[x[0] for x in pas.plugins.listPlugins(interface)[:-1]],
)
def _remove_plugin(pas, pluginid):
if pluginid in pas.objectIds():
pas.manage_delObjects([pluginid])
def post_install(context):
"""Post install script"""
aclu = getSite().acl_users
_add_plugin(aclu, DEFAULT_ID_USERS, TITLE_USERS, BdaUsersPlugin)
def uninstall(context):
"""Uninstall script"""
aclu = getSite().acl_users
_remove_plugin(aclu, DEFAULT_ID_USERS)
and this is then registered as post_handler
in install/uninstall GS-profiles:
<genericsetup:registerProfile
description="Installs the bda.pas.saml2 add-on."
directory="profiles/default"
name="default"
post_handler=".setuphandlers.post_install"
provides="Products.GenericSetup.interfaces.EXTENSION"
title="bda.pas.saml2"
/>
<genericsetup:registerProfile
description="Uninstalls the bda.pas.saml2 add-on."
directory="profiles/uninstall"
name="uninstall"
post_handler=".setuphandlers.uninstall"
provides="Products.GenericSetup.interfaces.EXTENSION"
title="bda.pas.saml2 (uninstall)"
/>
<utility
factory=".setuphandlers.HiddenProfiles"
name="bda.pas.saml2-hiddenprofiles"
/>
Finally I test the setup itself.
# -*- coding: utf-8 -*-
from bda.pas.saml2.testing import BDAPASSAML2_ZOPE_FIXTURE
import unittest
class TestPluginForUsersCapability(unittest.TestCase):
"""interface plonepas_interfaces.capabilities.IGroupCapability
Test if above interface works as expected
"""
layer = BDAPASSAML2_ZOPE_FIXTURE
def setUp(self):
"""Custom shared utility setup for tests."""
self.aclu = self.layer['app'].acl_users
def test_addplugin_users(self):
PLUGINID = 'bdasaml2test'
from bda.pas.saml2.setuphandlers import _add_plugin
from bda.pas.saml2.plugins.users import BdaUsersPlugin
result = _add_plugin(self.aclu, PLUGINID, 'Users', BdaUsersPlugin)
self.assertIs(result, None)
self.assertIn(PLUGINID, self.aclu.objectIds())
bdasaml2 = self.aclu[PLUGINID]
self.assertIsInstance(bdasaml2, BdaUsersPlugin)
result = _add_plugin(self.aclu, PLUGINID, 'Users', BdaUsersPlugin)
self.assertEqual(result, 'Users already installed.')
def test_removeplugin_users(self):
# add before remove
PLUGINID = 'bdasaml2test'
from bda.pas.saml2.setuphandlers import _add_plugin
from bda.pas.saml2.plugins.users import BdaUsersPlugin
_add_plugin(self.aclu, PLUGINID, 'Users', BdaUsersPlugin)
self.assertIn(PLUGINID, self.aclu.objectIds())
# now remove it
from bda.pas.saml2.setuphandlers import _remove_plugin # noqa
_remove_plugin(self.aclu, pluginid=PLUGINID)
self.assertNotIn(PLUGINID, self.aclu.objectIds())
for completeness here the testing.py
:
# -*- coding: utf-8 -*-
from plone.app.contenttypes.testing import PLONE_APP_CONTENTTYPES_FIXTURE
from plone.app.robotframework.testing import REMOTE_LIBRARY_BUNDLE_FIXTURE
from plone.app.testing import applyProfile
from plone.app.testing import FunctionalTesting
from plone.app.testing import IntegrationTesting
from plone.app.testing import PloneSandboxLayer
from plone.protect import auto
from plone.testing import Layer
from plone.testing import z2
from Products.CMFCore.interfaces import ISiteRoot
from Products.PlonePAS.setuphandlers import migrate_root_uf
from zope.component import provideUtility
import bda.pas.saml2
ORIGINAL_CSRF_DISABLED = auto.CSRF_DISABLED
class BdaPasSaml2ZopeLayer(Layer):
defaultBases = (
z2.INTEGRATION_TESTING,
)
# Products that will be installed, plus options
products = (
('Products.GenericSetup', {'loadZCML': True, 'product': True}, ),
('Products.CMFCore', {'loadZCML': True, 'product': True}, ),
('Products.PluggableAuthService', {
'loadZCML': True, 'product': True},),
('Products.PluginRegistry', {'loadZCML': True, 'product': True}, ),
('Products.PlonePAS', {'loadZCML': True, 'product': True}, ),
('plone.behavior', {'loadZCML': True, 'product': False}, ),
('bda.pas.saml2', {'loadZCML': True, 'product': False}, ),
)
def setUp(self):
self.setUpZCML()
def testSetUp(self):
self.setUpProducts()
provideUtility(self['app'], provides=ISiteRoot)
migrate_root_uf(self['app'])
def setUpZCML(self):
"""Stack a new global registry and load ZCML configuration of Plone
and the core set of add-on products into it.
"""
# Load dependent products's ZCML
from zope.configuration import xmlconfig
from zope.dottedname.resolve import resolve
import z3c.autoinclude
xmlconfig.file(
'meta.zcml',
z3c.autoinclude,
context=self['configurationContext']
)
def loadAll(filename):
for p, config in self.products:
if not config['loadZCML']:
continue
try:
package = resolve(p)
except ImportError:
continue
try:
xmlconfig.file(
filename,
package,
context=self['configurationContext']
)
except IOError:
pass
loadAll('meta.zcml')
loadAll('configure.zcml')
loadAll('overrides.zcml')
def setUpProducts(self):
"""Install all old-style products listed in the the ``products`` tuple
of this class.
"""
for prd, config in self.products:
if config['product']:
z2.installProduct(self['app'], prd)
BDAPASSAML2_ZOPE_FIXTURE = BdaPasSaml2ZopeLayer()
class BdaPasSaml2PloneLayer(PloneSandboxLayer):
defaultBases = (PLONE_APP_CONTENTTYPES_FIXTURE,)
def setUpZope(self, app, configurationContext):
auto.CSRF_DISABLED = True
self.loadZCML(package=bda.pas.saml2)
z2.installProduct(app, 'bda.pas.saml2')
def tearDownZope(self, app):
auto.CSRF_DISABLED = ORIGINAL_CSRF_DISABLED
def setUpPloneSite(self, portal):
applyProfile(portal, 'bda.pas.saml2:default')
BDAPASSAML2_PLONE_FIXTURE = BdaPasSaml2PloneLayer()
BDAPASSAML2_PLONE_INTEGRATION_TESTING = IntegrationTesting(
bases=(BDAPASSAML2_PLONE_FIXTURE,),
name='BdaPasSaml2PloneLayer:IntegrationTesting'
)
BDAPASSAML2_PLONE_FUNCTIONAL_TESTING = FunctionalTesting(
bases=(BDAPASSAML2_PLONE_FIXTURE,),
name='BdaPasSaml2PloneLayer:FunctionalTesting'
)
BDAPASSAML2_PLONE_ACCEPTANCE_TESTING = FunctionalTesting(
bases=(
BDAPASSAML2_PLONE_FIXTURE,
REMOTE_LIBRARY_BUNDLE_FIXTURE,
z2.ZSERVER_FIXTURE
),
name='BdaPasSaml2PloneLayer:AcceptanceTesting'
)
I hope that's it and I pasted all relevant parts.
HTH