1058 lines
44 KiB
Python
1058 lines
44 KiB
Python
|
# -*- coding: utf-8 -*-
|
|||
|
"""
|
|||
|
| This file is part of the web2py Web Framework
|
|||
|
| Copyrighted by Massimo Di Pierro <mdipierro@cs.depaul.edu>
|
|||
|
| License: LGPLv3 (http://www.gnu.org/licenses/lgpl.html)
|
|||
|
"""
|
|||
|
from gluon._compat import long
|
|||
|
from gluon import current
|
|||
|
from gluon.storage import Messages, Settings, Storage
|
|||
|
from gluon.utils import web2py_uuid
|
|||
|
from gluon.validators import CRYPT, IS_EMAIL, IS_EQUAL_TO, IS_INT_IN_RANGE, IS_LOWER, IS_MATCH, IS_NOT_EMPTY, \
|
|||
|
IS_NOT_IN_DB
|
|||
|
from pydal.objects import Table, Field, Row
|
|||
|
import datetime
|
|||
|
from gluon.settings import global_settings
|
|||
|
|
|||
|
DEFAULT = lambda: None
|
|||
|
|
|||
|
|
|||
|
class AuthAPI(object):
|
|||
|
"""
|
|||
|
AuthAPI is a barebones Auth implementation which does not have a concept of
|
|||
|
HTML forms or redirects, emailing or even an URL, you are responsible for
|
|||
|
all that if you use it.
|
|||
|
The main Auth functions such as login, logout, register, profile are designed
|
|||
|
in a Dict In -> Dict Out logic so, for instance, if you set
|
|||
|
registration_requires_verification you are responsible for sending the key to
|
|||
|
the user and even rolling back the transaction if you can't do it.
|
|||
|
|
|||
|
NOTES: * It does not support all the callbacks Traditional Auth does yet.
|
|||
|
Some of the callbacks will not be supported.
|
|||
|
Check the method signatures to find out which ones are supported.
|
|||
|
* register_fields and profile_fields settings are ignored for now.
|
|||
|
|
|||
|
WARNING: No builtin CSRF protection whatsoever.
|
|||
|
"""
|
|||
|
|
|||
|
default_settings = {
|
|||
|
'create_user_groups': 'user_%(id)s',
|
|||
|
'email_case_sensitive': False,
|
|||
|
'everybody_group_id': None,
|
|||
|
'expiration': 3600,
|
|||
|
'keep_session_onlogin': True,
|
|||
|
'keep_session_onlogout': False,
|
|||
|
'logging_enabled': True,
|
|||
|
'login_after_registration': False,
|
|||
|
'login_email_validate': True,
|
|||
|
'login_userfield': None,
|
|||
|
'logout_onlogout': None,
|
|||
|
'long_expiration': 3600 * 24 * 30,
|
|||
|
'ondelete': 'CASCADE',
|
|||
|
'password_field': 'password',
|
|||
|
'password_min_length': 4,
|
|||
|
'registration_requires_approval': False,
|
|||
|
'registration_requires_verification': False,
|
|||
|
'renew_session_onlogin': True,
|
|||
|
'renew_session_onlogout': True,
|
|||
|
'table_event_name': 'auth_event',
|
|||
|
'table_group_name': 'auth_group',
|
|||
|
'table_membership_name': 'auth_membership',
|
|||
|
'table_permission_name': 'auth_permission',
|
|||
|
'table_user_name': 'auth_user',
|
|||
|
'use_username': False,
|
|||
|
'username_case_sensitive': True
|
|||
|
}
|
|||
|
|
|||
|
default_messages = {
|
|||
|
'add_group_log': 'Group %(group_id)s created',
|
|||
|
'add_membership_log': None,
|
|||
|
'add_permission_log': None,
|
|||
|
'change_password_log': 'User %(id)s Password changed',
|
|||
|
'del_group_log': 'Group %(group_id)s deleted',
|
|||
|
'del_membership_log': None,
|
|||
|
'del_permission_log': None,
|
|||
|
'email_taken': 'This email already has an account',
|
|||
|
'group_description': 'Group uniquely assigned to user %(id)s',
|
|||
|
'has_membership_log': None,
|
|||
|
'has_permission_log': None,
|
|||
|
'invalid_email': 'Invalid email',
|
|||
|
'key_verified': 'Key verified',
|
|||
|
'invalid_login': 'Invalid login',
|
|||
|
'invalid_password': 'Invalid password',
|
|||
|
'invalid_user': 'Invalid user',
|
|||
|
'invalid_key': 'Invalid key',
|
|||
|
'invalid_username': 'Invalid username',
|
|||
|
'logged_in': 'Logged in',
|
|||
|
'logged_out': 'Logged out',
|
|||
|
'login_failed_log': None,
|
|||
|
'login_log': 'User %(id)s Logged-in',
|
|||
|
'logout_log': 'User %(id)s Logged-out',
|
|||
|
'mismatched_password': "Password fields don't match",
|
|||
|
'password_changed': 'Password changed',
|
|||
|
'profile_log': 'User %(id)s Profile updated',
|
|||
|
'profile_updated': 'Profile updated',
|
|||
|
'register_log': 'User %(id)s Registered',
|
|||
|
'registration_pending': 'Registration is pending approval',
|
|||
|
'registration_successful': 'Registration successful',
|
|||
|
'registration_verifying': 'Registration needs verification',
|
|||
|
'username_taken': 'Username already taken',
|
|||
|
'verify_log': 'User %(id)s verified registration key'
|
|||
|
}
|
|||
|
|
|||
|
def __init__(self, db=None, hmac_key=None, signature=True):
|
|||
|
self.db = db
|
|||
|
session = current.session
|
|||
|
auth = session.auth
|
|||
|
self.user_groups = auth and auth.user_groups or {}
|
|||
|
now = current.request.now
|
|||
|
# if we have auth info
|
|||
|
# if not expired it, used it
|
|||
|
# if expired, clear the session
|
|||
|
# else, only clear auth info in the session
|
|||
|
if auth:
|
|||
|
delta = datetime.timedelta(days=0, seconds=auth.expiration)
|
|||
|
if auth.last_visit and auth.last_visit + delta > now:
|
|||
|
self.user = auth.user
|
|||
|
# this is a trick to speed up sessions to avoid many writes
|
|||
|
if (now - auth.last_visit).seconds > (auth.expiration // 10):
|
|||
|
auth.last_visit = now
|
|||
|
else:
|
|||
|
self.user = None
|
|||
|
if session.auth:
|
|||
|
del session.auth
|
|||
|
session.renew(clear_session=True)
|
|||
|
else:
|
|||
|
self.user = None
|
|||
|
if session.auth:
|
|||
|
del session.auth
|
|||
|
|
|||
|
settings = self.settings = Settings(self.__class__.default_settings)
|
|||
|
settings.update(
|
|||
|
extra_fields={},
|
|||
|
hmac_key=hmac_key,
|
|||
|
)
|
|||
|
settings.lock_keys = True
|
|||
|
messages = self.messages = Messages(current.T)
|
|||
|
messages.update(self.default_messages)
|
|||
|
messages.lock_keys = True
|
|||
|
if signature is True:
|
|||
|
self.define_signature()
|
|||
|
else:
|
|||
|
self.signature = signature or None
|
|||
|
|
|||
|
def __validate(self, value, requires):
|
|||
|
if not isinstance(requires, (list, tuple)):
|
|||
|
requires = [requires]
|
|||
|
for validator in requires:
|
|||
|
(value, error) = validator(value)
|
|||
|
if error:
|
|||
|
return (value, error)
|
|||
|
return (value, None)
|
|||
|
|
|||
|
def _get_migrate(self, tablename, migrate=True):
|
|||
|
|
|||
|
if type(migrate).__name__ == 'str':
|
|||
|
return (migrate + tablename + '.table')
|
|||
|
elif not migrate:
|
|||
|
return False
|
|||
|
else:
|
|||
|
return True
|
|||
|
|
|||
|
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 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, migrate=None,
|
|||
|
fake_migrate=None):
|
|||
|
"""
|
|||
|
To be called unless tables are defined manually
|
|||
|
|
|||
|
Examples:
|
|||
|
Use as::
|
|||
|
|
|||
|
# defines all needed tables and table files
|
|||
|
# 'myprefix_auth_user.table', ...
|
|||
|
auth.define_tables(migrate='myprefix_')
|
|||
|
|
|||
|
# defines all needed tables without migration/table files
|
|||
|
auth.define_tables(migrate=False)
|
|||
|
|
|||
|
"""
|
|||
|
|
|||
|
db = self.db
|
|||
|
if migrate is None:
|
|||
|
migrate = db._migrate
|
|||
|
if fake_migrate is None:
|
|||
|
fake_migrate = db._fake_migrate
|
|||
|
|
|||
|
settings = self.settings
|
|||
|
if username is None:
|
|||
|
username = settings.use_username
|
|||
|
else:
|
|||
|
settings.use_username = username
|
|||
|
|
|||
|
if not self.signature:
|
|||
|
self.define_signature()
|
|||
|
if signature is True:
|
|||
|
signature_list = [self.signature]
|
|||
|
elif not signature:
|
|||
|
signature_list = []
|
|||
|
elif isinstance(signature, Table):
|
|||
|
signature_list = [signature]
|
|||
|
else:
|
|||
|
signature_list = signature
|
|||
|
self._table_signature_list = signature_list # Should it defined in __init__ first??
|
|||
|
|
|||
|
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
|
|||
|
# cas_provider Will always be None here but we compare it anyway so subclasses can use our define_tables
|
|||
|
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))
|
|||
|
|
|||
|
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
|
|||
|
if not user_id or self.table_user()[user_id]:
|
|||
|
self.table_event().insert(
|
|||
|
description=str(description % vars), origin=origin, user_id=user_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 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 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 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 and user_id == self.user_id:
|
|||
|
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 and user_id == self.user_id:
|
|||
|
del self.user_groups[group_id]
|
|||
|
return ret
|
|||
|
|
|||
|
def has_membership(self, group_id=None, user_id=None, role=None, cached=False):
|
|||
|
"""
|
|||
|
Checks if user is member of group_id or role
|
|||
|
|
|||
|
NOTE: To avoid database query at each page load that use auth.has_membership, someone can use cached=True.
|
|||
|
If cached is set to True has_membership() check group_id or role only against auth.user_groups variable
|
|||
|
which is populated properly only at login time. This means that if an user membership change during a
|
|||
|
given session the user has to log off and log in again in order to auth.user_groups to be properly
|
|||
|
recreated and reflecting the user membership modification. There is one exception to this log off and
|
|||
|
log in process which is in case that the user change his own membership, in this case auth.user_groups
|
|||
|
can be properly update for the actual connected user because web2py has access to the proper session
|
|||
|
user_groups variable. To make use of this exception someone has to place an "auth.update_groups()"
|
|||
|
instruction in his app code to force auth.user_groups to be updated. As mention this will only work if it
|
|||
|
the user itself that change it membership not if another user, let say an administrator, change someone
|
|||
|
else's membership.
|
|||
|
"""
|
|||
|
if not user_id and self.user:
|
|||
|
user_id = self.user.id
|
|||
|
if cached:
|
|||
|
id_role = group_id or role
|
|||
|
r = (user_id and id_role in self.user_groups.values()) or (user_id and id_role in self.user_groups)
|
|||
|
else:
|
|||
|
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
|
|||
|
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_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 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 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 _update_session_user(self, user):
|
|||
|
if global_settings.web2py_runtime_gae:
|
|||
|
user = Row(self.table_user()._filter_fields(user, id=True))
|
|||
|
delattr(user, self.settings.password_field)
|
|||
|
else:
|
|||
|
user = Row(user)
|
|||
|
for key in list(user.keys()):
|
|||
|
value = user[key]
|
|||
|
if callable(value) or key == self.settings.password_field:
|
|||
|
delattr(user, key)
|
|||
|
current.session.auth = Storage(user=user,
|
|||
|
last_visit=current.request.now,
|
|||
|
expiration=self.settings.expiration,
|
|||
|
hmac_key=web2py_uuid())
|
|||
|
return user
|
|||
|
|
|||
|
def login_user(self, user):
|
|||
|
"""
|
|||
|
Logins the `user = db.auth_user(id)`
|
|||
|
"""
|
|||
|
user = self._update_session_user(user)
|
|||
|
if self.settings.renew_session_onlogin:
|
|||
|
current.session.renew(clear_session=not self.settings.keep_session_onlogin)
|
|||
|
self.user = user
|
|||
|
self.update_groups()
|
|||
|
|
|||
|
def login(self, log=DEFAULT, **kwargs):
|
|||
|
"""
|
|||
|
Login a user
|
|||
|
|
|||
|
Keyword Args:
|
|||
|
username/email/name_of_your_username_field (string) - username
|
|||
|
password/name_of_your_passfield (string) - user's password
|
|||
|
remember_me (boolean) - extend the duration of the login to settings.long_expiration
|
|||
|
"""
|
|||
|
settings = self.settings
|
|||
|
session = current.session
|
|||
|
table_user = self.table_user()
|
|||
|
|
|||
|
if 'username' in table_user.fields or \
|
|||
|
not settings.login_email_validate:
|
|||
|
userfield_validator = IS_NOT_EMPTY(error_message=self.messages.is_empty)
|
|||
|
if not settings.username_case_sensitive:
|
|||
|
userfield_validator = [IS_LOWER(), userfield_validator]
|
|||
|
else:
|
|||
|
userfield_validator = IS_EMAIL(error_message=self.messages.invalid_email)
|
|||
|
if not settings.email_case_sensitive:
|
|||
|
userfield_validator = [IS_LOWER(), userfield_validator]
|
|||
|
|
|||
|
passfield = settings.password_field
|
|||
|
|
|||
|
if log is DEFAULT:
|
|||
|
log = self.messages['login_log']
|
|||
|
|
|||
|
user = None
|
|||
|
|
|||
|
# Setup the default field used for the userfield
|
|||
|
if self.settings.login_userfield:
|
|||
|
userfield = self.settings.login_userfield
|
|||
|
else:
|
|||
|
if 'username' in table_user.fields:
|
|||
|
userfield = 'username'
|
|||
|
else:
|
|||
|
userfield = 'email'
|
|||
|
|
|||
|
# Get the userfield from kwargs and validate it
|
|||
|
userfield_value = kwargs.get(userfield)
|
|||
|
if userfield_value is None:
|
|||
|
raise KeyError('%s not found in kwargs' % userfield)
|
|||
|
|
|||
|
validated, error = self.__validate(userfield_value, userfield_validator)
|
|||
|
|
|||
|
if error:
|
|||
|
return {'errors': {userfield: error}, 'message': self.messages.invalid_login, 'user': None}
|
|||
|
|
|||
|
# Get the user for this userfield and check it
|
|||
|
user = table_user(**{userfield: validated})
|
|||
|
|
|||
|
if user is None:
|
|||
|
return {'errors': {userfield: self.messages.invalid_user},
|
|||
|
'message': self.messages.invalid_login, 'user': None}
|
|||
|
|
|||
|
if (user.registration_key or '').startswith('pending'):
|
|||
|
return {'errors': None, 'message': self.messages.registration_pending, 'user': None}
|
|||
|
elif user.registration_key in ('disabled', 'blocked'):
|
|||
|
return {'errors': None, 'message': self.messages.login_disabled, 'user': None}
|
|||
|
elif (user.registration_key is not None and user.registration_key.strip()):
|
|||
|
return {'errors': None, 'message': self.messages.registration_verifying, 'user': None}
|
|||
|
|
|||
|
# Finally verify the password
|
|||
|
passfield = settings.password_field
|
|||
|
password = table_user[passfield].validate(kwargs.get(passfield, ''))[0]
|
|||
|
|
|||
|
if password == user[passfield]:
|
|||
|
self.login_user(user)
|
|||
|
session.auth.expiration = \
|
|||
|
kwargs.get('remember_me', False) and \
|
|||
|
settings.long_expiration or \
|
|||
|
settings.expiration
|
|||
|
session.auth.remember_me = kwargs.get('remember_me', False)
|
|||
|
self.log_event(log, user)
|
|||
|
return {'errors': None, 'message': self.messages.logged_in,
|
|||
|
'user': {k: user[k] for k in table_user.fields if table_user[k].readable}}
|
|||
|
else:
|
|||
|
self.log_event(self.messages['login_failed_log'], kwargs)
|
|||
|
return {'errors': {passfield: self.messages.invalid_password},
|
|||
|
'message': self.messages.invalid_login, 'user': None}
|
|||
|
|
|||
|
def logout(self, log=DEFAULT, onlogout=DEFAULT, **kwargs):
|
|||
|
"""
|
|||
|
Logs out user
|
|||
|
"""
|
|||
|
settings = self.settings
|
|||
|
session = current.session
|
|||
|
|
|||
|
if onlogout is DEFAULT:
|
|||
|
onlogout = settings.logout_onlogout
|
|||
|
if onlogout:
|
|||
|
onlogout(self.user)
|
|||
|
if log is DEFAULT:
|
|||
|
log = self.messages['logout_log']
|
|||
|
if self.user:
|
|||
|
self.log_event(log, self.user)
|
|||
|
|
|||
|
session.auth = None
|
|||
|
self.user = None
|
|||
|
if settings.renew_session_onlogout:
|
|||
|
session.renew(clear_session=not settings.keep_session_onlogout)
|
|||
|
|
|||
|
return {'errors': None, 'message': self.messages.logged_out, 'user': None}
|
|||
|
|
|||
|
def register(self, log=DEFAULT, **kwargs):
|
|||
|
"""
|
|||
|
Register a user.
|
|||
|
"""
|
|||
|
|
|||
|
table_user = self.table_user()
|
|||
|
settings = self.settings
|
|||
|
|
|||
|
if self.is_logged_in():
|
|||
|
raise AssertionError('User trying to register is logged in')
|
|||
|
|
|||
|
if log is DEFAULT:
|
|||
|
log = self.messages['register_log']
|
|||
|
|
|||
|
if self.settings.login_userfield:
|
|||
|
userfield = self.settings.login_userfield
|
|||
|
elif 'username' in table_user.fields:
|
|||
|
userfield = 'username'
|
|||
|
else:
|
|||
|
userfield = 'email'
|
|||
|
|
|||
|
# Ensure the username field is unique.
|
|||
|
unique_validator = IS_NOT_IN_DB(self.db, table_user[userfield])
|
|||
|
userfield_validator = table_user[userfield].requires
|
|||
|
if userfield_validator is None:
|
|||
|
userfield_validator = unique_validator
|
|||
|
elif isinstance(userfield_validator, (list, tuple)):
|
|||
|
if not any([isinstance(validator, IS_NOT_IN_DB) for validator in
|
|||
|
userfield_validator]):
|
|||
|
if isinstance(userfield_validator, list):
|
|||
|
userfield_validator.append(unique_validator)
|
|||
|
else:
|
|||
|
userfield_validator += (unique_validator, )
|
|||
|
elif not isinstance(userfield_validator, IS_NOT_IN_DB):
|
|||
|
userfield_validator = [userfield_validator, unique_validator]
|
|||
|
table_user[userfield].requires = userfield_validator
|
|||
|
|
|||
|
passfield = settings.password_field
|
|||
|
|
|||
|
try: # Make sure we have our original minimum length
|
|||
|
table_user[passfield].requires[-1].min_length = settings.password_min_length
|
|||
|
except:
|
|||
|
pass
|
|||
|
|
|||
|
key = web2py_uuid()
|
|||
|
if settings.registration_requires_approval:
|
|||
|
key = 'pending-' + key
|
|||
|
|
|||
|
table_user.registration_key.default = key
|
|||
|
|
|||
|
result = table_user.validate_and_insert(**kwargs)
|
|||
|
if result.errors:
|
|||
|
return {'errors': result.errors.as_dict(), 'message': None, 'user': None}
|
|||
|
|
|||
|
user = table_user[result.id]
|
|||
|
|
|||
|
message = self.messages.registration_successful
|
|||
|
|
|||
|
if settings.create_user_groups:
|
|||
|
d = user.as_dict()
|
|||
|
description = self.messages.group_description % d
|
|||
|
group_id = self.add_group(settings.create_user_groups % d, description)
|
|||
|
self.add_membership(group_id, result.id)
|
|||
|
|
|||
|
if self.settings.everybody_group_id:
|
|||
|
self.add_membership(self.settings.everybody_group_id, result)
|
|||
|
|
|||
|
if settings.registration_requires_verification:
|
|||
|
d = {k: user[k] for k in table_user.fields if table_user[k].readable}
|
|||
|
d['key'] = key
|
|||
|
if settings.login_after_registration and not settings.registration_requires_approval:
|
|||
|
self.login_user(user)
|
|||
|
return {'errors': None, 'message': None, 'user': d}
|
|||
|
|
|||
|
if settings.registration_requires_approval:
|
|||
|
user.update_record(registration_key='pending')
|
|||
|
message = self.messages.registration_pending
|
|||
|
elif settings.login_after_registration:
|
|||
|
user.update_record(registration_key='')
|
|||
|
self.login_user(user)
|
|||
|
message = self.messages.logged_in
|
|||
|
|
|||
|
self.log_event(log, user)
|
|||
|
|
|||
|
return {'errors': None, 'message': message,
|
|||
|
'user': {k: user[k] for k in table_user.fields if table_user[k].readable}}
|
|||
|
|
|||
|
def profile(self, log=DEFAULT, **kwargs):
|
|||
|
"""
|
|||
|
Lets the user change his/her profile
|
|||
|
"""
|
|||
|
|
|||
|
table_user = self.table_user()
|
|||
|
settings = self.settings
|
|||
|
table_user[settings.password_field].writable = False
|
|||
|
|
|||
|
if not self.is_logged_in():
|
|||
|
raise AssertionError('User is not logged in')
|
|||
|
|
|||
|
if not kwargs:
|
|||
|
user = table_user[self.user.id]
|
|||
|
return {'errors': None, 'message': None,
|
|||
|
'user': {k: user[k] for k in table_user.fields if table_user[k].readable}}
|
|||
|
|
|||
|
result = self.db(table_user.id == self.user.id).validate_and_update(**kwargs)
|
|||
|
user = table_user[self.user.id]
|
|||
|
|
|||
|
if result.errors:
|
|||
|
return {'errors': result.errors, 'message': None,
|
|||
|
'user': {k: user[k] for k in table_user.fields if table_user[k].readable}}
|
|||
|
|
|||
|
if log is DEFAULT:
|
|||
|
log = self.messages['profile_log']
|
|||
|
|
|||
|
self.log_event(log, user)
|
|||
|
self._update_session_user(user)
|
|||
|
return {'errors': None, 'message': self.messages.profile_updated,
|
|||
|
'user': {k: user[k] for k in table_user.fields if table_user[k].readable}}
|
|||
|
|
|||
|
def change_password(self, log=DEFAULT, **kwargs):
|
|||
|
"""
|
|||
|
Lets the user change password
|
|||
|
|
|||
|
Keyword Args:
|
|||
|
old_password (string) - User's current password
|
|||
|
new_password (string) - User's new password
|
|||
|
new_password2 (string) - Verify the new password
|
|||
|
"""
|
|||
|
settings = self.settings
|
|||
|
messages = self.messages
|
|||
|
|
|||
|
if not self.is_logged_in():
|
|||
|
raise AssertionError('User is not logged in')
|
|||
|
|
|||
|
db = self.db
|
|||
|
table_user = self.table_user()
|
|||
|
s = db(table_user.id == self.user.id)
|
|||
|
|
|||
|
request = current.request
|
|||
|
session = current.session
|
|||
|
passfield = settings.password_field
|
|||
|
|
|||
|
requires = table_user[passfield].requires
|
|||
|
if not isinstance(requires, (list, tuple)):
|
|||
|
requires = [requires]
|
|||
|
requires = list(filter(lambda t: isinstance(t, CRYPT), requires))
|
|||
|
if requires:
|
|||
|
requires[0] = CRYPT(**requires[0].__dict__) # Copy the existing CRYPT attributes
|
|||
|
requires[0].min_length = 0 # But do not enforce minimum length for the old password
|
|||
|
|
|||
|
old_password = kwargs.get('old_password', '')
|
|||
|
new_password = kwargs.get('new_password', '')
|
|||
|
new_password2 = kwargs.get('new_password2', '')
|
|||
|
|
|||
|
validator_old = requires
|
|||
|
validator_pass2 = IS_EQUAL_TO(new_password, error_message=messages.mismatched_password)
|
|||
|
|
|||
|
old_password, error_old = self.__validate(old_password, validator_old)
|
|||
|
new_password2, error_new2 = self.__validate(new_password2, validator_pass2)
|
|||
|
|
|||
|
errors = {}
|
|||
|
if error_old:
|
|||
|
errors['old_password'] = error_old
|
|||
|
if error_new2:
|
|||
|
errors['new_password2'] = error_new2
|
|||
|
if errors:
|
|||
|
return {'errors': errors, 'message': None}
|
|||
|
|
|||
|
current_user = s.select(limitby=(0, 1), orderby_on_limitby=False).first()
|
|||
|
if not old_password == current_user[passfield]:
|
|||
|
return {'errors': {'old_password': messages.invalid_password}, 'message': None}
|
|||
|
else:
|
|||
|
d = {passfield: new_password}
|
|||
|
resp = s.validate_and_update(**d)
|
|||
|
if resp.errors:
|
|||
|
return {'errors': {'new_password': resp.errors[passfield]}, 'message': None}
|
|||
|
if log is DEFAULT:
|
|||
|
log = messages['change_password_log']
|
|||
|
self.log_event(log, self.user)
|
|||
|
return {'errors': None, 'message': messages.password_changed}
|
|||
|
|
|||
|
def verify_key(self,
|
|||
|
key=None,
|
|||
|
ignore_approval=False,
|
|||
|
log=DEFAULT,
|
|||
|
):
|
|||
|
"""
|
|||
|
Verify a given registration_key actually exists in the user table.
|
|||
|
Resets the key to empty string '' or 'pending' if
|
|||
|
setttings.registration_requires_approval is true.
|
|||
|
|
|||
|
Keyword Args:
|
|||
|
key (string) - User's registration key
|
|||
|
"""
|
|||
|
table_user = self.table_user()
|
|||
|
user = table_user(registration_key=key)
|
|||
|
if (user is None) or (key is None):
|
|||
|
return {'errors': {'key': self.messages.invalid_key}, 'message': self.messages.invalid_key}
|
|||
|
|
|||
|
if self.settings.registration_requires_approval:
|
|||
|
user.update_record(registration_key='pending')
|
|||
|
result = {'errors': None, 'message': self.messages.registration_pending}
|
|||
|
else:
|
|||
|
user.update_record(registration_key='')
|
|||
|
result = {'errors': None, 'message': self.messages.key_verified}
|
|||
|
# make sure session has same user.registration_key as db record
|
|||
|
if current.session.auth and current.session.auth.user:
|
|||
|
current.session.auth.user.registration_key = user.registration_key
|
|||
|
if log is DEFAULT:
|
|||
|
log = self.messages['verify_log']
|
|||
|
self.log_event(log, user)
|
|||
|
return result
|