Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4b0e1856b5 | ||
|
|
94841c90c3 | ||
|
|
f14e5f728c | ||
|
|
8443c17839 | ||
|
|
be8114127e | ||
|
|
4eaef303ff | ||
|
|
319a3fc1dc | ||
|
|
463d643e2c | ||
|
|
0cbed12952 | ||
|
|
b5994e57a4 | ||
|
|
e239b975be | ||
|
|
0259ea3d29 | ||
|
|
db4c008de3 | ||
|
|
7921e5148a | ||
|
|
ee23eab77a | ||
|
|
2344386f77 | ||
|
|
b5e12031c5 | ||
|
|
85e6840cf0 | ||
|
|
4c3006acb4 | ||
|
|
f8f008cab5 | ||
|
|
6bff8af458 | ||
|
|
b67edb083e | ||
|
|
4125a97ce1 | ||
|
|
78cf55bf9a | ||
|
|
931daaff89 | ||
|
|
c6550f0adc |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -59,3 +59,4 @@ HOWTO-web2py-devel
|
||||
*.sublime-workspace
|
||||
.idea/*
|
||||
site-packages/
|
||||
logs/
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
## 2.13.1
|
||||
## 2.13.1-2
|
||||
|
||||
- fixed a security issue in request_reset_password
|
||||
- added fabfile.py
|
||||
- fixed oauth2 renew token, thanks dokime7
|
||||
- fixed add_membership, del_membership, add_membership IntegrityError (when auth.enable_record_versioning)
|
||||
|
||||
2
Makefile
2
Makefile
@@ -32,7 +32,7 @@ update:
|
||||
echo "remember that pymysql was tweaked"
|
||||
src:
|
||||
### Use semantic versioning
|
||||
echo 'Version 2.13.1-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
|
||||
echo 'Version 2.13.3-stable+timestamp.'`date +%Y.%m.%d.%H.%M.%S` > VERSION
|
||||
### rm -f all junk files
|
||||
make clean
|
||||
### clean up baisc apps
|
||||
|
||||
2
VERSION
2
VERSION
@@ -1 +1 @@
|
||||
Version 2.12.3-stable+timestamp.2015.08.18.19.14.07
|
||||
Version 2.13.3-stable+timestamp.2015.12.24.08.08.22
|
||||
|
||||
@@ -576,7 +576,7 @@ def bg_graph_model():
|
||||
meta_graphmodel = dict(group=request.application, color='#ECECEC')
|
||||
|
||||
group = meta_graphmodel['group'].replace(' ', '')
|
||||
if not subgraphs.has_key(group):
|
||||
if group not in subgraphs:
|
||||
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
||||
subgraphs[group]['tables'].append(tablename)
|
||||
|
||||
|
||||
@@ -155,8 +155,8 @@
|
||||
{{=T.M("Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
||||
dict(hours=total['oldest'][0], min=total['oldest'][1], sec=total['oldest'][2]))}}
|
||||
</p>
|
||||
{{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle().toggleClass( "hidden" );')}}
|
||||
<div class="hidden" id="all_keys">
|
||||
{{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle().toggleClass( "w2p_hidden" );')}}
|
||||
<div class="w2p_hidden" id="all_keys">
|
||||
{{=total['keys']}}
|
||||
</div>
|
||||
<br />
|
||||
@@ -183,8 +183,8 @@
|
||||
{{=T.M("RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
||||
dict(hours=ram['oldest'][0], min=ram['oldest'][1], sec=ram['oldest'][2]))}}
|
||||
</p>
|
||||
{{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle().toggleClass( "hidden" );')}}
|
||||
<div class="hidden" id="ram_keys">
|
||||
{{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle().toggleClass( "w2p_hidden" );')}}
|
||||
<div class="w2p_hidden" id="ram_keys">
|
||||
{{=ram['keys']}}
|
||||
</div>
|
||||
<br />
|
||||
@@ -212,8 +212,8 @@
|
||||
{{=T.M("DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
||||
dict(hours=disk['oldest'][0], min=disk['oldest'][1], sec=disk['oldest'][2]))}}
|
||||
</p>
|
||||
{{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle().toggleClass( "hidden" );')}}
|
||||
<div class="hidden" id="disk_keys">
|
||||
{{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle().toggleClass( "w2p_hidden" );')}}
|
||||
<div class="w2p_hidden" id="disk_keys">
|
||||
{{=disk['keys']}}
|
||||
</div>
|
||||
<br />
|
||||
@@ -249,8 +249,8 @@
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li>
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li>
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li>
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
@@ -207,7 +207,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi
|
||||
{{=peekfile('views',c, dict(id=id))}}
|
||||
</span>
|
||||
<span class="extras celled celled-one">
|
||||
{{if extend.has_key(c):}}{{=T("extends")}} <b>{{=extend[c]}}</b> {{pass}}
|
||||
{{if c in extend:}}{{=T("extends")}} <b>{{=extend[c]}}</b> {{pass}}
|
||||
{{if include[c]:}}{{=T("includes")}} {{pass}}{{=XML(', '.join([B(f).xml() for f in include[c]]))}}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -144,7 +144,7 @@ for c in controllers: controller_functions+=[c[:-3]+'/%s.html'%x for x in functi
|
||||
{{=peekfile('views',c)}}
|
||||
</span>
|
||||
<span class="extras celled">
|
||||
{{if extend.has_key(c):}}{{=T("extends")}} <b>{{=extend[c]}}</b> {{pass}}
|
||||
{{if c in extend:}}{{=T("extends")}} <b>{{=extend[c]}}</b> {{pass}}
|
||||
{{if include[c]:}}{{=T("includes")}} {{pass}}{{=XML(', '.join([B(f).xml() for f in include[c]]))}}
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -576,7 +576,7 @@ def bg_graph_model():
|
||||
meta_graphmodel = dict(group=request.application, color='#ECECEC')
|
||||
|
||||
group = meta_graphmodel['group'].replace(' ', '')
|
||||
if not subgraphs.has_key(group):
|
||||
if group not in subgraphs:
|
||||
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
||||
subgraphs[group]['tables'].append(tablename)
|
||||
|
||||
|
||||
@@ -155,8 +155,8 @@
|
||||
{{=T.M("Cache contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
||||
dict(hours=total['oldest'][0], min=total['oldest'][1], sec=total['oldest'][2]))}}
|
||||
</p>
|
||||
{{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle().toggleClass( "hidden" );')}}
|
||||
<div class="hidden" id="all_keys">
|
||||
{{=BUTTON(T('Cache Keys'), _onclick='jQuery("#all_keys").toggle().toggleClass( "w2p_hidden" );')}}
|
||||
<div class="w2p_hidden" id="all_keys">
|
||||
{{=total['keys']}}
|
||||
</div>
|
||||
<br />
|
||||
@@ -183,8 +183,8 @@
|
||||
{{=T.M("RAM contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
||||
dict(hours=ram['oldest'][0], min=ram['oldest'][1], sec=ram['oldest'][2]))}}
|
||||
</p>
|
||||
{{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle().toggleClass( "hidden" );')}}
|
||||
<div class="hidden" id="ram_keys">
|
||||
{{=BUTTON(T('RAM Cache Keys'), _onclick='jQuery("#ram_keys").toggle().toggleClass( "w2p_hidden" );')}}
|
||||
<div class="w2p_hidden" id="ram_keys">
|
||||
{{=ram['keys']}}
|
||||
</div>
|
||||
<br />
|
||||
@@ -212,8 +212,8 @@
|
||||
{{=T.M("DISK contains items up to **%(hours)02d** %%{hour(hours)} **%(min)02d** %%{minute(min)} **%(sec)02d** %%{second(sec)} old.",
|
||||
dict(hours=disk['oldest'][0], min=disk['oldest'][1], sec=disk['oldest'][2]))}}
|
||||
</p>
|
||||
{{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle().toggleClass( "hidden" );')}}
|
||||
<div class="hidden" id="disk_keys">
|
||||
{{=BUTTON(T('Disk Cache Keys'), _onclick='jQuery("#disk_keys").toggle().toggleClass( "w2p_hidden" );')}}
|
||||
<div class="w2p_hidden" id="disk_keys">
|
||||
{{=disk['keys']}}
|
||||
</div>
|
||||
<br />
|
||||
@@ -249,8 +249,8 @@
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['png'])}}">png</a></li>
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['svg'])}}">svg</a></li>
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['pdf'])}}">pdf</a></li>
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['ps'])}}">ps</a></li>
|
||||
<li><a href="{{=URL('appadmin', 'bg_graph_model', args=['dot'])}}">dot</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
<br />
|
||||
|
||||
@@ -576,7 +576,7 @@ def bg_graph_model():
|
||||
meta_graphmodel = dict(group=request.application, color='#ECECEC')
|
||||
|
||||
group = meta_graphmodel['group'].replace(' ', '')
|
||||
if not subgraphs.has_key(group):
|
||||
if group not in subgraphs:
|
||||
subgraphs[group] = dict(meta=meta_graphmodel, tables=[])
|
||||
subgraphs[group]['tables'].append(tablename)
|
||||
|
||||
|
||||
@@ -476,11 +476,11 @@ def compile_views(folder, skip_failed_views=False):
|
||||
data = parse_template(fname, path)
|
||||
except Exception, e:
|
||||
if skip_failed_views:
|
||||
failed_views.append(file)
|
||||
failed_views.append(fname)
|
||||
else:
|
||||
raise Exception("%s in %s" % (e, file))
|
||||
raise Exception("%s in %s" % (e, fname))
|
||||
else:
|
||||
filename = ('views/%s.py' % file).replace('/', '_').replace('\\', '_')
|
||||
filename = ('views/%s.py' % fname).replace('/', '_').replace('\\', '_')
|
||||
filename = pjoin(folder, 'compiled', filename)
|
||||
write_file(filename, data)
|
||||
save_pyc(filename)
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import uuid
|
||||
import time
|
||||
from gluon.serializers import json_parser
|
||||
import base64
|
||||
import hmac
|
||||
import hashlib
|
||||
from gluon.storage import Storage
|
||||
from gluon.utils import web2py_uuid
|
||||
from gluon import current
|
||||
from gluon.http import HTTP
|
||||
|
||||
|
||||
class Web2pyJwt(object):
|
||||
|
||||
"""
|
||||
If left externally, this needs the usual "singleton" approach.
|
||||
Given I (we) don't know if to include in auth yet, let's stick to basics.
|
||||
|
||||
Args:
|
||||
- secret_key: the secret. Without salting, an attacker knowing this can impersonate
|
||||
any user
|
||||
- algorithm : uses as they are in the JWT specs, HS256, HS384 or HS512 basically means
|
||||
signing with HMAC with a 256, 284 or 512bit hash
|
||||
- verify_expiration : verifies the expiration checking the exp claim
|
||||
- leeway: allow n seconds of skew when checking for token expiration
|
||||
- expiration : how many seconds a token may be valid
|
||||
- allow_refresh: enable the machinery to get a refreshed token passing a not-already-expired
|
||||
token
|
||||
- refresh_expiration_delta: to avoid continous refresh of the token
|
||||
- header_prefix : self-explanatory. "JWT" and "Bearer" seems to be the emerging standards
|
||||
- jwt_add_header: a dict holding additional mappings to the header. by default only alg and typ are filled
|
||||
- user_param: the name of the parameter holding the username when requesting a token. Can be useful, e.g, for
|
||||
email-based authentication, with "email" as a parameter
|
||||
- pass_param: same as above, but for the password
|
||||
- realm: self-explanatory
|
||||
- salt: can be static or a function that takes the payload as an argument.
|
||||
Example:
|
||||
def mysalt(payload):
|
||||
return payload['hmac_key'].split('-')[0]
|
||||
- additional_payload: can be a dict to merge with the payload or a function that takes
|
||||
the payload as input and returns the modified payload
|
||||
Example:
|
||||
def myadditional_payload(payload):
|
||||
payload['my_name_is'] = 'bond,james bond'
|
||||
return payload
|
||||
- before_authorization: can be a callable that takes the deserialized token (a dict) as input.
|
||||
Gets called right after signature verification but before the actual
|
||||
authorization takes place. You can raise with HTTP a proper error message
|
||||
Example:
|
||||
def mybefore_authorization(tokend):
|
||||
if not tokend['my_name_is'] == 'bond,james bond':
|
||||
raise HTTP(400, u'Invalid JWT my_name_is claim')
|
||||
- max_header_length: check max length to avoid load()ing unusually large tokens (could mean crafted, e.g. in a DDoS.)
|
||||
|
||||
Basic Usage:
|
||||
in models (or the controller needing it)
|
||||
|
||||
myjwt = Web2pyJwt('secret', auth)
|
||||
|
||||
in the controller issuing tokens
|
||||
|
||||
def login_and_take_token():
|
||||
return myjwt.jwt_token_manager()
|
||||
|
||||
A call then to /app/controller/login_and_take_token/auth with username and password returns the token
|
||||
A call to /app/controller/login_and_take_token/refresh with the original token returns the refreshed token
|
||||
|
||||
To protect a function with JWT
|
||||
|
||||
@myjwt.requires_jwt()
|
||||
@auth.requires_login()
|
||||
def protected():
|
||||
return '%s$%s' % (request.now, auth.user_id)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, secret_key,
|
||||
auth,
|
||||
algorithm='HS256',
|
||||
verify_expiration=True,
|
||||
leeway=30,
|
||||
expiration=60 * 5,
|
||||
allow_refresh=True,
|
||||
refresh_expiration_delta=60 * 60,
|
||||
header_prefix='Bearer',
|
||||
jwt_add_header=None,
|
||||
user_param='username',
|
||||
pass_param='password',
|
||||
realm='Login required',
|
||||
salt=None,
|
||||
additional_payload=None,
|
||||
before_authorization=None,
|
||||
max_header_length=4*1024,
|
||||
):
|
||||
self.secret_key = secret_key
|
||||
self.auth = auth
|
||||
self.algorithm = algorithm
|
||||
if self.algorithm not in ('HS256', 'HS384', 'HS512'):
|
||||
raise NotImplementedError('Algoritm %s not allowed' % algorithm)
|
||||
self.verify_expiration = verify_expiration
|
||||
self.leeway = leeway
|
||||
self.expiration = expiration
|
||||
self.allow_refresh = allow_refresh
|
||||
self.refresh_expiration_delta = refresh_expiration_delta
|
||||
self.header_prefix = header_prefix
|
||||
self.jwt_add_header = jwt_add_header or {}
|
||||
base_header = {'alg': self.algorithm, 'typ': 'JWT'}
|
||||
for k, v in self.jwt_add_header.iteritems():
|
||||
base_header[k] = v
|
||||
self.cached_b64h = self.jwt_b64e(json_parser.dumps(base_header))
|
||||
digestmod_mapping = {
|
||||
'HS256': hashlib.sha256,
|
||||
'HS384': hashlib.sha384,
|
||||
'HS512': hashlib.sha512
|
||||
}
|
||||
self.digestmod = digestmod_mapping[algorithm]
|
||||
self.user_param = user_param
|
||||
self.pass_param = pass_param
|
||||
self.realm = realm
|
||||
self.salt = salt
|
||||
self.additional_payload = additional_payload
|
||||
self.before_authorization = before_authorization
|
||||
self.max_header_length = max_header_length
|
||||
print 'initialized'
|
||||
|
||||
@staticmethod
|
||||
def jwt_b64e(string):
|
||||
if isinstance(string, unicode):
|
||||
string = string.encode('uft-8', 'strict')
|
||||
return base64.urlsafe_b64encode(string).strip(b'=')
|
||||
|
||||
@staticmethod
|
||||
def jwt_b64d(string):
|
||||
"""base64 decodes a single bytestring (and is tolerant to getting
|
||||
called with a unicode string).
|
||||
The result is also a bytestring.
|
||||
"""
|
||||
if isinstance(string, unicode):
|
||||
string = string.encode('ascii', 'ignore')
|
||||
return base64.urlsafe_b64decode(string + '=' * (-len(string) % 4))
|
||||
|
||||
def generate_token(self, payload):
|
||||
secret = self.secret_key
|
||||
if self.salt:
|
||||
if callable(self.salt):
|
||||
secret = "%s$%s" % (secret, self.salt(payload))
|
||||
else:
|
||||
secret = "%s$%s" % (secret, self.salt)
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode('ascii', 'ignore')
|
||||
b64h = self.cached_b64h
|
||||
b64p = self.jwt_b64e(json_parser.dumps(payload))
|
||||
jbody = b64h + '.' + b64p
|
||||
mauth = hmac.new(key=secret, msg=jbody, digestmod=self.digestmod)
|
||||
jsign = self.jwt_b64e(mauth.digest())
|
||||
return jbody + '.' + jsign
|
||||
|
||||
def verify_signature(self, body, signature, secret):
|
||||
mauth = hmac.new(key=secret, msg=body, digestmod=self.digestmod)
|
||||
return hmac.compare_digest(self.jwt_b64e(mauth.digest()), signature)
|
||||
|
||||
def load_token(self, token):
|
||||
if isinstance(token, unicode):
|
||||
token = token.encode('utf-8', 'strict')
|
||||
body, sig = token.rsplit('.', 1)
|
||||
b64h, b64b = body.split('.', 1)
|
||||
if b64h != self.cached_b64h:
|
||||
# header not the same
|
||||
raise HTTP(400, u'Invalid JWT Header')
|
||||
secret = self.secret_key
|
||||
tokend = json_parser.loads(self.jwt_b64d(b64b))
|
||||
if self.salt:
|
||||
if callable(self.salt):
|
||||
secret = "%s$%s" % (secret, self.salt(tokend))
|
||||
else:
|
||||
secret = "%s$%s" % (secret, self.salt)
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode('ascii', 'ignore')
|
||||
if not self.verify_signature(body, sig, secret):
|
||||
# signature verification failed
|
||||
raise HTTP(400, u'Token signature is invalid')
|
||||
if self.verify_expiration:
|
||||
now = time.mktime(datetime.datetime.utcnow().timetuple())
|
||||
if tokend['exp'] + self.leeway < now:
|
||||
raise HTTP(400, u'Token is expired')
|
||||
if callable(self.before_authorization):
|
||||
self.before_authorization(tokend)
|
||||
return tokend
|
||||
|
||||
def serialize_auth_session(self, session_auth):
|
||||
"""
|
||||
As bad as it sounds, as long as this is rarely used (vs using the token)
|
||||
this is the faster method, even if we ditch session in jwt_token_manager().
|
||||
We (mis)use the heavy default auth mechanism to avoid any further computation,
|
||||
while sticking to a somewhat-stable Auth API.
|
||||
"""
|
||||
now = time.mktime(datetime.datetime.utcnow().timetuple())
|
||||
expires = now + self.expiration
|
||||
payload = dict(
|
||||
hmac_key=session_auth['hmac_key'],
|
||||
user_groups=session_auth['user_groups'],
|
||||
user=session_auth['user'].as_dict(),
|
||||
iat=now,
|
||||
exp=expires
|
||||
)
|
||||
return payload
|
||||
|
||||
def refresh_token(self, orig_payload):
|
||||
now = time.mktime(datetime.datetime.utcnow().timetuple())
|
||||
if self.verify_expiration:
|
||||
orig_exp = orig_payload['exp']
|
||||
if orig_exp + self.leeway < now:
|
||||
# token already expired, can't be used for refresh
|
||||
raise HTTP(400, u'Token already expired')
|
||||
orig_iat = orig_payload.get('orig_iat') or orig_payload['iat']
|
||||
if orig_iat + self.refresh_expiration_delta < now:
|
||||
# refreshed too long ago
|
||||
raise HTTP(400, u'Token issued too long ago')
|
||||
expires = now + self.refresh_expiration_delta
|
||||
orig_payload.update(
|
||||
orig_iat=orig_iat,
|
||||
iat=now,
|
||||
exp=expires,
|
||||
hmac_key=web2py_uuid()
|
||||
)
|
||||
self.alter_payload(orig_payload)
|
||||
return orig_payload
|
||||
|
||||
def alter_payload(self, payload):
|
||||
if self.additional_payload:
|
||||
if callable(self.additional_payload):
|
||||
payload = self.additional_payload(payload)
|
||||
elif isinstance(self.additional_payload, dict):
|
||||
payload.update(self.additional_payload)
|
||||
return payload
|
||||
|
||||
def jwt_token_manager(self):
|
||||
"""
|
||||
The part that issues (and refreshes) tokens.
|
||||
Used in a controller, given myjwt is the istantiated class, as
|
||||
|
||||
def api_auth():
|
||||
return myjwt.jwt_token_manager()
|
||||
|
||||
Then, a call to /app/c/api_auth/auth with username and password
|
||||
returns a token, while /app/c/api_auth/refresh with the current token
|
||||
issues another token
|
||||
"""
|
||||
request = current.request
|
||||
# forget and unlock response
|
||||
if request.args(0) == 'auth':
|
||||
current.session.forget(current.response)
|
||||
username = request.vars[self.user_param]
|
||||
password = request.vars[self.pass_param]
|
||||
valid_user = self.auth.login_bare(username, password)
|
||||
if valid_user:
|
||||
payload = self.serialize_auth_session(current.session.auth)
|
||||
self.alter_payload(payload)
|
||||
return self.generate_token(payload)
|
||||
else:
|
||||
raise HTTP(
|
||||
401, u'Not Authorized',
|
||||
**{'WWW-Authenticate': u'JWT realm="%s"' % self.realm})
|
||||
elif request.args(0) == 'refresh':
|
||||
if not self.allow_refresh:
|
||||
raise HTTP(403, u'Refreshing token is not allowed')
|
||||
token = request.vars.token
|
||||
tokend = self.load_token(token)
|
||||
# verification can fail here
|
||||
refreshed = self.refresh_token(tokend)
|
||||
return self.generate_token(refreshed)
|
||||
|
||||
def inject_token(self, tokend):
|
||||
"""
|
||||
The real deal, not touching the db but still logging-in the user
|
||||
"""
|
||||
self.auth.user = Storage(tokend['user'])
|
||||
self.auth.user_groups = tokend['user_groups']
|
||||
self.auth.hmac_key = tokend['hmac_key']
|
||||
|
||||
def requires_jwt(self, otherwise=None):
|
||||
"""
|
||||
The validator that checks for the header or the
|
||||
_token var
|
||||
"""
|
||||
request = current.request
|
||||
token_in_header = request.env.http_authorization
|
||||
if token_in_header:
|
||||
parts = token_in_header.split()
|
||||
if parts[0].lower() != self.header_prefix.lower():
|
||||
raise HTTP(400, u'Invalid JWT header')
|
||||
elif len(parts) == 1:
|
||||
raise HTTP(400, u'Invalid JWT header, missing token')
|
||||
elif len(parts) > 2:
|
||||
raise HTTP(400, 'Invalid JWT header, token contains spaces')
|
||||
token = parts[1]
|
||||
else:
|
||||
token = request.vars._token
|
||||
if token and len(token) < self.max_header_length:
|
||||
tokend = self.load_token(token)
|
||||
self.inject_token(tokend)
|
||||
return self.auth.requires(True, otherwise=otherwise)
|
||||
@@ -2646,7 +2646,7 @@ def test():
|
||||
>>> form=FORM(INPUT(value="Hello World", _name="var", requires=IS_MATCH('^\w+$')))
|
||||
>>> isinstance(form.as_dict(), dict)
|
||||
True
|
||||
>>> form.as_dict(flat=True).has_key("vars")
|
||||
>>> "vars" in form.as_dict(flat=True)
|
||||
True
|
||||
>>> isinstance(form.as_json(), basestring) and len(form.as_json(sanitize=False)) > 0
|
||||
True
|
||||
|
||||
Submodule gluon/packages/dal updated: 1d8ac4f562...dcfb5f58aa
@@ -1679,9 +1679,6 @@ class SQLFORM(FORM):
|
||||
elif field.type == 'double':
|
||||
if value is not None:
|
||||
fields[fieldname] = safe_float(value)
|
||||
elif field.type in ('string', 'text'):
|
||||
if fieldname in self.request_vars:
|
||||
fields[fieldname] = self.request_vars[fieldname]
|
||||
|
||||
for fieldname in self.vars:
|
||||
if fieldname != 'id' and fieldname in self.table.fields\
|
||||
|
||||
385
gluon/tools.py
385
gluon/tools.py
@@ -32,8 +32,12 @@ import cStringIO
|
||||
import ConfigParser
|
||||
import email.utils
|
||||
import random
|
||||
import hmac
|
||||
import hashlib
|
||||
import json
|
||||
from email import MIMEBase, MIMEMultipart, MIMEText, Encoders, Header, message_from_string, Charset
|
||||
|
||||
from gluon.serializers import json_parser
|
||||
from gluon.contenttype import contenttype
|
||||
from gluon.storage import Storage, StorageList, Settings, Messages
|
||||
from gluon.utils import web2py_uuid
|
||||
@@ -1136,6 +1140,304 @@ def addrow(form, a, b, c, style, _id, position=-1):
|
||||
TD(c, _class='w2p_fc'), _id=_id))
|
||||
|
||||
|
||||
class AuthJWT(object):
|
||||
|
||||
"""
|
||||
If left externally, this needs the usual "singleton" approach.
|
||||
Given I (we) don't know if to include in auth yet, let's stick to basics.
|
||||
|
||||
Args:
|
||||
- secret_key: the secret. Without salting, an attacker knowing this can impersonate
|
||||
any user
|
||||
- algorithm : uses as they are in the JWT specs, HS256, HS384 or HS512 basically means
|
||||
signing with HMAC with a 256, 284 or 512bit hash
|
||||
- verify_expiration : verifies the expiration checking the exp claim
|
||||
- leeway: allow n seconds of skew when checking for token expiration
|
||||
- expiration : how many seconds a token may be valid
|
||||
- allow_refresh: enable the machinery to get a refreshed token passing a not-already-expired
|
||||
token
|
||||
- refresh_expiration_delta: to avoid continous refresh of the token
|
||||
- header_prefix : self-explanatory. "JWT" and "Bearer" seems to be the emerging standards
|
||||
- jwt_add_header: a dict holding additional mappings to the header. by default only alg and typ are filled
|
||||
- user_param: the name of the parameter holding the username when requesting a token. Can be useful, e.g, for
|
||||
email-based authentication, with "email" as a parameter
|
||||
- pass_param: same as above, but for the password
|
||||
- realm: self-explanatory
|
||||
- salt: can be static or a function that takes the payload as an argument.
|
||||
Example:
|
||||
def mysalt(payload):
|
||||
return payload['hmac_key'].split('-')[0]
|
||||
- additional_payload: can be a dict to merge with the payload or a function that takes
|
||||
the payload as input and returns the modified payload
|
||||
Example:
|
||||
def myadditional_payload(payload):
|
||||
payload['my_name_is'] = 'bond,james bond'
|
||||
return payload
|
||||
- before_authorization: can be a callable that takes the deserialized token (a dict) as input.
|
||||
Gets called right after signature verification but before the actual
|
||||
authorization takes place. You can raise with HTTP a proper error message
|
||||
Example:
|
||||
def mybefore_authorization(tokend):
|
||||
if not tokend['my_name_is'] == 'bond,james bond':
|
||||
raise HTTP(400, u'Invalid JWT my_name_is claim')
|
||||
- max_header_length: check max length to avoid load()ing unusually large tokens (could mean crafted, e.g. in a DDoS.)
|
||||
|
||||
Basic Usage:
|
||||
in models (or the controller needing it)
|
||||
|
||||
myjwt = AuthJWT(auth, secret_key='secret')
|
||||
|
||||
in the controller issuing tokens
|
||||
|
||||
def login_and_take_token():
|
||||
return myjwt.jwt_token_manager()
|
||||
|
||||
A call then to /app/controller/login_and_take_token/auth with username and password returns the token
|
||||
A call to /app/controller/login_and_take_token/refresh with the original token returns the refreshed token
|
||||
|
||||
To protect a function with JWT
|
||||
|
||||
@myjwt.allows_jwt()
|
||||
@auth.requires_login()
|
||||
def protected():
|
||||
return '%s$%s' % (request.now, auth.user_id)
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self,
|
||||
auth,
|
||||
secret_key,
|
||||
algorithm='HS256',
|
||||
verify_expiration=True,
|
||||
leeway=30,
|
||||
expiration=60 * 5,
|
||||
allow_refresh=True,
|
||||
refresh_expiration_delta=60 * 60,
|
||||
header_prefix='Bearer',
|
||||
jwt_add_header=None,
|
||||
user_param='username',
|
||||
pass_param='password',
|
||||
realm='Login required',
|
||||
salt=None,
|
||||
additional_payload=None,
|
||||
before_authorization=None,
|
||||
max_header_length=4*1024,
|
||||
):
|
||||
self.secret_key = secret_key
|
||||
self.auth = auth
|
||||
self.algorithm = algorithm
|
||||
if self.algorithm not in ('HS256', 'HS384', 'HS512'):
|
||||
raise NotImplementedError('Algoritm %s not allowed' % algorithm)
|
||||
self.verify_expiration = verify_expiration
|
||||
self.leeway = leeway
|
||||
self.expiration = expiration
|
||||
self.allow_refresh = allow_refresh
|
||||
self.refresh_expiration_delta = refresh_expiration_delta
|
||||
self.header_prefix = header_prefix
|
||||
self.jwt_add_header = jwt_add_header or {}
|
||||
base_header = {'alg': self.algorithm, 'typ': 'JWT'}
|
||||
for k, v in self.jwt_add_header.iteritems():
|
||||
base_header[k] = v
|
||||
self.cached_b64h = self.jwt_b64e(json_parser.dumps(base_header))
|
||||
digestmod_mapping = {
|
||||
'HS256': hashlib.sha256,
|
||||
'HS384': hashlib.sha384,
|
||||
'HS512': hashlib.sha512
|
||||
}
|
||||
self.digestmod = digestmod_mapping[algorithm]
|
||||
self.user_param = user_param
|
||||
self.pass_param = pass_param
|
||||
self.realm = realm
|
||||
self.salt = salt
|
||||
self.additional_payload = additional_payload
|
||||
self.before_authorization = before_authorization
|
||||
self.max_header_length = max_header_length
|
||||
|
||||
@staticmethod
|
||||
def jwt_b64e(string):
|
||||
if isinstance(string, unicode):
|
||||
string = string.encode('uft-8', 'strict')
|
||||
return base64.urlsafe_b64encode(string).strip(b'=')
|
||||
|
||||
@staticmethod
|
||||
def jwt_b64d(string):
|
||||
"""base64 decodes a single bytestring (and is tolerant to getting
|
||||
called with a unicode string).
|
||||
The result is also a bytestring.
|
||||
"""
|
||||
if isinstance(string, unicode):
|
||||
string = string.encode('ascii', 'ignore')
|
||||
return base64.urlsafe_b64decode(string + '=' * (-len(string) % 4))
|
||||
|
||||
def generate_token(self, payload):
|
||||
secret = self.secret_key
|
||||
if self.salt:
|
||||
if callable(self.salt):
|
||||
secret = "%s$%s" % (secret, self.salt(payload))
|
||||
else:
|
||||
secret = "%s$%s" % (secret, self.salt)
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode('ascii', 'ignore')
|
||||
b64h = self.cached_b64h
|
||||
b64p = self.jwt_b64e(json_parser.dumps(payload))
|
||||
jbody = b64h + '.' + b64p
|
||||
mauth = hmac.new(key=secret, msg=jbody, digestmod=self.digestmod)
|
||||
jsign = self.jwt_b64e(mauth.digest())
|
||||
return jbody + '.' + jsign
|
||||
|
||||
def verify_signature(self, body, signature, secret):
|
||||
mauth = hmac.new(key=secret, msg=body, digestmod=self.digestmod)
|
||||
return hmac.compare_digest(self.jwt_b64e(mauth.digest()), signature)
|
||||
|
||||
def load_token(self, token):
|
||||
if isinstance(token, unicode):
|
||||
token = token.encode('utf-8', 'strict')
|
||||
body, sig = token.rsplit('.', 1)
|
||||
b64h, b64b = body.split('.', 1)
|
||||
if b64h != self.cached_b64h:
|
||||
# header not the same
|
||||
raise HTTP(400, u'Invalid JWT Header')
|
||||
secret = self.secret_key
|
||||
tokend = json_parser.loads(self.jwt_b64d(b64b))
|
||||
if self.salt:
|
||||
if callable(self.salt):
|
||||
secret = "%s$%s" % (secret, self.salt(tokend))
|
||||
else:
|
||||
secret = "%s$%s" % (secret, self.salt)
|
||||
if isinstance(secret, unicode):
|
||||
secret = secret.encode('ascii', 'ignore')
|
||||
if not self.verify_signature(body, sig, secret):
|
||||
# signature verification failed
|
||||
raise HTTP(400, u'Token signature is invalid')
|
||||
if self.verify_expiration:
|
||||
now = time.mktime(datetime.datetime.utcnow().timetuple())
|
||||
if tokend['exp'] + self.leeway < now:
|
||||
raise HTTP(400, u'Token is expired')
|
||||
if callable(self.before_authorization):
|
||||
self.before_authorization(tokend)
|
||||
return tokend
|
||||
|
||||
def serialize_auth_session(self, session_auth):
|
||||
"""
|
||||
As bad as it sounds, as long as this is rarely used (vs using the token)
|
||||
this is the faster method, even if we ditch session in jwt_token_manager().
|
||||
We (mis)use the heavy default auth mechanism to avoid any further computation,
|
||||
while sticking to a somewhat-stable Auth API.
|
||||
"""
|
||||
now = time.mktime(datetime.datetime.utcnow().timetuple())
|
||||
expires = now + self.expiration
|
||||
payload = dict(
|
||||
hmac_key=session_auth['hmac_key'],
|
||||
user_groups=session_auth['user_groups'],
|
||||
user=session_auth['user'].as_dict(),
|
||||
iat=now,
|
||||
exp=expires
|
||||
)
|
||||
return payload
|
||||
|
||||
def refresh_token(self, orig_payload):
|
||||
now = time.mktime(datetime.datetime.utcnow().timetuple())
|
||||
if self.verify_expiration:
|
||||
orig_exp = orig_payload['exp']
|
||||
if orig_exp + self.leeway < now:
|
||||
# token already expired, can't be used for refresh
|
||||
raise HTTP(400, u'Token already expired')
|
||||
orig_iat = orig_payload.get('orig_iat') or orig_payload['iat']
|
||||
if orig_iat + self.refresh_expiration_delta < now:
|
||||
# refreshed too long ago
|
||||
raise HTTP(400, u'Token issued too long ago')
|
||||
expires = now + self.refresh_expiration_delta
|
||||
orig_payload.update(
|
||||
orig_iat=orig_iat,
|
||||
iat=now,
|
||||
exp=expires,
|
||||
hmac_key=web2py_uuid()
|
||||
)
|
||||
self.alter_payload(orig_payload)
|
||||
return orig_payload
|
||||
|
||||
def alter_payload(self, payload):
|
||||
if self.additional_payload:
|
||||
if callable(self.additional_payload):
|
||||
payload = self.additional_payload(payload)
|
||||
elif isinstance(self.additional_payload, dict):
|
||||
payload.update(self.additional_payload)
|
||||
return payload
|
||||
|
||||
def jwt_token_manager(self):
|
||||
"""
|
||||
The part that issues (and refreshes) tokens.
|
||||
Used in a controller, given myjwt is the istantiated class, as
|
||||
|
||||
def api_auth():
|
||||
return myjwt.jwt_token_manager()
|
||||
|
||||
Then, a call to /app/c/api_auth/auth with username and password
|
||||
returns a token, while /app/c/api_auth/refresh with the current token
|
||||
issues another token
|
||||
"""
|
||||
request = current.request
|
||||
response = current.response
|
||||
session = current.session
|
||||
# forget and unlock response
|
||||
if request.vars.token:
|
||||
if not self.allow_refresh:
|
||||
raise HTTP(403, u'Refreshing token is not allowed')
|
||||
token = request.vars.token
|
||||
tokend = self.load_token(token)
|
||||
# verification can fail here
|
||||
refreshed = self.refresh_token(tokend)
|
||||
ret = {'token':self.generate_token(refreshed)}
|
||||
elif self.user_param in request.vars and self.pass_param in request.vars:
|
||||
session.forget(response)
|
||||
username = request.vars[self.user_param]
|
||||
password = request.vars[self.pass_param]
|
||||
valid_user = self.auth.login_bare(username, password)
|
||||
if valid_user:
|
||||
payload = self.serialize_auth_session(current.session.auth)
|
||||
self.alter_payload(payload)
|
||||
ret = {'token':self.generate_token(payload)}
|
||||
else:
|
||||
raise HTTP(
|
||||
401, u'Not Authorized',
|
||||
**{'WWW-Authenticate': u'JWT realm="%s"' % self.realm})
|
||||
else:
|
||||
raise HTTP(400, u'Must pass token for refresh or username and password for login')
|
||||
response.headers['content-type'] = 'application/json'
|
||||
return json.dumps(ret)
|
||||
|
||||
def inject_token(self, tokend):
|
||||
"""
|
||||
The real deal, not touching the db but still logging-in the user
|
||||
"""
|
||||
self.auth.user = Storage(tokend['user'])
|
||||
self.auth.user_groups = tokend['user_groups']
|
||||
self.auth.hmac_key = tokend['hmac_key']
|
||||
|
||||
def allows_jwt(self, otherwise=None):
|
||||
"""
|
||||
The validator that checks for the header or the
|
||||
_token var
|
||||
"""
|
||||
request = current.request
|
||||
token_in_header = request.env.http_authorization
|
||||
if token_in_header:
|
||||
parts = token_in_header.split()
|
||||
if parts[0].lower() != self.header_prefix.lower():
|
||||
raise HTTP(400, u'Invalid JWT header')
|
||||
elif len(parts) == 1:
|
||||
raise HTTP(400, u'Invalid JWT header, missing token')
|
||||
elif len(parts) > 2:
|
||||
raise HTTP(400, 'Invalid JWT header, token contains spaces')
|
||||
token = parts[1]
|
||||
else:
|
||||
token = request.vars._token
|
||||
if token and len(token) < self.max_header_length:
|
||||
tokend = self.load_token(token)
|
||||
self.inject_token(tokend)
|
||||
return self.auth.requires(True, otherwise=otherwise)
|
||||
|
||||
|
||||
class Auth(object):
|
||||
|
||||
default_settings = dict(
|
||||
@@ -1424,7 +1726,7 @@ class Auth(object):
|
||||
hmac_key=None, controller='default', function='user',
|
||||
cas_provider=None, signature=True, secure=False,
|
||||
csrf_prevention=True, propagate_extension=None,
|
||||
url_index=None):
|
||||
url_index=None, jwt=None):
|
||||
|
||||
## next two lines for backward compatibility
|
||||
if not db and environment and isinstance(environment, DAL):
|
||||
@@ -1544,6 +1846,8 @@ class Auth(object):
|
||||
self.define_signature()
|
||||
else:
|
||||
self.signature = None
|
||||
|
||||
self.jwt_handler = jwt and AuthJWT(self, **jwt)
|
||||
|
||||
def get_vars_next(self):
|
||||
next = current.request.vars._next
|
||||
@@ -1612,7 +1916,7 @@ class Auth(object):
|
||||
'reset_password', 'request_reset_password',
|
||||
'change_password', 'profile', 'groups',
|
||||
'impersonate', 'not_authorized', 'confirm_registration',
|
||||
'bulk_register','manage_tokens'):
|
||||
'bulk_register','manage_tokens','jwt'):
|
||||
if len(request.args) >= 2 and args[0] == 'impersonate':
|
||||
return getattr(self, args[0])(request.args[1])
|
||||
else:
|
||||
@@ -1627,7 +1931,7 @@ class Auth(object):
|
||||
elif args(1) == self.settings.cas_actions['proxyvalidate']:
|
||||
return self.cas_validate(version=2, proxy=True)
|
||||
elif args(1) == self.settings.cas_actions['logout']:
|
||||
return self.logout(next=request.vars.service or DEFAULT)
|
||||
return self.logout(next=request.vars.service or DEFAULT)
|
||||
else:
|
||||
raise HTTP(404)
|
||||
|
||||
@@ -2652,8 +2956,8 @@ class Auth(object):
|
||||
user = table_user(**{username: entered_username})
|
||||
if user:
|
||||
# user in db, check if registration pending or disabled
|
||||
temp_user = user
|
||||
if temp_user.registration_key == 'pending':
|
||||
temp_user = user
|
||||
if (temp_user.registration_key or '').startswith('pending'):
|
||||
response.flash = self.messages.registration_pending
|
||||
return form
|
||||
elif temp_user.registration_key in ('disabled', 'blocked'):
|
||||
@@ -3028,7 +3332,11 @@ class Auth(object):
|
||||
DIV(_id="pre-reg", *self.settings.pre_registration_div),
|
||||
'', formstyle, '')
|
||||
|
||||
table_user.registration_key.default = key = web2py_uuid()
|
||||
key = web2py_uuid()
|
||||
if self.settings.registration_requires_approval:
|
||||
key = 'pending-'+key
|
||||
|
||||
table_user.registration_key.default = key
|
||||
if form.accepts(request, session if self.csrf_prevention else None,
|
||||
formname='register',
|
||||
onvalidation=onvalidation,
|
||||
@@ -3242,11 +3550,12 @@ class Auth(object):
|
||||
formname='retrieve_password', dbio=False,
|
||||
onvalidation=onvalidation, hideerror=self.settings.hideerror):
|
||||
user = table_user(email=form.vars.email)
|
||||
key = user.registration_key
|
||||
if not user:
|
||||
current.session.flash = \
|
||||
self.messages.invalid_email
|
||||
redirect(self.url(args=request.args))
|
||||
elif user.registration_key in ('pending', 'disabled', 'blocked'):
|
||||
elif key in ('pending', 'disabled', 'blocked') or (key or '').startswith('pending'):
|
||||
current.session.flash = \
|
||||
self.messages.registration_pending
|
||||
redirect(self.url(args=request.args))
|
||||
@@ -3450,6 +3759,11 @@ class Auth(object):
|
||||
except Exception:
|
||||
session.flash = self.messages.invalid_reset_password
|
||||
redirect(next, client_side=self.settings.client_side)
|
||||
|
||||
key = user.registration_key
|
||||
if key in ('pending', 'disabled', 'blocked') or (key or '').startswith('pending'):
|
||||
session.flash = self.messages.registration_pending
|
||||
redirect(next, client_side=self.settings.client_side)
|
||||
|
||||
if onvalidation is DEFAULT:
|
||||
onvalidation = self.settings.reset_password_onvalidation
|
||||
@@ -3544,11 +3858,12 @@ class Auth(object):
|
||||
onvalidation=onvalidation,
|
||||
hideerror=self.settings.hideerror):
|
||||
user = table_user(**{userfield:form.vars.get(userfield)})
|
||||
key = user.registration_key
|
||||
if not user:
|
||||
session.flash = self.messages['invalid_%s' % userfield]
|
||||
redirect(self.url(args=request.args),
|
||||
client_side=self.settings.client_side)
|
||||
elif user.registration_key in ('pending', 'disabled', 'blocked'):
|
||||
elif key in ('pending', 'disabled', 'blocked') or (key or '').startswith('pending'):
|
||||
session.flash = self.messages.registration_pending
|
||||
redirect(self.url(args=request.args),
|
||||
client_side=self.settings.client_side)
|
||||
@@ -3727,6 +4042,50 @@ class Auth(object):
|
||||
for callback in onaccept:
|
||||
callback(form)
|
||||
|
||||
def jwt(self):
|
||||
"""
|
||||
To use JWT authentication:
|
||||
1) instantiate auth with
|
||||
|
||||
auth = Auth(db, jwt = {'secret_key':'secret'})
|
||||
|
||||
where 'secret' is your own secret string.
|
||||
|
||||
2) Secorate functions that require login but should accept the JWT token credentials:
|
||||
|
||||
@auth.allows_jwt()
|
||||
@auth.requires_login()
|
||||
def myapi(): return 'hello %s' % auth.user.email
|
||||
|
||||
Notice jwt is allowed but not required. if user is logged in, myapi is accessible.
|
||||
|
||||
3) Use it!
|
||||
|
||||
Now API users can obtain a token with
|
||||
|
||||
http://.../app/default/user/jwt?username=...&password=....
|
||||
|
||||
(returns json object with a token attribute)
|
||||
API users can refresh an existing token with
|
||||
|
||||
http://.../app/default/user/jwt?token=...
|
||||
|
||||
they can authenticate themselves when calling http:/.../myapi by injecting a header
|
||||
|
||||
Authorization: Bearer <the jwt token>
|
||||
|
||||
Any additional attributes in the jwt argument of Auth() below:
|
||||
|
||||
auth = Auth(db, jwt = {...})
|
||||
|
||||
are passed to the constructor of class AuthJWT. Look there for documentation.
|
||||
"""
|
||||
if not self.jwt_handler:
|
||||
raise HTTP(400, "Not authorized")
|
||||
else:
|
||||
current.response.headers['content-type'] = 'application/json'
|
||||
raise HTTP(200, self.jwt_handler.jwt_token_manager())
|
||||
|
||||
def is_impersonating(self):
|
||||
return self.is_logged_in() and 'impersonator' in current.session.auth
|
||||
|
||||
@@ -3825,6 +4184,12 @@ class Auth(object):
|
||||
raise HTTP(403, 'ACCESS DENIED')
|
||||
return self.messages.access_denied
|
||||
|
||||
def allows_jwt(self, otherwise=None):
|
||||
if not self.jwt_handler:
|
||||
raise HTTP(400, "Not authorized")
|
||||
else:
|
||||
return self.jwt_handler.allows_jwt()
|
||||
|
||||
def requires(self, condition, requires_login=True, otherwise=None):
|
||||
"""
|
||||
Decorator that prevents access to action if not logged in
|
||||
@@ -4328,7 +4693,7 @@ class Auth(object):
|
||||
if resolve:
|
||||
if slug:
|
||||
wiki = self._wiki.read(slug, force_render)
|
||||
if isinstance(wiki, dict) and wiki.has_key('content'): # FIXME: .has_key() is deprecated
|
||||
if isinstance(wiki, dict) and 'content' in wiki:
|
||||
# We don't want to return a dict object, just the wiki
|
||||
wiki = wiki['content']
|
||||
else:
|
||||
@@ -5345,7 +5710,7 @@ class Service(object):
|
||||
|
||||
def return_error(id, code, message=None, data=None):
|
||||
error = {'code': code}
|
||||
if Service.jsonrpc_errors.has_key(code):
|
||||
if code in Service.jsonrpc_errors:
|
||||
error['message'] = Service.jsonrpc_errors[code][0]
|
||||
error['data'] = Service.jsonrpc_errors[code][1]
|
||||
if message is not None:
|
||||
|
||||
@@ -40,8 +40,8 @@ ProgramInfo = '''%s
|
||||
%s
|
||||
%s''' % (ProgramName, ProgramAuthor, ProgramVersion)
|
||||
|
||||
if not sys.version[:3] in ['2.5', '2.6', '2.7']:
|
||||
msg = 'Warning: web2py requires Python 2.5, 2.6 or 2.7 but you are running:\n%s'
|
||||
if not sys.version[:3] in ['2.6', '2.7']:
|
||||
msg = 'Warning: web2py requires Python 2.6 or 2.7 but you are running:\n%s'
|
||||
msg = msg % sys.version
|
||||
sys.stderr.write(msg)
|
||||
|
||||
@@ -56,8 +56,8 @@ def run_system_tests(options):
|
||||
major_version = sys.version_info[0]
|
||||
minor_version = sys.version_info[1]
|
||||
if major_version == 2:
|
||||
if minor_version in (5, 6):
|
||||
sys.stderr.write("Python 2.5 or 2.6\n")
|
||||
if minor_version in (6,):
|
||||
sys.stderr.write('Python 2.6\n')
|
||||
ret = subprocess.call(['unit2', '-v', 'gluon.tests'])
|
||||
elif minor_version in (7,):
|
||||
call_args = [sys.executable, '-m', 'unittest', '-v', 'gluon.tests']
|
||||
@@ -1117,12 +1117,12 @@ def start(cron=True):
|
||||
if hasattr(options, key):
|
||||
setattr(options, key, getattr(options2, key))
|
||||
|
||||
logfile0 = os.path.join('extras', 'examples', 'logging.example.conf')
|
||||
logfile0 = os.path.join('examples', 'logging.example.conf')
|
||||
if not os.path.exists('logging.conf') and os.path.exists(logfile0):
|
||||
import shutil
|
||||
sys.stdout.write("Copying logging.conf.example to logging.conf ... ")
|
||||
shutil.copyfile('logging.example.conf', logfile0)
|
||||
sys.stdout.write("OK\n")
|
||||
shutil.copyfile(logfile0, 'logging.conf')
|
||||
sys.stdout.write('OK\n')
|
||||
|
||||
# ## if -T run doctests (no cron)
|
||||
if hasattr(options, 'test') and options.test:
|
||||
|
||||
14
web2py.py
14
web2py.py
@@ -3,6 +3,9 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
import gluon.widget
|
||||
from multiprocessing import freeze_support
|
||||
# import gluon.import_all ##### This should be uncommented for py2exe.py
|
||||
|
||||
if hasattr(sys, 'frozen'):
|
||||
path = os.path.dirname(os.path.abspath(sys.executable)) # for py2exe
|
||||
@@ -14,17 +17,10 @@ os.chdir(path)
|
||||
|
||||
sys.path = [path] + [p for p in sys.path if not p == path]
|
||||
|
||||
# import gluon.import_all ##### This should be uncommented for py2exe.py
|
||||
import gluon.widget
|
||||
|
||||
# Start Web2py and Web2py cron service!
|
||||
if __name__ == '__main__':
|
||||
try:
|
||||
from multiprocessing import freeze_support
|
||||
freeze_support()
|
||||
except:
|
||||
sys.stderr.write('Sorry, -K only supported for python 2.6-2.7\n')
|
||||
if os.environ.has_key("COVERAGE_PROCESS_START"):
|
||||
freeze_support()
|
||||
if 'COVERAGE_PROCESS_START' in os.environ:
|
||||
try:
|
||||
import coverage
|
||||
coverage.process_startup()
|
||||
|
||||
Reference in New Issue
Block a user