Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Plone 5 compatibilty and features #5

Merged
merged 5 commits into from Aug 28, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 4 additions & 7 deletions .gitignore
@@ -1,18 +1,13 @@
*~
*.pyc
*.pyo
*.py[cod]
*.tmp*
*.mo
*.egg
*.EGG
*.EGG*
*.egg*
*.EGG-INFO
*.kpf
*.swp
*.wpr
.*.cfg
.installed.cfg
.mr.developer.cfg
.hg/
.bzr/
.svn/
Expand All @@ -27,6 +22,8 @@ fake-eggs/
parts/
dist/
var/
include/
lib*



Expand Down
10 changes: 9 additions & 1 deletion CHANGES.txt
@@ -1,9 +1,17 @@
1.2 (unreleased)
2.0 (unreleased)
================

- Added challenge header and replacement pattern from WSA
[tomgross]

- Plone 5 compatibility
[tomgross]

- Ported tests to plone.app.testing
[tomgross]

- Allow other default roles than Member
[Sebastian Gottfried]

1.1 (2014-11-06)
================
Expand Down
86 changes: 54 additions & 32 deletions Products/AutoUserMakerPASPlugin/auth.py
Expand Up @@ -29,6 +29,7 @@

from Products.PluggableAuthService.PluggableAuthService import \
_SWALLOWABLE_PLUGIN_EXCEPTIONS
from Products.CMFPlone.utils import safeToInt

stripDomainNamesKey = 'strip_domain_names'
stripDomainNamesListKey = 'strip_domain_name_list'
Expand All @@ -44,21 +45,19 @@
httpSharingTokensKey = 'http_sharing_tokens'
httpSharingLabelsKey = 'http_sharing_labels'
usernameKey = 'user_id'
useCustomRedirectionKey = 'use_custom_redirection'
challengePatternKey = 'challenge_pattern'
challengeReplacementKey = 'challenge_replacement'
challengeHeaderEnabledKey = 'challenge_header_enabled'
challengeHeaderNameKey = 'challenge_header_name'
defaultRolesKey = 'default_roles'

PWCHARS = string.letters + string.digits + string.punctuation

_defaultChallengePattern = re.compile('http://(.*)')
_defaultChallengePattern = 'http://(.*)'
_defaultChallengeReplacement = r'https://\1'


def safe_int(s, default=0):
try:
return int(s)
except (ValueError, TypeError):
return default


class AutoUserMakerPASPlugin(BasePlugin):
""" An authentication plugin that creates member objects

Expand Down Expand Up @@ -110,7 +109,6 @@ def authenticateCredentials(self, credentials):
# Make a user with id `userId`, and assign him at least the Member
# role, since user doesn't exist.


# Make sure we actually have user adders and role assigners. It
# would be ugly to succeed at making the user but be unable to
# assign him the role.
Expand Down Expand Up @@ -209,33 +207,37 @@ def authenticateCredentials(self, credentials):

security.declarePublic('loginUrl')
def loginUrl(self, currentUrl):
"""Given the URL of the page where the user presently is, return the URL which will prompt him for authentication and land him at the same place.
"""Given the URL of the page where the user presently is,
return the URL which will prompt him for authentication
and land him at the same place.

If something goes wrong, return ''.

"""
usingCustomRedirection = False
pattern, replacement = _defaultChallengePattern, _defaultChallengeReplacement
match = pattern.match(currentUrl)
config = self.getConfig()
usingCustomRedirection = config[useCustomRedirectionKey]
pattern, replacement = usingCustomRedirection and \
(config[challengePatternKey], config[challengeReplacementKey]) or \
(_defaultChallengePattern, _defaultChallengeReplacement)
match = re.match(pattern, currentUrl)
# Let the web server's auth have a swing at it:
if match:
# will usually start with http:// but may start with
# https:// (and thus not match) if you're already logged in
# and try to access something you're not privileged to
if match: # will usually start with http:// but may start with
# https:// (and thus not match) if you're already
# logged in and try to access something you're not privileged to
try:
destination = match.expand(replacement)
except re.error:
# Don't screw up your replacement string, please. If you do, we
# at least try not to punish the user with a traceback.
except re.error: # Don't screw up your replacement string, please.
# If you do, we at least try not to punish the user with a traceback.
if usingCustomRedirection:
logger.error(("Your custom Replacement Pattern could not be "
"applied to a URL which needs authentication:"
" %s. Please correct it." % currentUrl))
logger.error("Your custom WebServerAuth Replacement "
"Pattern could not be applied to a URL " \
"which needs authentication: %s. Please correct it." % currentUrl)
else:
return destination
# Our regex didn't match, or something went wrong.
return ''


security.declarePrivate('challenge')
def challenge(self, request, response):
url = self.loginUrl(request.ACTUAL_URL)
Expand Down Expand Up @@ -310,6 +312,11 @@ def __init__(self):
(httpSharingLabelsKey, 'lines', 'w', []),
('required_roles', 'lines', 'wd', []),
('login_users', 'lines', 'wd', []),
(useCustomRedirectionKey, 'boolean', 'w', False),
(challengePatternKey, 'string', 'w', _defaultChallengePattern),
(challengeReplacementKey, 'string', 'w', _defaultChallengeReplacement),
(challengeHeaderEnabledKey, 'boolean', 'w', False),
(challengeHeaderNameKey, 'string', 'w', ""),
(defaultRolesKey, 'lines', 'w', ['Member']))
# Create any missing properties
ids = {}
Expand Down Expand Up @@ -339,6 +346,10 @@ def getConfig(self):
>>> import pprint
>>> pprint.pprint(handler.getConfig())
{'auto_update_user_properties': 0,
'challenge_header_enabled': False,
'challenge_header_name': '',
'challenge_pattern': 'http://(.*)',
'challenge_replacement': 'https://\\\\1',
'default_roles': ('Member',),
'http_authz_tokens': (),
'http_commonname': ('HTTP_SHIB_PERSON_COMMONNAME',),
Expand All @@ -351,7 +362,9 @@ def getConfig(self):
'http_sharing_tokens': (),
'http_state': ('HTTP_SHIB_ORGPERSON_STATE',),
'strip_domain_name_list': (),
'strip_domain_names': 1}
'strip_domain_names': 1,
'use_custom_redirection': False}

"""
return {
stripDomainNamesKey: self.getProperty(stripDomainNamesKey),
Expand All @@ -367,6 +380,11 @@ def getConfig(self):
httpAuthzTokensKey: self.getProperty(httpAuthzTokensKey),
httpSharingTokensKey: self.getProperty(httpSharingTokensKey),
httpSharingLabelsKey: self.getProperty(httpSharingLabelsKey),
useCustomRedirectionKey : self.getProperty(useCustomRedirectionKey),
challengePatternKey: self.getProperty(challengePatternKey),
challengeReplacementKey: self.getProperty(challengeReplacementKey),
challengeHeaderEnabledKey: self.getProperty(challengeHeaderEnabledKey),
challengeHeaderNameKey: self.getProperty(challengeHeaderNameKey)
defaultRolesKey: self.getProperty(defaultRolesKey)}

security.declarePublic('getSharingConfig')
Expand Down Expand Up @@ -473,7 +491,6 @@ def loginUsers(self):
def defaultRoles(self):
return self.getProperty('default_roles', ['Member'])


security.declarePrivate('extractCredentials')
def extractCredentials(self, request):
"""Search a Zope request for Shibboleth tokens. See IExtractionPlugin.
Expand Down Expand Up @@ -626,7 +643,7 @@ def getRoles(self):
>>> from Products.AutoUserMakerPASPlugin.auth import \
ApacheAuthPluginHandler
>>> handler = ApacheAuthPluginHandler('someId')
>>> handler = handler.__of__(self.portal.acl_users)
>>> handler = handler.__of__(layer['portal'].acl_users)
>>> from pprint import pprint
>>> roles = [role['id'] for role in handler.getRoles()]
>>> result = ['Contributor', 'Editor', 'Manager', 'Owner', 'Reader', 'Reviewer']
Expand All @@ -644,7 +661,7 @@ def getUsers(self):
>>> from Products.AutoUserMakerPASPlugin.auth import \
ApacheAuthPluginHandler
>>> handler = ApacheAuthPluginHandler('someId')
>>> handler = handler.__of__(self.portal.acl_users)
>>> handler = handler.__of__(layer['portal'].acl_users)
>>> handler.getUsers()
['', 'test_user_1_']
"""
Expand All @@ -661,7 +678,7 @@ def getGroups(self):
>>> from Products.AutoUserMakerPASPlugin.auth import \
ApacheAuthPluginHandler
>>> handler = ApacheAuthPluginHandler('someId')
>>> handler = handler.__of__(self.portal.acl_users)
>>> handler = handler.__of__(layer['portal'].acl_users)
>>> groups = handler.getGroups()
>>> 'Administrators' in groups
True
Expand Down Expand Up @@ -696,9 +713,9 @@ def manage_changeConfig(self, REQUEST=None):
if not REQUEST:
return None
reqget = REQUEST.form.get
strip = safe_int(reqget(stripDomainNamesKey, 1), default=1)
strip = safeToInt(reqget(stripDomainNamesKey, 1), default=1)
strip = max(min(strip, 2), 0) # 0 < x < 2
autoupdate = safe_int(reqget(autoUpdateUserPropertiesKey, 0),
autoupdate = safeToInt(reqget(autoUpdateUserPropertiesKey, 0),
default=0)
# If Shib fields change, then update the authz_mappings property.
tokens = self.getTokens()
Expand Down Expand Up @@ -726,6 +743,11 @@ def manage_changeConfig(self, REQUEST=None):
httpAuthzTokensKey: reqget(httpAuthzTokensKey, ''),
httpSharingTokensKey: reqget(httpSharingTokensKey, ''),
httpSharingLabelsKey: reqget(httpSharingLabelsKey, ''),
useCustomRedirectionKey: reqget(useCustomRedirectionKey, False),
challengeReplacementKey: reqget(challengeReplacementKey, ''),
challengePatternKey: reqget(challengePatternKey, ''),
challengeHeaderEnabledKey: reqget(challengeHeaderEnabledKey, False),
challengeHeaderNameKey: reqget(challengeHeaderNameKey, ''),
defaultRolesKey: reqget(defaultRolesKey, '')})
return REQUEST.RESPONSE.redirect('%s/manage_config' %
self.absolute_url())
Expand Down Expand Up @@ -787,8 +809,8 @@ def manage_changeMapping(self, REQUEST=None):
# now process delete checkboxes
deleteIds = REQUEST.form.get('delete_ids', [])
# make sure deleteIds is a list on integers, in descending order
deleteIds = [safe_int(did)
for did in deleteIds if safe_int(did, None) != None]
deleteIds = [safeToInt(did)
for did in deleteIds if safeToInt(did, None) != None]
deleteIds.sort(reverse=True)
# now shorten without shifting indexes of items still to be removed
for ii in deleteIds:
Expand Down
64 changes: 64 additions & 0 deletions Products/AutoUserMakerPASPlugin/config.zpt
Expand Up @@ -11,6 +11,69 @@
tal:define="config context/getConfig"
method="POST">

<fieldset style="margin: .5em 0 0 0; padding: .5em; border: none">
<legend>To prompt the user for credentials, redirect...</legend>
<table border="0" cellspacing="0" cellpadding="2" style="margin: 0 0 1em 2em">
<tr valign="top">
<td>
<input type="radio" name="use_custom_redirection" id="use_custom_redirection_0" value="0" tal:attributes="checked not:config/use_custom_redirection" />
</td>
<td>
<label for="use_custom_redirection_0">To the HTTPS version of wherever he was going</label>
</td>
</tr>
<tr valign="top">
<td>
<input type="radio" name="use_custom_redirection" id="use_custom_redirection_1" value="1" tal:attributes="checked config/use_custom_redirection" />
</td>
<td>
<label for="use_custom_redirection_1">To a custom URL:</label>
</td>
</tr>
<tr valign="top">
<td>
</td>
<td>
<table border="0" cellspacing="0" cellpadding="2">
<tr valign="top">
<td align="right">
<label for="challenge_pattern">Matching&nbsp;pattern:</label>
</td>
<td>
<input name="challenge_pattern" id="challenge_pattern" type="text" size="40" tal:attributes="value config/challenge_pattern" />
<p class="form-help" style="margin: .5em 0">
<font size="-1">A regular expression matching every URL in your Plone site and capturing (using parentheses) the parts you'll need when constructing the replacement pattern.</font>
</p>
<p class="form-help" style="margin: .5em 0">
<font size="-1"><a href="http://www.python.org/doc/2.5.2/lib/re-syntax.html" target="_blank">Regular expression reference</a></font>
</p>
</td>
</tr>
<tr valign="top">
<td align="right">
<label for="challenge_replacement">Replacement&nbsp;pattern:</label>
</td>
<td>
<input name="challenge_replacement" id="challenge_replacement" type="text" size="40" tal:attributes="value config/challenge_replacement" />
<p class="form-help" style="margin: .5em 0">
<font size="-1">The URL to redirect to. Make sure it's an HTTPS URL, and <a href="http://docs.python.org/library/re.html#re.sub">use backreferences</a> (like \1, \2, and so on) to substitute in the parts you captured above.</font>
</p>
</td>
</tr>
</table>
</td>
</tr>
<tr valign="top">
<td style="padding-top: 1em">
<input type="checkbox" id="challenge_header_enabled" name="challenge_header_enabled" value="1" tal:attributes="checked config/challenge_header_enabled" />
</td>
<td style="padding-top: 1em">
<label for="challenge_header_enabled">Only when the</label> <input type="text" size="40" name="challenge_header_name" id="challenge_header_name" tal:attributes="value config/challenge_header_name" /> <label for="challenge_header_enabled">request header is present</label>
</td>
</tr>
</table>
</fieldset>
<fieldset style="margin: .5em 0 0 0; padding: .5em; border: none">
<p>
<tal:use_session tal:condition="python:config['strip_domain_names'] == 0">
<input type="radio" id="strip_domain_names0" name="strip_domain_names"
Expand Down Expand Up @@ -179,6 +242,7 @@
tal:content="python:'\n'.join(config['http_sharing_labels'])">
</textarea>
</p>
</fieldset>

<input type="submit" i18n:domain="plone"
i18n:attributes="value" value="Save" />
Expand Down