Auth refactor, extracted many methods into a base class for more generic auth mechanisms.

Partially addresses #1526
Includes a solution for IS_LOWER and IS_UPPER validator problems I mentioned in #1353
This commit is contained in:
Leonel Câmara
2016-11-05 16:37:22 +00:00
parent 6d38aa7a3d
commit 02f0bdb8d3
6 changed files with 1438 additions and 813 deletions
+1020
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -13,6 +13,7 @@ from .test_contribs import *
from .test_routes import * from .test_routes import *
from .test_router import * from .test_router import *
from .test_validators import * from .test_validators import *
from .test_authapi import *
from .test_tools import * from .test_tools import *
from .test_utils import * from .test_utils import *
from .test_serializers import * from .test_serializers import *
+171
View File
@@ -0,0 +1,171 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
""" Unit tests for authapi """
import os
import unittest
from gluon.globals import Request, Response, Session
from gluon.languages import translator
from gluon.dal import DAL, Field
from gluon.authapi import AuthAPI
from gluon.storage import Storage
from gluon._compat import to_bytes, to_native, add_charset
DEFAULT_URI = os.getenv('DB', 'sqlite:memory')
class TestAuthAPI(unittest.TestCase):
def setUp(self):
self.request = Request(env={})
self.request.application = 'a'
self.request.controller = 'c'
self.request.function = 'f'
self.request.folder = 'applications/admin'
self.response = Response()
self.session = Session()
T = translator('', 'en')
self.session.connect(self.request, self.response)
from gluon.globals import current
self.current = current
self.current.request = self.request
self.current.response = self.response
self.current.session = self.session
self.current.T = T
self.db = DAL(DEFAULT_URI, check_reserved=['all'])
self.auth = AuthAPI(self.db)
self.auth.define_tables(username=True, signature=False)
# Create a user
self.auth.table_user().validate_and_insert(first_name='Bart',
last_name='Simpson',
username='bart',
email='bart@simpson.com',
password='bart_password',
registration_key='',
registration_id=''
)
self.db.commit()
def test_login(self):
result = self.auth.login(**{'username': 'bart', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
self.assertTrue(result['user']['email'] == 'bart@simpson.com')
self.auth.logout()
self.assertFalse(self.auth.is_logged_in())
self.auth.settings.username_case_sensitive = False
result = self.auth.login(**{'username': 'BarT', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
def test_logout(self):
self.auth.login(**{'username': 'bart', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
result = self.auth.logout()
self.assertTrue(not self.auth.is_logged_in())
self.assertTrue(result['user'] is None)
def test_register(self):
self.auth.settings.login_after_registration = True
result = self.auth.register(**{
'username': 'lisa',
'first_name': 'Lisa',
'last_name': 'Simpson',
'email': 'lisa@simpson.com',
'password': 'lisa_password'
})
self.assertTrue(result['user']['email'] == 'lisa@simpson.com')
self.assertTrue(self.auth.is_logged_in())
with self.assertRaises(AssertionError): # Can't register if you're logged in
result = self.auth.register(**{
'username': 'lisa',
'first_name': 'Lisa',
'last_name': 'Simpson',
'email': 'lisa@simpson.com',
'password': 'lisa_password'
})
self.auth.logout()
self.auth.settings.login_after_registration = False
result = self.auth.register(**{
'username': 'barney',
'first_name': 'Barney',
'last_name': 'Gumble',
'email': 'barney@simpson.com',
'password': 'barney_password'
})
self.assertTrue(result['user']['email'] == 'barney@simpson.com')
self.assertFalse(self.auth.is_logged_in())
self.auth.settings.login_userfield = 'email'
result = self.auth.register(**{
'username': 'lisa',
'first_name': 'Lisa',
'last_name': 'Simpson',
'email': 'lisa@simpson.com',
'password': 'lisa_password'
})
self.assertTrue(result['errors']['email'] == self.auth.messages.email_taken)
self.assertTrue(result['user'] is None)
self.auth.settings.registration_requires_verification = True
result = self.auth.register(**{
'username': 'homer',
'first_name': 'Homer',
'last_name': 'Simpson',
'email': 'homer@simpson.com',
'password': 'homer_password'
})
self.assertTrue('key' in result['user'])
def test_profile(self):
with self.assertRaises(AssertionError):
# We are not logged in
self.auth.profile()
self.auth.login(**{'username': 'bart', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
result = self.auth.profile(email='bartolo@simpson.com')
self.assertTrue(result['user']['email'] == 'bartolo@simpson.com')
self.assertTrue(self.auth.table_user()[result['user']['id']].email == 'bartolo@simpson.com')
def test_change_password(self):
with self.assertRaises(AssertionError):
# We are not logged in
self.auth.change_password()
self.auth.login(**{'username': 'bart', 'password': 'bart_password'})
self.assertTrue(self.auth.is_logged_in())
self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='1234')
self.auth.logout()
self.assertTrue(not self.auth.is_logged_in())
self.auth.login(username='bart', password='1234')
self.assertTrue(self.auth.is_logged_in())
result = self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='5678')
self.assertTrue('new_password2' in result['errors'])
result = self.auth.change_password(old_password='bart_password', new_password='1234', new_password2='1234')
self.assertTrue('old_password' in result['errors'])
def test_verify_key(self):
self.auth.settings.registration_requires_verification = True
result = self.auth.register(**{
'username': 'homer',
'first_name': 'Homer',
'last_name': 'Simpson',
'email': 'homer@simpson.com',
'password': 'homer_password'
})
self.assertTrue('key' in result['user'])
homer_id = result['user']['id']
homers_key = result['user']['key']
result = self.auth.verify_key(key=None)
self.assertTrue(result['errors'] is not None)
result = self.auth.verify_key(key='12345')
self.assertTrue(result['errors'] is not None)
result = self.auth.verify_key(key=homers_key)
self.assertTrue(result['errors'] is None)
self.assertEqual(self.auth.table_user()[homer_id].registration_key, '')
self.auth.settings.registration_requires_approval = True
result = self.auth.register(**{
'username': 'lisa',
'first_name': 'Lisa',
'last_name': 'Simpson',
'email': 'lisa@simpson.com',
'password': 'lisa_password'
})
lisa_id = result['user']['id']
result = self.auth.verify_key(key=result['user']['key'])
self.assertEqual(self.auth.table_user()[lisa_id].registration_key, 'pending')
+6 -2
View File
@@ -702,15 +702,19 @@ class TestValidators(unittest.TestCase):
def test_IS_LOWER(self): def test_IS_LOWER(self):
rtn = IS_LOWER()('ABC') rtn = IS_LOWER()('ABC')
self.assertEqual(rtn, ('abc', None))
rtn = IS_LOWER()(b'ABC')
self.assertEqual(rtn, (b'abc', None)) self.assertEqual(rtn, (b'abc', None))
rtn = IS_LOWER()('Ñ') rtn = IS_LOWER()('Ñ')
self.assertEqual(rtn, (b'\xc3\xb1', None)) self.assertEqual(rtn, ('ñ', None))
def test_IS_UPPER(self): def test_IS_UPPER(self):
rtn = IS_UPPER()('abc') rtn = IS_UPPER()('abc')
self.assertEqual(rtn, ('ABC', None))
rtn = IS_UPPER()(b'abc')
self.assertEqual(rtn, (b'ABC', None)) self.assertEqual(rtn, (b'ABC', None))
rtn = IS_UPPER()('ñ') rtn = IS_UPPER()('ñ')
self.assertEqual(rtn, (b'\xc3\x91', None)) self.assertEqual(rtn, ('Ñ', None))
def test_IS_SLUG(self): def test_IS_SLUG(self):
rtn = IS_SLUG()('abc123') rtn = IS_SLUG()('abc123')
+108 -690
View File
@@ -36,6 +36,7 @@ import json
from email import message_from_string from email import message_from_string
from gluon.authapi import AuthAPI
from gluon.contenttype import contenttype from gluon.contenttype import contenttype
from gluon.storage import Storage, StorageList, Settings, Messages from gluon.storage import Storage, StorageList, Settings, Messages
from gluon.utils import web2py_uuid, compare from gluon.utils import web2py_uuid, compare
@@ -55,7 +56,7 @@ Field = DAL.Field
__all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki', __all__ = ['Mail', 'Auth', 'Recaptcha', 'Recaptcha2', 'Crud', 'Service', 'Wiki',
'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate'] 'PluginManager', 'fetch', 'geocode', 'reverse_geocode', 'prettydate']
### mind there are two loggers here (logger and crud.settings.logger)! # mind there are two loggers here (logger and crud.settings.logger)!
logger = logging.getLogger("web2py") logger = logging.getLogger("web2py")
DEFAULT = lambda: None DEFAULT = lambda: None
@@ -1318,9 +1319,9 @@ class AuthJWT(object):
while sticking to a somewhat-stable Auth API. while sticking to a somewhat-stable Auth API.
""" """
# TODO: Check the following comment # TODO: Check the following comment
## is the following safe or should we use # is the following safe or should we use
## calendar.timegm(datetime.datetime.utcnow().timetuple()) # calendar.timegm(datetime.datetime.utcnow().timetuple())
## result seem to be the same (seconds since epoch, in UTC) # result seem to be the same (seconds since epoch, in UTC)
now = time.mktime(datetime.datetime.now().timetuple()) now = time.mktime(datetime.datetime.now().timetuple())
expires = now + self.expiration expires = now + self.expiration
payload = dict( payload = dict(
@@ -1484,172 +1485,119 @@ class AuthJWT(object):
return decorator return decorator
class Auth(object):
default_settings = dict( class Auth(AuthAPI):
hideerror=False,
password_min_length=4, default_settings = dict(AuthAPI.default_settings,
cas_maps=None,
reset_password_requires_verification=False,
registration_requires_verification=False,
registration_requires_approval=False,
bulk_register_enabled=False,
login_after_registration=False,
login_after_password_change=True,
alternate_requires_registration=False,
create_user_groups="user_%(id)s",
everybody_group_id=None,
manager_actions={},
auth_manager_role=None,
two_factor_authentication_group=None,
auth_two_factor_enabled=False,
auth_two_factor_tries_left=3,
login_captcha=None,
register_captcha=None,
pre_registration_div=None,
retrieve_username_captcha=None,
retrieve_password_captcha=None,
captcha=None,
prevent_open_redirect_attacks=True,
prevent_password_reset_attacks=True,
expiration=3600, # one hour
long_expiration=3600 * 30 * 24, # one month
remember_me_form=True,
allow_basic_login=False, allow_basic_login=False,
allow_basic_login_only=False, allow_basic_login_only=False,
on_failed_authentication=lambda x: redirect(x),
formstyle=None,
label_separator=None,
logging_enabled=True,
allow_delete_accounts=False, allow_delete_accounts=False,
password_field='password', alternate_requires_registration=False,
table_user_name='auth_user', auth_manager_role=None,
table_group_name='auth_group', auth_two_factor_enabled=False,
table_membership_name='auth_membership', auth_two_factor_tries_left=3,
table_permission_name='auth_permission', bulk_register_enabled=False,
table_event_name='auth_event', captcha=None,
cas_maps=None,
client_side=True,
formstyle=None,
hideerror=False,
label_separator=None,
login_after_password_change=True,
login_after_registration=False,
login_captcha=None,
long_expiration=3600 * 30 * 24, # one month
mailer=None,
manager_actions={},
multi_login=False,
on_failed_authentication=lambda x: redirect(x),
pre_registration_div=None,
prevent_open_redirect_attacks=True,
prevent_password_reset_attacks=True,
profile_fields=None,
register_captcha=None,
register_fields=None,
register_verify_password=True,
remember_me_form=True,
reset_password_requires_verification=False,
retrieve_password_captcha=None,
retrieve_username_captcha=None,
showid=False,
table_cas=None,
table_cas_name='auth_cas', table_cas_name='auth_cas',
table_token_name='auth_token', table_event=None,
table_user=None,
table_group=None, table_group=None,
table_membership=None, table_membership=None,
table_permission=None, table_permission=None,
table_event=None, table_token_name='auth_token',
table_cas=None, table_user=None,
showid=False, two_factor_authentication_group=None,
use_username=False,
login_email_validate=True,
login_userfield=None,
multi_login=False,
logout_onlogout=None,
register_fields=None,
register_verify_password=True,
profile_fields=None,
email_case_sensitive=True,
username_case_sensitive=True,
update_fields=['email'], update_fields=['email'],
ondelete="CASCADE", wiki=Settings()
client_side=True,
renew_session_onlogin=True,
renew_session_onlogout=True,
keep_session_onlogin=True,
keep_session_onlogout=False,
wiki=Settings(),
) )
# ## these are messages that can be customized # ## these are messages that can be customized
default_messages = dict( default_messages = dict(AuthAPI.default_messages,
login_button='Log In',
register_button='Sign Up',
password_reset_button='Request reset password',
password_change_button='Change password',
profile_save_button='Apply changes',
submit_button='Submit',
verify_password='Verify Password',
delete_label='Check to delete',
function_disabled='Function disabled',
access_denied='Insufficient privileges', access_denied='Insufficient privileges',
registration_verifying='Registration needs verification', bulk_invite_body='You have been invited to join %(site)s, click %(link)s to complete the process',
registration_pending='Registration is pending approval', bulk_invite_subject='Invitation to join %(site)s',
email_taken='This email already has an account', delete_label='Check to delete',
invalid_username='Invalid username',
username_taken='Username already taken',
login_disabled='Login disabled by administrator',
logged_in='Logged in',
email_sent='Email sent', email_sent='Email sent',
unable_to_send_email='Unable to send email',
email_verified='Email verified', email_verified='Email verified',
logged_out='Logged out', function_disabled='Function disabled',
registration_successful='Registration successful', impersonate_log='User %(id)s is impersonating %(other_id)s',
invalid_email='Invalid email', invalid_reset_password='Invalid reset password',
invalid_login='Invalid login',
invalid_user='Invalid user',
invalid_password='Invalid password',
invalid_two_factor_code='Incorrect code. {0} more attempt(s) remaining.', invalid_two_factor_code='Incorrect code. {0} more attempt(s) remaining.',
is_empty="Cannot be empty", is_empty="Cannot be empty",
mismatched_password="Password fields don't match", label_client_ip='Client IP',
verify_email='Welcome %(username)s! Click on the link %(link)s to verify your email', label_description='Description',
verify_email_subject='Email verification', label_email='E-mail',
username_sent='Your username was emailed to you', label_first_name='First name',
label_group_id='Group ID',
label_last_name='Last name',
label_name='Name',
label_origin='Origin',
label_password='Password',
label_record_id='Record ID',
label_registration_id='Registration identifier',
label_registration_key='Registration key',
label_remember_me="Remember me (for 30 days)",
label_reset_password_key='Reset Password key',
label_role='Role',
label_table_name='Object or table name',
label_time_stamp='Timestamp',
label_two_factor='Authentication code',
label_user_id='User ID',
label_username='Username',
login_button='Log In',
login_disabled='Login disabled by administrator',
new_password='New password',
new_password_sent='A new password was emailed to you', new_password_sent='A new password was emailed to you',
password_changed='Password changed', old_password='Old password',
retrieve_username='Your username is: %(username)s', password_change_button='Change password',
retrieve_username_subject='Username retrieve', password_reset_button='Request reset password',
retrieve_password='Your password is: %(password)s', profile_save_button='Apply changes',
retrieve_password_subject='Password retrieve', register_button='Sign Up',
reset_password='Click on the link %(link)s to reset your password', reset_password='Click on the link %(link)s to reset your password',
reset_password_log='User %(id)s Password reset',
reset_password_subject='Password reset', reset_password_subject='Password reset',
bulk_invite_subject='Invitation to join %(site)s', retrieve_password='Your password is: %(password)s',
retrieve_password_log='User %(id)s Password retrieved',
retrieve_password_subject='Password retrieve',
retrieve_two_factor_code='Your temporary login code is {0}', retrieve_two_factor_code='Your temporary login code is {0}',
retrieve_two_factor_code_subject='Two-step Login Authentication Code', retrieve_two_factor_code_subject='Two-step Login Authentication Code',
bulk_invite_body='You have been invited to join %(site)s, click %(link)s to complete the process', retrieve_username='Your username is: %(username)s',
invalid_reset_password='Invalid reset password',
profile_updated='Profile updated',
new_password='New password',
old_password='Old password',
group_description='Group uniquely assigned to user %(id)s',
register_log='User %(id)s Registered',
login_log='User %(id)s Logged-in',
login_failed_log=None,
logout_log='User %(id)s Logged-out',
profile_log='User %(id)s Profile updated',
verify_email_log='User %(id)s Verification email sent',
retrieve_username_log='User %(id)s Username retrieved', retrieve_username_log='User %(id)s Username retrieved',
retrieve_password_log='User %(id)s Password retrieved', retrieve_username_subject='Username retrieve',
reset_password_log='User %(id)s Password reset', submit_button='Submit',
change_password_log='User %(id)s Password changed',
add_group_log='Group %(group_id)s created',
del_group_log='Group %(group_id)s deleted',
add_membership_log=None,
del_membership_log=None,
has_membership_log=None,
add_permission_log=None,
del_permission_log=None,
has_permission_log=None,
impersonate_log='User %(id)s is impersonating %(other_id)s',
label_first_name='First name',
label_last_name='Last name',
label_username='Username',
label_email='E-mail',
label_password='Password',
label_registration_key='Registration key',
label_reset_password_key='Reset Password key',
label_registration_id='Registration identifier',
label_role='Role',
label_description='Description',
label_user_id='User ID',
label_group_id='Group ID',
label_name='Name',
label_table_name='Object or table name',
label_record_id='Record ID',
label_time_stamp='Timestamp',
label_client_ip='Client IP',
label_origin='Origin',
label_remember_me="Remember me (for 30 days)",
label_two_factor='Authentication code',
two_factor_comment='This code was emailed to you and is required for login.', two_factor_comment='This code was emailed to you and is required for login.',
verify_password_comment='please input your password again', unable_send_email='Unable to send email',
username_sent='Your username was emailed to you',
verify_email='Welcome %(username)s! Click on the link %(link)s to verify your email',
verify_email_log='User %(id)s Verification email sent',
verify_email_subject='Email verification',
verify_password='Verify Password',
verify_password_comment='please input your password again'
) )
""" """
Class for authentication, authorization, role based access control. Class for authentication, authorization, role based access control.
@@ -1816,7 +1764,7 @@ class Auth(object):
self.user = auth.user self.user = auth.user
# this is a trick to speed up sessions to avoid many writes # this is a trick to speed up sessions to avoid many writes
if (now - auth.last_visit).seconds > (auth.expiration / 10): if (now - auth.last_visit).seconds > (auth.expiration / 10):
auth.last_visit = request.now auth.last_visit = now
else: else:
self.user = None self.user = None
if session.auth: if session.auth:
@@ -1893,8 +1841,7 @@ class Auth(object):
# ## these are messages that can be customized # ## these are messages that can be customized
messages = self.messages = Messages(current.T) messages = self.messages = Messages(current.T)
messages.update(Auth.default_messages) messages.update(Auth.default_messages)
messages.update(ajax_failed_authentication= messages.update(ajax_failed_authentication=DIV(H4('NOT AUTHORIZED'),
DIV(H4('NOT AUTHORIZED'),
'Please ', 'Please ',
A('login', A('login',
_href=self.settings.login_url + _href=self.settings.login_url +
@@ -1936,27 +1883,6 @@ class Auth(object):
return next return next
return None return None
def _get_user_id(self):
"""accessor for auth.user_id"""
return self.user and self.user.id or None
user_id = property(_get_user_id, doc="user.id or None")
def table_user(self):
return self.db[self.settings.table_user_name]
def table_group(self):
return self.db[self.settings.table_group_name]
def table_membership(self):
return self.db[self.settings.table_membership_name]
def table_permission(self):
return self.db[self.settings.table_permission_name]
def table_event(self):
return self.db[self.settings.table_event_name]
def table_cas(self): def table_cas(self):
return self.db[self.settings.table_cas_name] return self.db[self.settings.table_cas_name]
@@ -2213,15 +2139,6 @@ class Auth(object):
return self.bar return self.bar
def __get_migrate(self, tablename, migrate=True):
if type(migrate).__name__ == 'str':
return (migrate + tablename + '.table')
elif migrate == False:
return False
else:
return True
def enable_record_versioning(self, def enable_record_versioning(self,
tables, tables,
archive_db=None, archive_db=None,
@@ -2267,49 +2184,6 @@ class Auth(object):
current_record=current_record, current_record=current_record,
current_record_label=current_record_label) current_record_label=current_record_label)
def define_signature(self):
db = self.db
settings = self.settings
request = current.request
T = current.T
reference_user = 'reference %s' % settings.table_user_name
def lazy_user(auth=self):
return auth.user_id
def represent(id, record=None, s=settings):
try:
user = s.table_user(id)
return '%s %s' % (user.get("first_name", user.get("email")),
user.get("last_name", ''))
except:
return id
ondelete = self.settings.ondelete
self.signature = Table(
self.db, 'auth_signature',
Field('is_active', 'boolean',
default=True,
readable=False, writable=False,
label=T('Is Active')),
Field('created_on', 'datetime',
default=request.now,
writable=False, readable=False,
label=T('Created On')),
Field('created_by',
reference_user,
default=lazy_user, represent=represent,
writable=False, readable=False,
label=T('Created By'), ondelete=ondelete),
Field('modified_on', 'datetime',
update=request.now, default=request.now,
writable=False, readable=False,
label=T('Modified On')),
Field('modified_by',
reference_user, represent=represent,
default=lazy_user, update=lazy_user,
writable=False, readable=False,
label=T('Modified By'), ondelete=ondelete))
def define_tables(self, username=None, signature=None, enable_tokens=False, def define_tables(self, username=None, signature=None, enable_tokens=False,
migrate=None, fake_migrate=None): migrate=None, fake_migrate=None):
""" """
@@ -2333,179 +2207,11 @@ class Auth(object):
if fake_migrate is None: if fake_migrate is None:
fake_migrate = db._fake_migrate fake_migrate = db._fake_migrate
settings = self.settings settings = self.settings
if username is None:
username = settings.use_username
else:
settings.use_username = username
settings.enable_tokens = enable_tokens settings.enable_tokens = enable_tokens
if not self.signature: super(Auth, self).define_tables(username, signature, migrate, fake_migrate)
self.define_signature()
if signature:
signature_list = [self.signature]
elif not signature:
signature_list = []
elif isinstance(signature, Table):
signature_list = [signature]
else:
signature_list = signature
is_not_empty = IS_NOT_EMPTY(error_message=self.messages.is_empty)
is_crypted = CRYPT(key=settings.hmac_key,
min_length=settings.password_min_length)
is_unique_email = [
IS_EMAIL(error_message=self.messages.invalid_email),
IS_NOT_IN_DB(db, '%s.email' % settings.table_user_name,
error_message=self.messages.email_taken)]
if not settings.email_case_sensitive:
is_unique_email.insert(1, IS_LOWER())
if settings.table_user_name not in db.tables:
passfield = settings.password_field
extra_fields = settings.extra_fields.get(
settings.table_user_name, []) + signature_list
if username or settings.cas_provider:
is_unique_username = \
[IS_MATCH('[\w\.\-]+', strict=True,
error_message=self.messages.invalid_username),
IS_NOT_IN_DB(db, '%s.username' % settings.table_user_name,
error_message=self.messages.username_taken)]
if not settings.username_case_sensitive:
is_unique_username.insert(1, IS_LOWER())
db.define_table(
settings.table_user_name,
Field('first_name', length=128, default='',
label=self.messages.label_first_name,
requires=is_not_empty),
Field('last_name', length=128, default='',
label=self.messages.label_last_name,
requires=is_not_empty),
Field('email', length=512, default='',
label=self.messages.label_email,
requires=is_unique_email),
Field('username', length=128, default='',
label=self.messages.label_username,
requires=is_unique_username),
Field(passfield, 'password', length=512,
readable=False, label=self.messages.label_password,
requires=[is_crypted]),
Field('registration_key', length=512,
writable=False, readable=False, default='',
label=self.messages.label_registration_key),
Field('reset_password_key', length=512,
writable=False, readable=False, default='',
label=self.messages.label_reset_password_key),
Field('registration_id', length=512,
writable=False, readable=False, default='',
label=self.messages.label_registration_id),
*extra_fields,
**dict(
migrate=self.__get_migrate(settings.table_user_name,
migrate),
fake_migrate=fake_migrate,
format='%(username)s'))
else:
db.define_table(
settings.table_user_name,
Field('first_name', length=128, default='',
label=self.messages.label_first_name,
requires=is_not_empty),
Field('last_name', length=128, default='',
label=self.messages.label_last_name,
requires=is_not_empty),
Field('email', length=512, default='',
label=self.messages.label_email,
requires=is_unique_email),
Field(passfield, 'password', length=512,
readable=False, label=self.messages.label_password,
requires=[is_crypted]),
Field('registration_key', length=512,
writable=False, readable=False, default='',
label=self.messages.label_registration_key),
Field('reset_password_key', length=512,
writable=False, readable=False, default='',
label=self.messages.label_reset_password_key),
Field('registration_id', length=512,
writable=False, readable=False, default='',
label=self.messages.label_registration_id),
*extra_fields,
**dict(
migrate=self.__get_migrate(settings.table_user_name,
migrate),
fake_migrate=fake_migrate,
format='%(first_name)s %(last_name)s (%(id)s)'))
reference_table_user = 'reference %s' % settings.table_user_name
if settings.table_group_name not in db.tables:
extra_fields = settings.extra_fields.get(
settings.table_group_name, []) + signature_list
db.define_table(
settings.table_group_name,
Field('role', length=512, default='',
label=self.messages.label_role,
requires=IS_NOT_IN_DB(db, '%s.role' % settings.table_group_name)),
Field('description', 'text',
label=self.messages.label_description),
*extra_fields,
**dict(
migrate=self.__get_migrate(
settings.table_group_name, migrate),
fake_migrate=fake_migrate,
format='%(role)s (%(id)s)'))
reference_table_group = 'reference %s' % settings.table_group_name
if settings.table_membership_name not in db.tables:
extra_fields = settings.extra_fields.get(
settings.table_membership_name, []) + signature_list
db.define_table(
settings.table_membership_name,
Field('user_id', reference_table_user,
label=self.messages.label_user_id),
Field('group_id', reference_table_group,
label=self.messages.label_group_id),
*extra_fields,
**dict(
migrate=self.__get_migrate(
settings.table_membership_name, migrate),
fake_migrate=fake_migrate))
if settings.table_permission_name not in db.tables:
extra_fields = settings.extra_fields.get(
settings.table_permission_name, []) + signature_list
db.define_table(
settings.table_permission_name,
Field('group_id', reference_table_group,
label=self.messages.label_group_id),
Field('name', default='default', length=512,
label=self.messages.label_name,
requires=is_not_empty),
Field('table_name', length=512,
label=self.messages.label_table_name),
Field('record_id', 'integer', default=0,
label=self.messages.label_record_id,
requires=IS_INT_IN_RANGE(0, 10 ** 9)),
*extra_fields,
**dict(
migrate=self.__get_migrate(
settings.table_permission_name, migrate),
fake_migrate=fake_migrate))
if settings.table_event_name not in db.tables:
db.define_table(
settings.table_event_name,
Field('time_stamp', 'datetime',
default=current.request.now,
label=self.messages.label_time_stamp),
Field('client_ip',
default=current.request.client,
label=self.messages.label_client_ip),
Field('user_id', reference_table_user, default=None,
label=self.messages.label_user_id),
Field('origin', default='auth', length=512,
label=self.messages.label_origin,
requires=is_not_empty),
Field('description', 'text', default='',
label=self.messages.label_description,
requires=is_not_empty),
*settings.extra_fields.get(settings.table_event_name, []),
**dict(
migrate=self.__get_migrate(
settings.table_event_name, migrate),
fake_migrate=fake_migrate))
now = current.request.now now = current.request.now
reference_table_user = 'reference %s' % settings.table_user_name
if settings.cas_domains: if settings.cas_domains:
if settings.table_cas_name not in db.tables: if settings.table_cas_name not in db.tables:
db.define_table( db.define_table(
@@ -2518,7 +2224,7 @@ class Auth(object):
Field('renew', 'boolean', default=False), Field('renew', 'boolean', default=False),
*settings.extra_fields.get(settings.table_cas_name, []), *settings.extra_fields.get(settings.table_cas_name, []),
**dict( **dict(
migrate=self.__get_migrate( migrate=self._get_migrate(
settings.table_cas_name, migrate), settings.table_cas_name, migrate),
fake_migrate=fake_migrate)) fake_migrate=fake_migrate))
if settings.enable_tokens: if settings.enable_tokens:
@@ -2532,7 +2238,7 @@ class Auth(object):
Field('expires_on', 'datetime', default=datetime.datetime(2999, 12, 31)), Field('expires_on', 'datetime', default=datetime.datetime(2999, 12, 31)),
Field('token', writable=False, default=web2py_uuid, unique=True), Field('token', writable=False, default=web2py_uuid, unique=True),
*extra_fields, *extra_fields,
**dict(migrate=self.__get_migrate(settings.table_token_name, migrate), **dict(migrate=self._get_migrate(settings.table_token_name, migrate),
fake_migrate=fake_migrate)) fake_migrate=fake_migrate))
if not db._lazy_tables: if not db._lazy_tables:
settings.table_user = db[settings.table_user_name] settings.table_user = db[settings.table_user_name]
@@ -2566,26 +2272,6 @@ class Auth(object):
maps=maps) maps=maps)
return self return self
def log_event(self, description, vars=None, origin='auth'):
"""
Examples:
Use as::
auth.log_event(description='this happened', origin='auth')
"""
if not self.settings.logging_enabled or not description:
return
elif self.is_logged_in():
user_id = self.user.id
else:
user_id = None # user unknown
vars = vars or {}
# log messages should not be translated
if type(description).__name__ == 'lazyT':
description = description.m
self.table_event().insert(description=str(description % vars), origin=origin, user_id=user_id)
def get_or_create_user(self, keys, update_fields=['email'], def get_or_create_user(self, keys, update_fields=['email'],
login=True, get=True): login=True, get=True):
""" """
@@ -2683,29 +2369,6 @@ class Auth(object):
raise http_401 raise http_401
return (True, True, is_valid_user) return (True, True, is_valid_user)
def login_user(self, user):
"""
Logins the `user = db.auth_user(id)`
"""
from gluon.settings import global_settings
if global_settings.web2py_runtime_gae:
user = Row(self.table_user()._filter_fields(user, id=True))
delattr(user, 'password')
else:
user = Row(user)
for key in list(user.keys()):
value = user[key]
if callable(value) or key == 'password':
delattr(user, key)
if self.settings.renew_session_onlogin:
current.session.renew(clear_session=not self.settings.keep_session_onlogin)
current.session.auth = Storage(user=user,
last_visit=current.request.now,
expiration=self.settings.expiration,
hmac_key=web2py_uuid())
self.user = user
self.update_groups()
def _get_login_settings(self): def _get_login_settings(self):
table_user = self.table_user() table_user = self.table_user()
userfield = self.settings.login_userfield or 'username' \ userfield = self.settings.login_userfield or 'username' \
@@ -3447,16 +3110,6 @@ class Auth(object):
return form return form
def is_logged_in(self):
"""
Checks if the user is logged in and returns True/False.
If so user is in auth.user as well as in session.auth.user
"""
if self.user:
return True
return False
def verify_email(self, def verify_email(self,
next=DEFAULT, next=DEFAULT,
onaccept=DEFAULT, onaccept=DEFAULT,
@@ -3684,7 +3337,6 @@ class Auth(object):
raise Exception raise Exception
except Exception as e: except Exception as e:
session.flash = self.messages.invalid_reset_password session.flash = self.messages.invalid_reset_password
redirect(self.url('login', vars=dict(test=e)))
redirect(next, client_side=self.settings.client_side) redirect(next, client_side=self.settings.client_side)
passfield = self.settings.password_field passfield = self.settings.password_field
form = SQLFORM.factory( form = SQLFORM.factory(
@@ -4205,21 +3857,6 @@ class Auth(object):
else: else:
return SQLFORM(table_user, user.id, readonly=True) return SQLFORM(table_user, user.id, readonly=True)
def update_groups(self):
if not self.user:
return
user_groups = self.user_groups = {}
if current.session.auth:
current.session.auth.user_groups = self.user_groups
table_group = self.table_group()
table_membership = self.table_membership()
memberships = self.db(
table_membership.user_id == self.user.id).select()
for membership in memberships:
group = table_group(membership.group_id)
if group:
user_groups[membership.group_id] = group.role
def groups(self): def groups(self):
""" """
Displays the groups and their roles for the logged in user Displays the groups and their roles for the logged in user
@@ -4337,6 +3974,7 @@ class Auth(object):
If role is provided instead of group_id then the If role is provided instead of group_id then the
group_id is calculated. group_id is calculated.
""" """
def has_membership(self=self, group_id=group_id, role=role): def has_membership(self=self, group_id=group_id, role=role):
return self.has_membership(group_id=group_id, role=role) return self.has_membership(group_id=group_id, role=role)
return self.requires(has_membership, otherwise=otherwise) return self.requires(has_membership, otherwise=otherwise)
@@ -4348,6 +3986,7 @@ class Auth(object):
if user logged in is not a member of any group (role) that if user logged in is not a member of any group (role) that
has 'name' access to 'table_name', 'record_id'. has 'name' access to 'table_name', 'record_id'.
""" """
def has_permission(self=self, name=name, table_name=table_name, record_id=record_id): def has_permission(self=self, name=name, table_name=table_name, record_id=record_id):
return self.has_permission(name, table_name, record_id) return self.has_permission(name, table_name, record_id)
return self.requires(has_permission, otherwise=otherwise) return self.requires(has_permission, otherwise=otherwise)
@@ -4363,231 +4002,6 @@ class Auth(object):
return URL.verify(current.request, user_signature=True, hash_vars=hash_vars) return URL.verify(current.request, user_signature=True, hash_vars=hash_vars)
return self.requires(verify, otherwise) return self.requires(verify, otherwise)
def add_group(self, role, description=''):
"""
Creates a group associated to a role
"""
group_id = self.table_group().insert(role=role, description=description)
self.log_event(self.messages['add_group_log'], dict(group_id=group_id, role=role))
return group_id
def del_group(self, group_id):
"""
Deletes a group
"""
self.db(self.table_group().id == group_id).delete()
self.db(self.table_membership().group_id == group_id).delete()
self.db(self.table_permission().group_id == group_id).delete()
if group_id in self.user_groups:
del self.user_groups[group_id]
self.log_event(self.messages.del_group_log, dict(group_id=group_id))
def id_group(self, role):
"""
Returns the group_id of the group specified by the role
"""
rows = self.db(self.table_group().role == role).select()
if not rows:
return None
return rows[0].id
def user_group(self, user_id=None):
"""
Returns the group_id of the group uniquely associated to this user
i.e. `role=user:[user_id]`
"""
return self.id_group(self.user_group_role(user_id))
def user_group_role(self, user_id=None):
if not self.settings.create_user_groups:
return None
if user_id:
user = self.table_user()[user_id]
else:
user = self.user
return self.settings.create_user_groups % user
def has_membership(self, group_id=None, user_id=None, role=None):
"""
Checks if user is member of group_id or role
"""
group_id = group_id or self.id_group(role)
try:
group_id = int(group_id)
except:
group_id = self.id_group(group_id) # interpret group_id as a role
if not user_id and self.user:
user_id = self.user.id
membership = self.table_membership()
if group_id and user_id and self.db((membership.user_id == user_id) &
(membership.group_id == group_id)).select():
r = True
else:
r = False
self.log_event(self.messages['has_membership_log'],
dict(user_id=user_id, group_id=group_id, check=r))
return r
def add_membership(self, group_id=None, user_id=None, role=None):
"""
Gives user_id membership of group_id or role
if user is None than user_id is that of current logged in user
"""
group_id = group_id or self.id_group(role)
try:
group_id = int(group_id)
except:
group_id = self.id_group(group_id) # interpret group_id as a role
if not user_id and self.user:
user_id = self.user.id
if not group_id:
raise ValueError('group_id not provided or invalid')
if not user_id:
raise ValueError('user_id not provided or invalid')
membership = self.table_membership()
db = membership._db
record = db((membership.user_id == user_id) &
(membership.group_id == group_id),
ignore_common_filters=True).select().first()
if record:
if hasattr(record, 'is_active') and not record.is_active:
record.update_record(is_active=True)
return record.id
else:
id = membership.insert(group_id=group_id, user_id=user_id)
if role:
self.user_groups[group_id] = role
else:
self.update_groups()
self.log_event(self.messages['add_membership_log'],
dict(user_id=user_id, group_id=group_id))
return id
def del_membership(self, group_id=None, user_id=None, role=None):
"""
Revokes membership from group_id to user_id
if user_id is None than user_id is that of current logged in user
"""
group_id = group_id or self.id_group(role)
try:
group_id = int(group_id)
except:
group_id = self.id_group(group_id) # interpret group_id as a role
if not user_id and self.user:
user_id = self.user.id
membership = self.table_membership()
self.log_event(self.messages['del_membership_log'],
dict(user_id=user_id, group_id=group_id))
ret = self.db(membership.user_id == user_id)(membership.group_id == group_id).delete()
if group_id in self.user_groups:
del self.user_groups[group_id]
return ret
def has_permission(self,
name='any',
table_name='',
record_id=0,
user_id=None,
group_id=None,
):
"""
Checks if user_id or current logged in user is member of a group
that has 'name' permission on 'table_name' and 'record_id'
if group_id is passed, it checks whether the group has the permission
"""
if not group_id and self.settings.everybody_group_id and \
self.has_permission(name, table_name, record_id, user_id=None,
group_id=self.settings.everybody_group_id):
return True
if not user_id and not group_id and self.user:
user_id = self.user.id
if user_id:
membership = self.table_membership()
rows = self.db(membership.user_id == user_id).select(membership.group_id)
groups = set([row.group_id for row in rows])
if group_id and group_id not in groups:
return False
else:
groups = set([group_id])
permission = self.table_permission()
rows = self.db(permission.name ==
name)(permission.table_name ==
str(table_name))(permission.record_id ==
record_id).select(permission.group_id)
groups_required = set([row.group_id for row in rows])
if record_id:
rows = self.db(permission.name ==
name)(permission.table_name ==
str(table_name))(permission.record_id ==
0).select(permission.group_id)
groups_required = groups_required.union(set([row.group_id for row in rows]))
if groups.intersection(groups_required):
r = True
else:
r = False
if user_id:
self.log_event(self.messages['has_permission_log'],
dict(user_id=user_id, name=name,
table_name=table_name, record_id=record_id))
return r
def add_permission(self,
group_id,
name='any',
table_name='',
record_id=0,
):
"""
Gives group_id 'name' access to 'table_name' and 'record_id'
"""
permission = self.table_permission()
if group_id == 0:
group_id = self.user_group()
record = self.db((permission.group_id == group_id) &
(permission.name == name) &
(permission.table_name == str(table_name)) &
(permission.record_id == long(record_id)),
ignore_common_filters=True
).select(limitby=(0, 1), orderby_on_limitby=False).first()
if record:
if hasattr(record, 'is_active') and not record.is_active:
record.update_record(is_active=True)
id = record.id
else:
id = permission.insert(group_id=group_id, name=name,
table_name=str(table_name),
record_id=long(record_id))
self.log_event(self.messages['add_permission_log'],
dict(permission_id=id, group_id=group_id,
name=name, table_name=table_name,
record_id=record_id))
return id
def del_permission(self,
group_id,
name='any',
table_name='',
record_id=0,
):
"""
Revokes group_id 'name' access to 'table_name' and 'record_id'
"""
permission = self.table_permission()
self.log_event(self.messages['del_permission_log'],
dict(group_id=group_id, name=name,
table_name=table_name, record_id=record_id))
return self.db(permission.group_id ==
group_id)(permission.name ==
name)(permission.table_name ==
str(table_name))(permission.record_id ==
long(record_id)).delete()
def accessible_query(self, name, table, user_id=None): def accessible_query(self, name, table, user_id=None):
""" """
Returns a query with all accessible records for user_id or Returns a query with all accessible records for user_id or
@@ -5702,6 +5116,7 @@ class Service(object):
self.error() self.error()
class JsonRpcException(Exception): class JsonRpcException(Exception):
def __init__(self, code, info): def __init__(self, code, info):
jrpc_error = Service.jsonrpc_errors.get(code) jrpc_error = Service.jsonrpc_errors.get(code)
if jrpc_error: if jrpc_error:
@@ -6218,6 +5633,7 @@ class PluginManager(object):
class Expose(object): class Expose(object):
def __init__(self, base=None, basename=None, extensions=None, def __init__(self, base=None, basename=None, extensions=None,
allow_download=True, follow_symlink_out=False): allow_download=True, follow_symlink_out=False):
""" """
@@ -6245,7 +5661,8 @@ class Expose(object):
""" """
# why would this not be callable? but otherwise tests do not pass # why would this not be callable? but otherwise tests do not pass
if current.session and callable(current.session.forget): current.session.forget() if current.session and callable(current.session.forget):
current.session.forget()
self.follow_symlink_out = follow_symlink_out self.follow_symlink_out = follow_symlink_out
self.base = self.normalize_path( self.base = self.normalize_path(
base or os.path.join(current.request.folder, 'static')) base or os.path.join(current.request.folder, 'static'))
@@ -7032,6 +6449,7 @@ class Wiki(object):
class Config(object): class Config(object):
def __init__( def __init__(
self, self,
filename, filename,
+14 -3
View File
@@ -2478,9 +2478,14 @@ class IS_LOWER(Validator):
('\\xc3\\xb1', None) ('\\xc3\\xb1', None)
""" """
def __call__(self, value): def __call__(self, value):
return (to_bytes(to_unicode(value).lower()), None) cast_back = lambda x: x
if isinstance(value, str):
cast_back = to_native
elif isinstance(value, bytes):
cast_back = to_bytes
value = to_unicode(value).lower()
return (cast_back(value), None)
class IS_UPPER(Validator): class IS_UPPER(Validator):
@@ -2495,7 +2500,13 @@ class IS_UPPER(Validator):
""" """
def __call__(self, value): def __call__(self, value):
return (to_bytes(to_unicode(value).upper()), None) cast_back = lambda x: x
if isinstance(value, str):
cast_back = to_native
elif isinstance(value, bytes):
cast_back = to_bytes
value = to_unicode(value).upper()
return (cast_back(value), None)
def urlify(s, maxlen=80, keep_underscores=False): def urlify(s, maxlen=80, keep_underscores=False):