Skip to content

Commit

Permalink
Merge pull request #5 from collective/tomgross-loginurlconfig
Browse files Browse the repository at this point in the history
Plone 5 compatibilty and features
  • Loading branch information
tomgross committed Aug 28, 2015
2 parents 93c192e + a15a983 commit c14d210
Show file tree
Hide file tree
Showing 14 changed files with 251 additions and 131 deletions.
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

0 comments on commit c14d210

Please sign in to comment.