Challenge Protocol Chooser -------------------------- The Challenge Protocol Chooser is a plugin that decides what authentication protocol to use for a given request type. Let's start by setting up a PAS instance inside our existing test folder. >>> folder = self.folder >>> 'acl_users' in folder.objectIds() True >>> folder.manage_delObjects(ids=['acl_users']) >>> 'acl_users' in folder.objectIds() False >>> dispatcher = folder.manage_addProduct['PluggableAuthService'] >>> dispatcher.addPluggableAuthService() >>> 'acl_users' in folder.objectIds() True >>> folder.acl_users.meta_type 'Pluggable Auth Service' Now, we'll setup this PAS instance with what most people would get by default, users and roles stored in ZODB with HTTP Basic auth. >>> pas = folder.acl_users >>> dispatcher = pas.manage_addProduct['PluggableAuthService'] >>> dispatcher.addZODBUserManager('users') >>> dispatcher.addZODBRoleManager('roles') >>> dispatcher.addHTTPBasicAuthHelper('http_auth') >>> plugins = pas.plugins >>> from Products.PluggableAuthService.interfaces.plugins import \ ... IAuthenticationPlugin, IUserEnumerationPlugin, IRolesPlugin, \ ... IRoleEnumerationPlugin, IRoleAssignerPlugin, \ ... IChallengePlugin, IExtractionPlugin, IUserAdderPlugin >>> plugins.activatePlugin(IUserAdderPlugin, 'users') >>> plugins.activatePlugin(IAuthenticationPlugin, 'users') >>> plugins.activatePlugin(IUserEnumerationPlugin, 'users') >>> plugins.activatePlugin(IRolesPlugin, 'roles') >>> plugins.activatePlugin(IRoleEnumerationPlugin, 'roles') >>> plugins.activatePlugin(IRoleAssignerPlugin, 'roles') >>> plugins.activatePlugin(IExtractionPlugin, 'http_auth') >>> plugins.activatePlugin(IChallengePlugin, 'http_auth') Create a user for testing: >>> pas.getUserById('test_user_') is None True >>> username, password = 'test_user_', 'test_user_pw' >>> pas._doAddUser(username, password, ['Manager'], []) >>> pas.getUserById('test_user_') is None False We are now going to try some different kinds of requests and make sure all of them work. They all use HTTP Basic Auth, which is the default in this configuration we just set up. For the sake of testing, we are going to create a simple script that requires the 'Manager' role to be called. >>> folder_name = folder.getId() >>> dispatcher = folder.manage_addProduct['PythonScripts'] >>> _ = dispatcher.manage_addPythonScript('test_script') >>> script = folder._getOb('test_script') >>> script.write('return "Access Granted"') >>> script.manage_permission(permission_to_manage='View', ... roles=['Manager'], acquire=0) Access the script through a simple ``GET`` request, simulating browser access. Anonymous user should be challenged with a 401 response status. >>> print http(r""" ... GET /%s/test_script HTTP/1.1 ... """ % (folder_name), handle_errors=True) HTTP/1.1 401 Unauthorized... With the right credentials though the request should succeed: >>> print http(r""" ... GET /%s/test_script HTTP/1.1 ... Authorization: Basic %s:%s ... """ % (folder_name, username, password), handle_errors=True) HTTP/1.1 200 OK ... Access Granted Now a PROPFIND request, simulating a WebDAV client. Anonymous user should be challenged with a 401 response status: >>> print http(r""" ... PROPFIND /%s/test_script HTTP/1.1 ... """ % (folder_name), handle_errors=True) HTTP/1.1 401 Unauthorized... And with the right credentials the request should succeed: >>> print http(r""" ... PROPFIND /%s/test_script HTTP/1.1 ... Authorization: Basic %s:%s ... """ % (folder_name, username, password), handle_errors=True) HTTP/1.1 207 Multi-Status... >>> print http(r""" ... GET /%s/test_script/manage_DAVget HTTP/1.1 ... Authorization: Basic %s:%s ... """ % (folder_name, username, password), handle_errors=True) HTTP/1.1 200 OK... And a XML-RPC Request. Again, Anonymous user should be challenged with a 401 response status. >>> print http(r""" ... POST /%s HTTP/1.1 ... Content-Type: text/xml; charset="utf-8" ... ... ... test_script ... ... """ % (folder_name), handle_errors=True) HTTP/1.1 401 Unauthorized... And with valid credentials the reqeuest should succeed: >>> print http(r""" ... POST /%s HTTP/1.1 ... Content-Type: text/xml; charset="utf-8" ... Authorization: Basic %s:%s ... ... ... test_script ... ... """ % (folder_name, username, password), handle_errors=True) HTTP/1.1 200 OK Content-Length: 140 Content-Type: text/xml Access Granted Adding a Cookie Auth Helper now to test the correct behaviour of the Challenge Protocol Helper. >>> dispatcher = pas.manage_addProduct['PluggableAuthService'] >>> dispatcher.addCookieAuthHelper('cookie_auth', ... cookie_name='__ac') >>> plugins.activatePlugin(IExtractionPlugin, 'cookie_auth') >>> plugins.activatePlugin(IChallengePlugin, 'cookie_auth') Re-activate HTTP Auth Helper so that it appears **after** Cookie Auth Helper: >>> plugins.deactivatePlugin(IExtractionPlugin, 'http_auth') >>> plugins.activatePlugin(IExtractionPlugin, 'http_auth') >>> plugins.deactivatePlugin(IChallengePlugin, 'http_auth') >>> plugins.activatePlugin(IChallengePlugin, 'http_auth') Now, invalid credentials should result in a 302 response status for a normal (eg: browser) request: >>> print http(r""" ... GET /%s/test_script HTTP/1.1 ... """ % (folder_name), handle_errors=True) HTTP/1.1 302 Moved Temporarily... And the same for a WebDAV request: >>> print http(r""" ... PROPFIND /%s/test_script HTTP/1.1 ... """ % (folder_name), handle_errors=True) HTTP/1.1 302 Moved Temporarily... >>> print http(r""" ... GET /%s/test_script/manage_DAVget HTTP/1.1 ... """ % (folder_name), handle_errors=True) HTTP/1.1 302 Moved Temporarily... And for a XML-RPC request: >>> print http(r""" ... POST /%s HTTP/1.1 ... Content-Type: text/xml; charset="utf-8" ... ... ... test_script ... ... """ % (folder_name), handle_errors=True) HTTP/1.1 302 Moved Temporarily... However, not all WebDAV and XML-RPC clients understand the redirect. Even worse, they will not be able to display the login form that is the target of this redirect. For this reason we should disable the Cookie Auth Helper for non-browser requests. In fact, we might only want plugins that understand the 'http' authorization protocol to issue challenges for WebDAV and XML-RPC. To do this, we use the Challenge Protocol Chooser plugin together with the Request Type Sniffer plugin. >>> from Products.PluggableAuthService.interfaces.plugins import \ ... IRequestTypeSniffer, IChallengeProtocolChooser >>> dispatcher = pas.manage_addProduct['PluggableAuthService'] >>> dispatcher.addRequestTypeSnifferPlugin('sniffer') >>> plugins.activatePlugin(IRequestTypeSniffer, 'sniffer') >>> mapping = {'WebDAV': ['http'], ... 'XML-RPC': ['http'], ... 'Browser': []} >>> dispatcher.addChallengeProtocolChooserPlugin('chooser', ... mapping=mapping) >>> plugins.activatePlugin(IChallengeProtocolChooser, 'chooser') Now, invalid credentials should result in a 302 response status for a normal (eg: browser) request: >>> print http(r""" ... GET /%s/test_script HTTP/1.1 ... """ % (folder_name), handle_errors=True) HTTP/1.1 302 Moved Temporarily... A WebDAV request should result in a 401 response status: >>> print http(r""" ... PROPFIND /%s/test_script HTTP/1.1 ... """ % (folder_name), handle_errors=True) HTTP/1.1 401 Unauthorized... >>> print http(r""" ... GET /%s/test_script/manage_DAVget HTTP/1.1 ... """ % (folder_name), handle_errors=True) HTTP/1.1 401 Unauthorized... And a XML-RPC request should also result in a 401 response status: >>> print http(r""" ... POST /%s HTTP/1.1 ... Content-Type: text/xml; charset="utf-8" ... ... ... test_script ... ... """ % (folder_name), handle_errors=True) HTTP/1.1 401 Unauthorized...