Authorization
Once a new user is registered, a new group is created to contain the user. The role of the new user is conventionally “user_[id]“ where [id] is the id of the newly created user. The creation of the group can be disabled with
auth.settings.create_user_groups = None
although we do not suggest doing so. Notice that create_user_groups
is not a boolean (although it can be False
) but it defaults to:
auth.settings.create_user_groups="user_%(id)s"
It store a template for the name of the group created for user id
.
Users have membership in groups. Each group is identified by a name/role. Groups have permissions. Users have permissions because of the groups they belong to. By default each user is made member of their own group.
You can also do
auth.settings.everybody_group_id = 5
to make any new user automatically member of group number 5. Here 5 is used as an example and we assume the group was created already.
You can create groups, give membership and permissions via appadmin or programmatically using the following methods:
auth.add_group('role', 'description')
returns the id of the newly created group.
auth.del_group(group_id)
deletes the group with group_id
.
auth.del_group(auth.id_group('user_7'))
deletes the group with role “user_7”, i.e., the group uniquely associated to user number 7.
auth.user_group(user_id)
returns the id of the group uniquely associated to the user identified by user_id
.
auth.add_membership(group_id, user_id)
gives user_id
membership of the group group_id
. If the user_id
is not specified, then web2py assumes the current logged-in user.
auth.del_membership(group_id, user_id)
revokes user_id
membership of the group group_id
. If the user_id
is not specified, then web2py assumes the current logged-in user.
auth.has_membership(group_id, user_id, role)
checks whether user_id
has membership of the group group_id
or the group with the specified role. Only group_id
or role
should be passed to the function, not both. If the user_id
is not specified, then web2py assumes the current logged-in user.
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.
auth.add_permission(group_id, 'name', 'object', record_id)
gives permission “name” (user defined) on the object “object” (also user defined) to members of the group group_id
. If “object” is a tablename then the permission can refer to the entire table by setting record_id
to a value of zero, or the permission can refer to a specific record by specifying a record_id
value greater than zero. When giving permissions on tables, it is common to use a permission name in the set (‘create’, ‘read’, ‘update’, ‘delete’, ‘select’) since these permissions are understood and can be enforced by the CRUD APIs.
If group_id
is zero, web2py uses the group uniquely associated to the current logged-in user.
You can also use auth.id_group(role="...")
to get the id of a group given its name.
auth.del_permission(group_id, 'name', 'object', record_id)
revokes the permission.
auth.has_permission('name', 'object', record_id, user_id)
checks whether the user identified by user_id
has membership in a group with the requested permission.
rows = db(auth.accessible_query('read', db.mytable, user_id)) .select(db.mytable.ALL)
returns all rows of table “mytable” that user user_id
has “read” permission on. If the user_id
is not specified, then web2py assumes the current logged-in user. The accessible_query(...)
can be combined with other queries to make more complex ones. accessible_query(...)
is the only Auth method to require a JOIN, so it does not work on the Google App Engine.
Assuming the following definitions:
>>> from gluon.tools import Auth
>>> auth = Auth(db)
>>> auth.define_tables()
>>> secrets = db.define_table('secret_document', Field('body'))
>>> james_bond = db.auth_user.insert(first_name='James',
last_name='Bond')
Here is an example:
>>> doc_id = db.secret_document.insert(body = 'top secret')
>>> agents = auth.add_group(role = 'Secret Agent')
>>> auth.add_membership(agents, james_bond)
>>> auth.add_permission(agents, 'read', secrets)
>>> print auth.has_permission('read', secrets, doc_id, james_bond)
True
>>> print auth.has_permission('update', secrets, doc_id, james_bond)
False
Decorators
The most common way to check permission is not by explicit calls to the above methods, but by decorating functions so that permissions are checked relative to the logged-in visitor. Here are some examples:
def function_one():
return 'this is a public function'
@auth.requires_login()
def function_two():
return 'this requires login'
@auth.requires_membership('agents')
def function_three():
return 'you are a secret agent'
@auth.requires_permission('read', secrets)
def function_four():
return 'you can read secret documents'
@auth.requires_permission('delete', 'any file')
def function_five():
import os
for file in os.listdir('./'):
os.unlink(file)
return 'all files deleted'
@auth.requires(auth.user_id==1 or request.client=='127.0.0.1', requires_login=True)
def function_six():
return 'you can read secret documents'
@auth.requires_permission('add', 'number')
def add(a, b):
return a + b
def function_seven():
return add(3, 4)
The condition argument of @auth.requires(condition)
can be a callable and unless the condition is simple, it better to pass a callable than a condition since this will be faster, as the condition will only be evaluated if needed. For example
@auth.requires(lambda: check_condition())
def action():
....
@auth.requires
also takes an optional argument requires_login
which defaults to True
. If set to False, it does not require login before evaluating the condition as true/false. The condition can be a boolean value or a function evaluating to boolean.
Note that access to all functions apart from the first one is restricted based on permissions that the visitor may or may not have.
If the visitor is not logged in, then the permission cannot be checked; the visitor is redirected to the login page and then back to the page that requires permissions.
Combining requirements
Occasionally, it is necessary to combine requirements. This can be done via a generic requires
decorator which takes a single argument, a true or false condition. For example, to give access to agents, but only on Tuesday:
@auth.requires(auth.has_membership(group_id='agents') and request.now.weekday()==1)
def function_seven():
return 'Hello agent, it must be Tuesday!'
or equivalently:
@auth.requires(auth.has_membership(role='Secret Agent') and request.now.weekday()==1)
def function_seven():
return 'Hello agent, it must be Tuesday!'
Authorization and CRUD
Using decorators and/or explicit checks provides one way to implement access control.
Another way to implement access control is to always use CRUD (as opposed to SQLFORM
) to access the database and to ask CRUD to enforce access control on database tables and records. This is done by linking Auth
and CRUD with the following statement:
crud.settings.auth = auth
This will prevent the visitor from accessing any of the CRUD functions unless the visitor is logged in and has explicit access. For example, to allow a visitor to post comments, but only update their own comments (assuming crud, auth and db.comment are defined):
def give_create_permission(form):
group_id = auth.id_group('user_%s' % auth.user.id)
auth.add_permission(group_id, 'read', db.comment)
auth.add_permission(group_id, 'create', db.comment)
auth.add_permission(group_id, 'select', db.comment)
def give_update_permission(form):
comment_id = form.vars.id
group_id = auth.id_group('user_%s' % auth.user.id)
auth.add_permission(group_id, 'update', db.comment, comment_id)
auth.add_permission(group_id, 'delete', db.comment, comment_id)
auth.settings.register_onaccept = give_create_permission
crud.settings.auth = auth
def post_comment():
form = crud.create(db.comment, onaccept=give_update_permission)
comments = db(db.comment).select()
return dict(form=form, comments=comments)
def update_comment():
form = crud.update(db.comment, request.args(0))
return dict(form=form)
You can also select specific records (those you have ‘read’ access to):
def post_comment():
form = crud.create(db.comment, onaccept=give_update_permission)
query = auth.accessible_query('read', db.comment, auth.user.id)
comments = db(query).select(db.comment.ALL)
return dict(form=form, comments=comments)
The permissions names enforced by :
crud.settings.auth = auth
are “read”, “create”, “update”, “delete”, “select”, “impersonate”.
Authorization and downloads
The use of decorators and the use of crud.settings.auth
do not enforce authorization on files downloaded by the usual download function
def download(): return response.download(request, db)
If one wishes to do so, one must declare explicitly which “upload” fields contain files that need access control upon download. For example:
db.define_table('dog',
Field('small_image', 'upload'),
Field('large_image', 'upload'))
db.dog.large_image.authorize = lambda record: auth.is_logged_in() and auth.has_permission('read', db.dog, record.id, auth.user.id)
The attribute authorize
of upload field can be None (the default) or a function that decides whether the user is logged in and has permission to ‘read’ the current record. In this example, there is no restriction on downloading images linked by the “small_image” field, but we require access control on images linked by the “large_image” field.
Access Control and Basic Authentication
Occasionally, it may be necessary to expose actions that have decorators that require access control as services; i.e., to call them from a program or script and still be able to use authentication to check for authorization.
Auth enables login via basic authentication:
auth.settings.allow_basic_login = True
With this set, an action like
@auth.requires_login()
def give_me_time():
import time
return time.ctime()
can be called, for example, from a shell command:
wget --user=[username] --password=[password] --auth-no-challenge
http://.../[app]/[controller]/give_me_time
It is also possible to log in by calling auth.basic()
rather than using an @auth
decorator:
def give_me_time():
import time
auth.basic()
if auth.user:
return time.ctime()
else:
return 'Not authorized'
Basic login is often the only option for services (described in the next chapter), but it is disabled by default.
Application Management via privileged users (Experimental)
Normally administrator functions such as defining users and groups are managed by the server administrator. However, you may want a group of privileged users to have administrator rights for a specific application. This is possible with versions after web2py v2.5.1 (Upgrading an existing application requires the new appadmin controller and the new appadmin.html view, copied from the welcome app. Also, apps created prior to web2py v2.6 need the new javascript file in welcome/static/js/web2py.js)
The concept allows different management settings, each of which allows a user group to edit a certain set of tables in this application.
Example: First, create a group (also known as a role) for your privileged users. In this example, it will be called admin. Give a user membership of this role. Second, think of a name to describe this management setting, such as db_admin.
Add the following setting in the model where you created and configured your auth object (probably in the model db):
auth.settings.manager_actions = dict(db_admin=dict(role='admin', heading='Manage Database', tables = db.tables))
A menu item has the URL like below, passing the management setting name as an arg:
URL('appadmin', 'manage', args=['db_admin'])
This URL appears as /appadmin/manage/auth.
Advanced use
This mechanism allows multiple management settings; each additional management setting is just another key defined in auth.settings.manager_actions.
For example, you may want a group of users (such as ‘Super’) to have access to every table in a management setting called “db_admin”, and another group (such as ‘Content Manager’) to have admin access to tables relating to content in a management setting called “content_admin”.
This can be set up like this:
auth.settings.manager_actions = dict(
db_admin=dict(role='Super', heading='Manage Database', tables=db.tables),
content_admin=dict(role='Content Manager', tables=[content_db.articles, content_db.recipes, content_db.comments])
content_mgr_group_v2 = dict(role='Content Manager v2', db=content_db,
tables=['articles', 'recipes', 'comments'],
smartgrid_args=dict(
DEFAULT=dict(maxtextlength=50, paginate=30),
comments=dict(maxtextlength=100, editable=False)
)
)
(The heading key is optional. If missing, a smart default will be used)
You could then make two new menu items with these URLs:
URL('appadmin', 'manage', args=['db_admin'])
URL('appadmin', 'manage', args=['content_admin'])
The management setting called “content_mgr_group_v2” shows some more advanced possibilities. The key smartgrid_args is passed to the smartgrid used to edit or view the tables. Apart from the special key DEFAULT, table names are passed as keys (such as the table called “comments”). The syntax in this example names the tables as a list of strings, using the key db=content_db to specify the database.
Manual Authentication
Some times you want to implement your own logic and do “manual” user login. This can also be done by calling the function:
user = auth.login_bare(username, password)
login_bare
returns user if the user exists and the password is valid, else it returns False. username
is the email if the “auth_user” table does not have a “username” field.
Auth Settings and messages
Here is a list of all parameters that can be customized for Auth
The following must point to a gluon.tools.Mail
object to allow auth
to send emails:
auth.settings.mailer = None
Read more about setting up mail here: Mail and Auth
The following must be the name of the controller that defined the user
action:
auth.settings.controller = 'default'
The following was a very important setting in older web2py versions:
auth.settings.hmac_key = None
Where it was set to something like “sha512:a-pass-phrase” and passed to the CRYPT validator for the “password” field of the auth_user
table, providing the algorithm and a-pass-phrase used to hash the passwords. However, web2py no longers needs this setting because it handles this automatically.
By default, auth also requires a minimum password length of 4. This can be changed:
auth.settings.password_min_length = 4
To disable an action append its name to this list:
auth.settings.actions_disabled = []
For example:
auth.settings.actions_disabled.append('register')
will disable registration.
If you want to receive an email to verify registration set this to True
:
auth.settings.registration_requires_verification = False
To automatically login people after registration, even if they have not completed the email verification process, set the following to True
:
auth.settings.login_after_registration = False
If new registrants must wait for approval before being able to login set this to True
:
auth.settings.registration_requires_approval = False
Approval consists of setting registration_key==''
via appadmin or programmatically.
If you do not want a new group for each new user set the following to False
:
auth.settings.create_user_groups = True
The following settings determine alternative login methods and login forms, as discussed previously:
auth.settings.login_methods = [auth]
auth.settings.login_form = auth
Do you want to allow basic login?
auth.settings.allows_basic_login = False
The following is the URL of the login
action:
auth.settings.login_url = URL('user', args='login')
If the user tried to access the register page but is already logged in, he will be redirected to this URL:
auth.settings.logged_url = URL('user', args='profile')
This must point to the URL of the download action, in case the profile contains images:
auth.settings.download_url = URL('download')
These must point to the URL you want to redirect your users to after the various possible auth
actions (in case there is no referrer):
Note: If your app is based on the standard scaffold app Welcome, you use the auth.navbar. To get the settings below to take effect, you need to edit layout.html and set argument referrer_actions = None. auth.navbar(mode=’dropdown’, referrer_actions=None)
It is also possible to keep referrer_actions for some auth events. For example
auth.navbar(referrer_actions=['login', 'profile'])
If the default behavior is left unchanged, auth.navbar uses the _next URL parameter, and uses that to send the user back to the referring page. However, if navbar’s default auto-referring behavior is changed, the settings below will take effect.
auth.settings.login_next = URL('index')
auth.settings.logout_next = URL('index')
auth.settings.profile_next = URL('index')
auth.settings.register_next = URL('user', args='login')
auth.settings.retrieve_username_next = URL('index')
auth.settings.retrieve_password_next = URL('index')
auth.settings.change_password_next = URL('index')
auth.settings.request_reset_password_next = URL('user', args='login')
auth.settings.reset_password_next = URL('user', args='login')
auth.settings.verify_email_next = URL('user', args='login')
If the visitor is not logger in, and calls a function that requires authentication, the user is redirected to auth.settings.login_url
which defaults to URL('default', 'user/login')
. One can replace this behavior by redefining:
auth.settings.on_failed_authentication = lambda url: redirect(url)
This is the function called for the redirection. The argument url
` passed to this function is the url for the login page.
If the visitor does not have permission to access a given function, the visitor is redirect to the URL defined by
auth.settings.on_failed_authorization = URL('user', args='on_failed_authorization')
You can change this variable and redirect the user elsewhere.
Often on_failed_authorization
is a URL but it can be a function that returns the URL and it will be called on failed authorization.
These are lists of callbacks that should be executed after form validation for each of the corresponding action before any database IO:
auth.settings.login_onvalidation = []
auth.settings.register_onvalidation = []
auth.settings.profile_onvalidation = []
auth.settings.retrieve_password_onvalidation = []
auth.settings.reset_password_onvalidation = []
Each callback must be a function that takes the form
object and it can modify the attributes of the form object before database IO is performed.
These are lists of callbacks that should be executed after the database IO is performed and before redirection:
auth.settings.login_onaccept = []
auth.settings.register_onaccept = []
auth.settings.profile_onaccept = []
auth.settings.verify_email_onaccept = []
Here is an example:
auth.settings.register_onaccept.append(lambda form: mail.send(to='you@example.com', subject='new user',
message='new user email is %s'%form.vars.email))
You can enable captcha for any of the auth
actions:
auth.settings.captcha = None
auth.settings.login_captcha = None
auth.settings.register_captcha = None
auth.settings.retrieve_username_captcha = None
auth.settings.retrieve_password_captcha = None
If the .captcha
settings points to a gluon.tools.Recaptcha
, all forms for which the corresponding option (like .login_captcha
) is set to None
will have a captcha, while those for which the corresponding option is set to False
will not. If, instead, .captcha
is set to None
, only those form who have a corresponding option set to a gluon.tools.Recaptcha
object will have captcha and the others will not.
This is the login session expiration time:
auth.settings.expiration = 3600 # seconds
You can change the name of the password field (in Firebird for example “password” is a keyword and cannot be used to name a field):
auth.settings.password_field = 'password'
Normally the login form tries to validate an email. This can be disabled by changing this setting:
auth.settings.login_email_validate = True
Do you want to show the record id in the edit profile page?
auth.settings.showid = False
For custom forms you may want to disable automatic error notification in forms:
auth.settings.hideerror = False
Also for custom forms you can change the style:
auth.settings.formstyle = 'table3cols'
(it can be “bootstrap3_inline”, “table3cols”, “table2cols”, “divs” and “ul”; for all options, see gluon/sqlhtml.py)
And you can set the separator for auth-generated forms:
auth.settings.label_separator = ':'
By default the login form gives the option to extend the login via “remember me” option. The expiration time can be changed or the option disabled via these settings:
auth.settings.long_expiration = 3600*24*30 # one month
auth.settings.remember_me_form = True
You can also customize the following messages whose use and context should be obvious:
auth.messages.submit_button = 'Submit'
auth.messages.verify_password = 'Verify Password'
auth.messages.delete_label = 'Check to delete:'
auth.messages.function_disabled = 'Function disabled'
auth.messages.access_denied = 'Insufficient privileges'
auth.messages.registration_verifying = 'Registration needs verification'
auth.messages.registration_pending = 'Registration is pending approval'
auth.messages.login_disabled = 'Login disabled by administrator'
auth.messages.logged_in = 'Logged in'
auth.messages.email_sent = 'Email sent'
auth.messages.unable_to_send_email = 'Unable to send email'
auth.messages.email_verified = 'Email verified'
auth.messages.logged_out = 'Logged out'
auth.messages.registration_successful = 'Registration successful'
auth.messages.invalid_email = 'Invalid email'
auth.messages.unable_send_email = 'Unable to send email'
auth.messages.invalid_login = 'Invalid login'
auth.messages.invalid_user = 'Invalid user'
auth.messages.is_empty = "Cannot be empty"
auth.messages.mismatched_password = "Password fields don't match"
auth.messages.verify_email = ...
auth.messages.verify_email_subject = 'Password verify'
auth.messages.username_sent = 'Your username was emailed to you'
auth.messages.new_password_sent = 'A new password was emailed to you'
auth.messages.password_changed = 'Password changed'
auth.messages.retrieve_username = 'Your username is: %(username)s'
auth.messages.retrieve_username_subject = 'Username retrieve'
auth.messages.retrieve_password = 'Your password is: %(password)s'
auth.messages.retrieve_password_subject = 'Password retrieve'
auth.messages.reset_password = ...
auth.messages.reset_password_subject = 'Password reset'
auth.messages.invalid_reset_password = 'Invalid reset password'
auth.messages.profile_updated = 'Profile updated'
auth.messages.new_password = 'New password'
auth.messages.old_password = 'Old password'
auth.messages.group_description = 'Group uniquely assigned to user %(id)s'
auth.messages.register_log = 'User %(id)s Registered'
auth.messages.login_log = 'User %(id)s Logged-in'
auth.messages.logout_log = 'User %(id)s Logged-out'
auth.messages.profile_log = 'User %(id)s Profile updated'
auth.messages.verify_email_log = 'User %(id)s Verification email sent'
auth.messages.retrieve_username_log = 'User %(id)s Username retrieved'
auth.messages.retrieve_password_log = 'User %(id)s Password retrieved'
auth.messages.reset_password_log = 'User %(id)s Password reset'
auth.messages.change_password_log = 'User %(id)s Password changed'
auth.messages.add_group_log = 'Group %(group_id)s created'
auth.messages.del_group_log = 'Group %(group_id)s deleted'
auth.messages.add_membership_log = None
auth.messages.del_membership_log = None
auth.messages.has_membership_log = None
auth.messages.add_permission_log = None
auth.messages.del_permission_log = None
auth.messages.has_permission_log = None
auth.messages.label_first_name = 'First name'
auth.messages.label_last_name = 'Last name'
auth.messages.label_username = 'Username'
auth.messages.label_email = 'E-mail'
auth.messages.label_password = 'Password'
auth.messages.label_registration_key = 'Registration key'
auth.messages.label_reset_password_key = 'Reset Password key'
auth.messages.label_registration_id = 'Registration identifier'
auth.messages.label_role = 'Role'
auth.messages.label_description = 'Description'
auth.messages.label_user_id = 'User ID'
auth.messages.label_group_id = 'Group ID'
auth.messages.label_name = 'Name'
auth.messages.label_table_name = 'Table name'
auth.messages.label_record_id = 'Record ID'
auth.messages.label_time_stamp = 'Timestamp'
auth.messages.label_client_ip = 'Client IP'
auth.messages.label_origin = 'Origin'
auth.messages.label_remember_me = "Remember me (for 30 days)"
add|del|has
membership logs allow the use of “%(user_id)s” and “%(group_id)s”. add|del|has
permission logs allow the use of “%(user_id)s”, “%(name)s”, “%(table_name)s”, and “%(record_id)s”.