RESTfulAuth
I wrote this tool, based on the DigestAuth tool shipped with CherryPy, because standard digest auth per RFC2617 does not offer any way of logging a user out or of time-limiting the use of the authentication credentials. I wanted an authentication scheme which would work well with my app which is designed on RESTful lines - don't add new protocols if HTTP will do the job. In particular I wanted to avoid the use (and added complexity) of cookies and sessions.
The RFC does suggest the use of expiring nonces as a way of mitigating replay attacks. Expiring nonces also allow all the extra features I wanted to be included in digest authentication.
So this tool extends the standard DigestAuth tool by:
1. Allowing the time for which a nonce is valid to be specified so that a user is automatically logged out when the nonce expires.
2. Allowing a nonce refresh period to be specified to provide some protection against replay attacks.
3. Allowing the user to log out by visiting a link which expires all her nonces.
4. Providing a simple role based authorisation scheme.
5. Allows authorisation to be specified per-method and per URI.
6. Facilitates the use of this tool in an suexec CGI or mod_wsgi environment by allowing the HTTP Authorization header to be passed in with a different name.
All the above are configured on a per-URI basis in the CherryPy config.
This scheme is susceptible to all the attacks described in the RFC with the exception that it allows replay attacks to be mitigated. But for any serious application SSL will mitigate the remaining attacks. Server side security (eg of the password database) is not addressed by this tool.
Changes from DigestAuth
The nonce storage mechanism is added. This uses the filesystem as a database. The nonce files created are never read - the tool relies entirely on the nonce file metadata.
The nonces are generated by the tool - the standard DigestAuth tool will frequently produce identical nonces since the timestamp it uses has only a 1 second granularity.
Caveats This tool was designed for use in a low traffic environment with a total user count in the hundreds.
I'm not a security expert, but I believe that this tool is at least as secure as the built-in DigestAuth tool.
The test cases (after the code) are not terribly comprehensive.
""" Authentication and authorisation tool for RESTful apps.
Authentication is performed by expiring-nonce HTTP digest authentication,
providing a means of time-limiting logins and a means of logging users out.
Authorisation uses role-based access control.
Tool parameters are:
realm: a string containing the authentication realm.
users: a dict of the form: {username: (password, role1, ...)} or a
callable returning the password/roles tuple when passed the
username.
refresh: an integer giving the refresh time for nonces in seconds.
expires: an integer giving the maximum life of a nonce (from the last
refresh if refreshed). Zero indicates a one-time nonce. If less
than 'refresh' (but not zero) 'refresh' will be used as the
expiry time.
logout: a boolean value. True indicates that all nonces originating from
the client making the request to this resource should be expired
(regardless of realm). This allows a 'logout' function to be
implemented without the use of javascript, cookies or .htaccess
magic. A 'logout' link should point to a non-cacheable resource
that is protected by restful_auth with this flag set.
Default is False.
roles: a dict of the form {method1: (role1, role2, ...), ....}
The roles for the current method are compared to the user's
roles as returned from the 'users' parameter. If no match is
found the authentication fails. The default is an empty dict,
in which case this tool does not check roles - any authenticated
user will be authorised by this tool. The roles are available to
the page handler in cherrypy.resource.loginroles.
If the 'expires' parameter is greater than 'refresh', then when an nonce
has been issued for the time given by the 'refresh' parameter a new nonce
will be sent to the client together with the 'stale=true' flag, avoiding
the need for the user to re-enter her credentials. The 'refresh' timer is
then restarted. If both the 'refresh' and the 'expires' times have been
exceeded then the 'stale' flag will not be sent and the user must
re-authenticate.
Some examples of using the restful-auth tool:
The root of the application tree. Here we set up the default parameters for
the application tree. The nonce will be refreshed every minute and will
expire if there is no activity for 10 minutes.
app = main.AppRoot(config, {'tools.restful_auth.on' : False,
'tools.restful_auth.realm': 'Application Realm',
'tools.restful_auth.users': lib.get_users,
'tools.restful_auth.refresh': 60,
'tools.restful_auth.expires': 600})
This URI expires all the nonces from this host.
app.logout = main.Logout(config, {'tools.restful_auth.on': True,
'tools.restful_auth.logout': True})
Here we set up the access rights for this branch of the tree. Guests and
users can read the resource, users can modify it, and administrators can
create new resources. In the users dict administrators would have both the
'admin' and the 'user' roles.
app.branch = uri.branch(config,
{'tools.restful_auth.roles': {'GET': ('guest', 'user'),
'PUT': ('user'),
'POST': ('admins')}})
Anyone can access a this leaf - restful_auth is off.
app.branch.leaf1 = uri.Leaf1(config)
Only users with the appropriate roles can access this leaf.
app.branch.leaf2 = uri.Leaf2(config, {'tools.restful_auth.on' : True})
This leaf does something serious like modify application settings, so we
specify a new realm, a one-time nonce so that every access must be
authenticated, and only allow access to a user with the superadmin role.
app.branch.leaf3 = uri.Leaf3(config,
{'tools.restful_auth.on':True,
'tools.restful_auth.realm':'Administrator realm',
'tools.restful_auth.users': lib.get_users,
'tools.restful_auth.refresh': 15,
'tools.restful_auth.expires': 0,
'tools.restful_auth.roles': {'GET': ('superadmin',),
'POST': ('superadmin',)}})
Copyright (c) PR Hardman 2007.
This module is free software, and you may redistribute it and/or modify
it under the same terms as Python itself, so long as this copyright message
and disclaimer are retained in their original form.
IN NO EVENT SHALL THE AUTHOR BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
THIS CODE, EVEN IF THE AUTHOR HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
THE AUTHOR SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE. THE CODE PROVIDED HEREUNDER IS ON AN "AS IS" BASIS,
AND THERE IS NO OBLIGATION WHATSOEVER TO PROVIDE MAINTENANCE,
SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
Created 5 December 2007
"""
import time
import os
import glob
import md5
import random
import cherrypy
import cherrypy.lib.httpauth as httpauth
#------------------------------- restful_auth tool ---------------------#
NONCE_DIR = os.path.join(os.path.dirname(__file__), ".nonce")
N_EXPIRED = 1
N_REFRESH = 2
N_VALID = 3
STALE_WWW_AUTH = 'Digest realm="%s", nonce="%s", algorithm="MD5", qop="auth", stale=true'
STD_WWW_AUTH = 'Digest realm="%s", nonce="%s", algorithm="MD5", qop="auth"'
def nonce_state(nonce, expires):
""" Report the nonce state.
A nonce may be: 1) Expired: Doesn't exist or both 'refresh' and
'expires' exceeded
2) Refresh due: 'refresh' exceeded but 'expires' not
exceeded
3) One-time: 'expires' is 0. Expire forcibly (delete).
3) Valid: none of the above
"""
fn = '%s-%s' % (nonce, cherrypy.request.remote.ip)
filename = os.path.join(NONCE_DIR, fn)
if not os.path.exists(filename):
# Already expired
return N_EXPIRED
stats = os.stat(filename)
if stats.st_mtime < time.time():
# Expire the nonce
os.remove(filename)
return N_EXPIRED
elif stats.st_atime < time.time():
os.remove(filename)
return N_REFRESH
if expires == 0:
os.remove(filename)
return N_VALID
return N_VALID
def record_nonce(nonce, refresh, expires):
""" Record the nonce together with it's expiry time and the client IP """
if not os.path.exists(NONCE_DIR):
os.mkdir(NONCE_DIR, 0700)
# The nonce is a hex digest so it can be used as
# a filename with the client ip appended.
fn = '%s-%s' % (nonce, cherrypy.request.remote.ip)
filename = os.path.join(NONCE_DIR, fn)
# expire any stale nonces
for name in glob.glob(os.path.join(NONCE_DIR,'*')):
if os.stat(name).st_mtime < time.time():
os.remove(name)
# Record this nonce/IP
try:
fd = os.open(filename, os.O_RDONLY | os.O_CREAT | os.O_EXCL)
except:
raise
os.close(fd)
# Mark when this nonce will expire
now = int(time.time())
if expires > refresh:
os.utime(filename, (now + refresh, now + expires))
else:
os.utime(filename, (now + refresh, now + refresh))
return
def do_logout():
""" Expire all nonces from the current client """
client_ip = cherrypy.request.remote.ip
for name in glob.glob(os.path.join(NONCE_DIR,'*')):
if name not in ('.', '..'):
if name[name.rfind('-') + 1:] == client_ip:
os.remove(name)
return
def check_roles(resource_roles, user_roles):
""" Check that one of the resource roles matches the list of user roles """
for role in resource_roles:
if role in user_roles:
return True
return False
def check_auth(users, realm, expires, roles):
"""If an authorization header contains valid credentials return True,
else return False. If the nonce has expired but the expires time has not
been exceeded return the 'stale' flag.
If the application is running under CGI/suexec or mod_wsgi without the
WSGIPassAuthorization directive then the authorization
header (which will be removed by suexec/mod_wsgi) must be written to the
x-dbappauth header by mod-rewrite and mod_headers in the application's
.htaccess file as follows:
RewriteEngine on
RewriteCond %{REQUEST_FILENAME} !-f
RewriteRule ^(.*)$ /cgi-bin/run_cgi.cgi/$1 [E=MY_HTTP_AUTH:%{HTTP:Authorization},QSA,PT,L]
RequestHeader set x-myappauth "%{MY_HTTP_AUTH}e" env=MY_HTTP_AUTH
Based on cherrypy.lib.auth.check_auth()
"""
auth = False
stale = False
if 'authorization' in cherrypy.request.headers:
auth_hdr = cherrypy.request.headers['authorization']
elif 'x-myappauth' in cherrypy.request.headers:
auth_hdr = cherrypy.request.headers['x-myappauth']
if not auth_hdr:
return auth, stale
else:
return auth, stale
# make sure the provided credentials are correctly set
ah = httpauth.parseAuthorization(auth_hdr)
if ah is None:
raise cherrypy.HTTPError(400, 'Bad Request')
# These lines only required for CherryPy pre 3.1.0rc1
if ah['realm'] != realm:
return auth, stale
nstate = nonce_state(ah['nonce'], expires)
if nstate == N_EXPIRED:
# No need to validate the authorization
return auth, stale
elif nstate == N_REFRESH:
stale = True
encrypt = httpauth.DIGEST_AUTH_ENCODERS[httpauth.MD5]
if callable(users):
# A callable must return a tuple
userinfo = users(ah["username"])
else:
if not isinstance(users, dict):
raise ValueError, "Authentication users must be a dict of tuples"
# fetch the user password
userinfo = users.get(ah["username"], None)
if not isinstance(userinfo, tuple):
raise ValueError, "Authentication users must be a dict of tuples"
if userinfo:
# Check that the user's role matches the role allowed
if not roles or check_roles(roles.get(cherrypy.request.method),
userinfo[1:]):
# Validate the authorization by re-computing it here
# and compare it with what the user-agent provided
if httpauth.checkResponse(ah, userinfo[0],
method=cherrypy.request.method,
encrypt=encrypt, realm=realm):
cherrypy.request.login = ah["username"]
cherrypy.request.loginroles = userinfo[1:]
return True, stale
cherrypy.request.login = False
cherrypy.request.loginroles = ()
return auth, stale
def restful_auth(realm, users, refresh, expires, logout=False, roles=None):
""" Authentication and authorisation tool for RESTful apps.
Authentication is performed by nonce-expiring HTTP digest authentication
providing a means of time-limiting logins an a means of logging users out.
Authorisation uses role-based access control.
Tool parameters are:
realm: a string containing the authentication realm.
users: a dict of the form: {username: (password, role1, ...)} or a
callable returning the password/roles tuple when passed the
username.
refresh: an integer giving the refresh time for nonces in seconds.
expires: an integer giving the maximum life of a nonce (from the last
refresh if refreshed). Zero indicates a one-time nonce. If less
than 'refresh' (but not zero) 'refresh' will be used as the
expiry time.
logout: a boolean value. True indicates that all nonces originating from
the client making the request to this resource should be expired
(regardless of realm). This allows a 'logout' function to be
implemented without the use of javascript, cookies or .htaccess
magic. A 'logout' link should point to a non-cacheable resource
that is protected by restful_auth with this flag set.
Default is False.
roles: a dict of the form {method1: (role1, role2, ...), ....}
The roles for the current method are compared to the user's
roles as returned from the 'users' parameter. If no match is
found the authentication fails. The default is an empty dict,
in which case this tool does not check roles - any authenticated
user will be authorised by this tool. The roles are available to
the page handler in cherrypy.resource.loginroles.
If the 'expires' parameter is greater than 'refresh', then when an nonce
has been issued for the time given by the 'refresh' parameter a new nonce
will be sent to the client together with the 'stale=true' flag, avoiding
the need for the user to re-enter her credentials. The 'refresh' timer is
then restarted. If both the 'refresh' and the 'expires' times have been
exceeded then the 'stale' flag will not be sent and the user must
re-authenticate.
"""
if logout:
do_logout()
return
auth, stale = check_auth(users, realm, expires, roles)
# Calculate the nonce and build the www-authenticate header here
# rather than use the function in httpauth because that function may give
# duplicate nonces since it only uses the seconds from time.time().
if auth:
if not stale:
return
else:
nonce = md5.new("%d:%s:%.6f" %
(time.time(), realm, random.random())).hexdigest()
www_auth = STALE_WWW_AUTH % (realm, nonce)
else:
nonce = md5.new("%d:%s:%.6f" %
(time.time(), realm, random.random())).hexdigest()
www_auth = STD_WWW_AUTH % (realm, nonce)
cherrypy.response.headers['www-authenticate'] = www_auth
# Record the new nonce
record_nonce(nonce, refresh, expires)
raise cherrypy.HTTPError(401,
"You are not authorized to access that resource")
cherrypy.tools.restful_auth = cherrypy.Tool('on_start_resource',
restful_auth)
Test Cases
""" Test cases for the restful_auth tool """
from cherrypy.test import test
test.prefer_parent_path()
import os
import time
import cherrypy
from cherrypy.lib import httpauth
import tools
def setup_server():
class Root:
def index(self):
return "This is public."
index.exposed = True
class Protected:
def index(self):
return "Hello %s, you've been authorized." % cherrypy.request.login
index.exposed = True
class Roles:
def index(self, *args, **kwargs):
if cherrypy.request.method == 'GET':
return "Hello %s, you've been authorized to GET." \
% cherrypy.request.login
elif cherrypy.request.method == 'POST':
return "Hello %s, you've been authorized to POST." \
% cherrypy.request.login
else:
raise cherrypy.HTTPError(405)
index.exposed = True
class Protected3:
def index(self):
return "Hello %s, you've been authorized." % cherrypy.request.login
index.exposed = True
class Protected4:
def index(self):
return "Hello %s, you've been authorized." % cherrypy.request.login
index.exposed = True
class Logout:
def index(self):
return "You are now logged out."
index.exposed = True
test_roles = {'test': ('test#', 'role1', 'role3'),
'postman': ('postman#', 'role2', 'role3')}
def fetch_user(user):
""" Return the user's password and login roles """
return test_roles.get(user)
conf = {'/': {'tools.restful_auth.on': False,
'tools.restful_auth.realm': 'localhost',
'tools.restful_auth.users': fetch_user,
'tools.restful_auth.refresh': 30,
'tools.restful_auth.expires': 60},
'/digest': { 'tools.restful_auth.on': True,},
'/refresh': { 'tools.restful_auth.on': True,
'tools.restful_auth.refresh': 1,
'tools.restful_auth.expires': 3},
'/onetime': { 'tools.restful_auth.on': True,
'tools.restful_auth.expires': 0},
'/logout': {'tools.restful_auth.on': True,
'tools.restful_auth.logout': True},
'/roles': {'tools.restful_auth.on': True,
'tools.restful_auth.roles': {'GET': ('role1', 'role3'),
'POST': ('role2',)}},
}
root = Root()
root.digest = Protected()
root.refresh = Protected()
root.onetime = Protected()
root.roles = Roles()
root.logout = Logout()
cherrypy.tree.mount(root, config=conf)
cherrypy.config.update({'environment': 'test_suite',
'log.error_file': os.path.join(os.path.dirname(__file__),
'cp_error_log'),
})
from cherrypy.test import helper
class RESTWebCase(helper.CPWebCase):
""" Add extra methods for our test cases """
def get_nonce(self, msg = None):
""" Extract the nonce from the www-authenticate request header """
www_auth = None
for k, v in self.headers:
if k.lower() == "www-authenticate":
www_auth = v
break
if www_auth is None:
self._handlewebError(
"Check nonce: www-authenticate header not found: %s" % msg)
return
nonce = None
www_auth = www_auth[7:]
items = www_auth.split(', ')
for item in items:
key, value = item.split('=')
if key.lower() == 'nonce':
nonce = value.strip('"')
break
if nonce is None:
self._handlewebError(
"Nonce was not found in www-authenticate header: %s" % msg)
return nonce
def calc_auth_hdr(self, nonce, path, user, password, method='GET'):
""" Return the authenticate header string """
base_auth = 'Digest username="%s", realm="localhost", nonce="%s", uri=%s, algorithm=MD5, response="%s", qop=auth, nc=%s, cnonce="1522e61005789929"'
auth = base_auth % (user, nonce, path, '', '00000001')
params = httpauth.parseAuthorization(auth)
response = httpauth._computeDigestResponse(params, password, method)
return base_auth % (user, nonce, path, response, '00000001')
def assertStale(self, msg = None):
""" Check that the stale flag is set in the www-authenticate header """
www_auth = None
for k, v in self.headers:
if k.lower() == "www-authenticate":
www_auth = v
break
if www_auth is None:
self._handlewebError(
"Checking stale flag on: : www-authenticate header not found", msg)
stale = None
www_auth = www_auth[7:]
items = www_auth.split(', ')
for item in items:
key, value = item.split('=')
if key.lower() == 'stale':
stale = value.strip('"')
break
if stale is None:
self._handlewebError(
"Stale flag not found in www-authenticate header", msg)
if stale != 'true':
self._handlewebError(
"Stale flag not set to 'true' in www-authenticate header")
return
def assertNotStale(self, msg = None):
""" Check that the stale flag is not set in the www-authenticate header """
www_auth = None
for k, v in self.headers:
if k.lower() == "www-authenticate":
www_auth = v
break
if www_auth is None:
self._handlewebError(
"Checking stale flag off: : www-authenticate header not found", msg)
stale = None
www_auth = www_auth[7:]
items = www_auth.split(', ')
for item in items:
key, value = item.split('=')
if key.lower() == 'stale':
stale = value.strip('"')
break
if stale:
self._handlewebError(
"Stale flag was found in www-authenticate header", msg)
return
class RESTAuthTest(RESTWebCase):
def testBasicRestfulDigest(self):
""" Basic restful auth test including test of digest auth """
path = "/digest/"
self.getPage(path)
self.assertStatus(401, 'Basic test first request')
value = None
for k, v in self.headers:
if k.lower() == "www-authenticate":
if v.startswith("Digest"):
value = v
break
if value is None:
self._handlewebError("Digest authentification scheme was not found")
value = value[7:]
items = value.split(', ')
tokens = {}
for item in items:
key, value = item.split('=')
tokens[key.lower()] = value
missing_msg = "%s is missing"
bad_value_msg = "'%s' was expecting '%s' but found '%s'"
nonce = None
if 'realm' not in tokens:
self._handlewebError(missing_msg % 'realm')
elif tokens['realm'] != '"localhost"':
self._handlewebError(bad_value_msg % ('realm', '"localhost"', tokens['realm']))
if 'nonce' not in tokens:
self._handlewebError(missing_msg % 'nonce')
else:
nonce = tokens['nonce'].strip('"')
if 'algorithm' not in tokens:
self._handlewebError(missing_msg % 'algorithm')
elif tokens['algorithm'] != '"MD5"':
self._handlewebError(bad_value_msg % ('algorithm', '"MD5"', tokens['algorithm']))
if 'qop' not in tokens:
self._handlewebError(missing_msg % 'qop')
elif tokens['qop'] != '"auth"':
self._handlewebError(bad_value_msg % ('qop', '"auth"', tokens['qop']))
auth = self.calc_auth_hdr(nonce, path, 'test', 'test#')
msg = 'Basic test after authentication'
self.getPage(path, [('Authorization', auth)])
self.assertStatus('200 OK', msg)
self.assertBody("Hello test, you've been authorized.", msg)
def testPublic(self):
""" Public access """
self.getPage("/")
msg = 'Public test'
self.assertStatus('200 OK', msg)
self.assertHeader('Content-Type', 'text/html', msg)
self.assertBody('This is public.', msg)
def testRefresh(self):
""" Test refreshing nonce """
path = "/refresh/"
self.getPage(path)
self.assertStatus(401, 'Refresh test first request')
nonce1 = self.get_nonce('Refresh test first nonce')
auth = self.calc_auth_hdr(nonce1, path, 'test', 'test#')
msg = 'Refresh test first authenticated request'
self.getPage(path, [('Authorization', auth)])
self.assertStatus('200 OK', msg)
self.assertBody("Hello test, you've been authorized.", msg)
# Wait two seconds, then check we get a new nonce with the stale flag
time.sleep(2)
self.getPage(path, [('Authorization', auth)])
msg = 'Refresh test - nonce requires refresh request'
self.assertStatus(401, msg)
self.assertStale(msg)
nonce2 = self.get_nonce('Refresh test second nonce')
self.assertNotEqual(nonce1, nonce2)
auth = self.calc_auth_hdr(nonce2, path, 'test', 'test#')
msg = 'Refresh test - refreshed nonce request'
self.getPage(path, [('Authorization', auth)])
self.assertStatus('200 OK', msg)
self.assertBody("Hello test, you've been authorized.", msg)
# Wait four seconds, then check we get a 401 without the stale flag
# and with a new nonce
time.sleep(4)
self.getPage(path, [('Authorization', auth)])
msg = 'Refresh test - expired nonce request'
self.assertStatus(401, msg)
self.assertNotStale(msg)
nonce3 = self.get_nonce('Refresh test third nonce')
self.assertNotEqual(nonce3, nonce2)
def testOneTime(self):
""" Test one-time nonce """
path = "/onetime/"
self.getPage(path)
self.assertStatus(401, 'One-time test first request')
nonce = self.get_nonce('One-time test nonce')
auth = self.calc_auth_hdr(nonce, path, 'test', 'test#')
self.getPage(path, [('Authorization', auth)])
msg = 'One-time test - THE request'
self.assertStatus('200 OK', msg)
self.assertBody("Hello test, you've been authorized.", msg)
# Now check the nonce has expired
self.getPage(path, [('Authorization', auth)])
self.assertStatus(401, 'One-time request on expired nonce')
def testLogout(self):
""" Test logout """
# First authenticate using the /digest path
path = "/digest/"
self.getPage(path)
self.assertStatus(401, 'Logout test first request')
nonce = self.get_nonce('Logout test nonce')
auth = self.calc_auth_hdr(nonce, path, 'test', 'test#')
self.getPage(path, [('Authorization', auth)])
msg = 'Logout test after authentication'
self.assertStatus('200 OK', msg)
self.assertBody("Hello test, you've been authorized.", msg)
# Now logout and check we get a 401 on a subsequent access to /digest
path = '/logout/'
msg = 'Logout test - request to Logout page'
self.getPage('/logout/', [('Authorization', auth)])
self.assertStatus('200 OK', msg)
self.assertBody("You are now logged out.", msg)
self.getPage('/digest/', [('Authorization', auth)])
self.assertStatus(401, 'Logout test - request after logout')
def testRoles(self):
""" Test login roles """
# GET a page as 'test'
path = "/roles/"
self.getPage(path)
self.assertStatus(401, "Role test first request as 'test'")
nonce = self.get_nonce("'Role test nonce for 'test'")
auth = self.calc_auth_hdr(nonce, path, 'test', 'test#')
msg = "Role test authorised GET as 'test'"
self.getPage(path, [('Authorization', auth)])
self.assertStatus('200 OK', msg)
self.assertBody("Hello test, you've been authorized to GET.", msg)
# POST some data as 'test'
msg = "Role test un-authorised POST as 'test'"
self.getPage(path, [('Authorization', auth)], 'POST', 'some data')
self.assertStatus(401, msg)
# Try again with the correct method in the response
nonce = self.get_nonce("'Role test POST nonce for 'test'")
auth = self.calc_auth_hdr(nonce, path, 'test', 'test#', 'POST')
msg = "Role test 'authorised' POST as 'test'"
self.getPage(path, [('Authorization', auth)], 'POST', 'some data')
self.assertStatus(401, msg)
# POST some data as 'postman'
auth = self.calc_auth_hdr(nonce, path, 'postman', 'postman#')
self.getPage(path, [('Authorization', auth)], 'POST', 'some data')
self.assertStatus(401, "Role test first POST as 'postman'")
nonce = self.get_nonce("Role test nonce for 'postman'")
auth = self.calc_auth_hdr(nonce, path, 'postman', 'postman#', 'POST')
self.getPage(path, [('Authorization', auth)], 'POST', 'some data')
msg = "Role test authorised POST as 'postman'"
self.assertStatus('200 OK', msg)
self.assertBody("Hello postman, you've been authorized to POST.", msg)
if __name__ == "__main__":
setup_server()
helper.testmain()

