I try to increase security by using a server call to get the current user:
user=anvil.server.call('get_user')
if user is None:
...
And if the user is None, further access is denied.
But for some reason I am worried that somebody could bypass this by simply replacing my server call (which after all is in the Form) with
user='I_can_bypass_this'
And even if I add a check that the user name really must be in the list of users, this leaves the problem of users who correctly guess another username (since it is often an email-address, this is not too difficult).
Surely this just reveals my lack of understanding of how things work?
You cannot effectively secure a web application by making simple checks like that on the client side. As you quite rightly say such checks can be bypassed.
Search on this forum and in the Anvil documents for information and tutorials on how to secure your data. Anvil has a complete log in and user management system built in (itās called the Users service).
Here is one such tutorial, and if you look there are others, too :
As @david.wylie and @stucork have suggested, itās good practice to perform validation checks on both the client-side and server-side. I normally use an alert to display validation errors on the client-side, and raise an Exception server-side in response to validation errors. You might find this how-to guide in the docs helpful.
If youāre using the Users Service, you can also customise your @anvil.server.callable decorators inside your Server Modules to check that a user is logged in:
authenticated_callable = anvil.server.callable(require_user=True)
# This will raise an anvil.users.AuthenticationFailed Exception
# if there is no logged-in user.
@authenticated_callable
def get_data():
return data
We may want to distinguish between some different uses and patterns.
First, there are those cases when we want to get some private data. In that case it makes perfect sense to have a server function where we get the user and fetch the data that user is authorized to get.
Second, there are cases where we not really want to fetch data, but still want to make sure that a user is logged in and/or satisfy certain criteria before an action is allowed (say a button click that executes another function).
I guess I am looking for a pattern that fits the second use case that also avoids too many server calls since this slows down the app. The pattern of getting the user once and then using this object to check every time a button is clicked whether the user is allowed, is vulnerable because one could just eliminate the check and replace it with a random user. This led me to the other and slower extreme: Have a server based check every time the different buttons are clicked.
A better alternative, perhaps, is suggested in this thread: Check the user to get access to the form. This speeds things up: Once in the form I know they are true users and no checks are needed. But it reduces flexibility. I had a form where some buttons were āfreeā and did not require user checks, and some buttons represented āpaidā features and actions. The ācheck-user-before-open-formā pattern works best if all options on the page are paid or free (or require a login or not), and not so well if there is a mix.
There is probably no perfect pattern here and no way around relatively frequent calls to server modules given the use case I have.
Digression: I have always wondered exactly how easy it is to break into tables and so on if they are not server based. The docs say that a determined user can do it. But if it requires a lot of effort - and if security is not absolutely crucial - then perhaps server modules is not required. It boils down to a trade-off between speed and security.
I generally try to avoid too many server calls since it creates slightly slower applications.
from functools import wraps
def admin(func):
@wraps
def wrapper(*args, **kwargs):
admin_role = app_tables.roles.get(name='admin')
user = anvil.users.get_user()
if user is not None and admin_role in user['roles']:
return func(*args, **kwargs)
else:
raise anvil.users.AuthenticationFailed('not an admin')
return wrapper
@anvil.server.callable
@admin
def get_data_for_admin():
user = anvil.users.get_user()
return app_tables.data.search(user=user)
which uses the access_control ideas suggested by @alcampopiano in his post
Generalise the approach for multiple roles
and for a more advanced option to generalise this approach for multiple roles you could do something inspired by this post:
passing args to a decorator
from functools import wraps
class access_control:
def __init__(self, role):
self.role = role
def __call__(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
role_row = app_tables.roles.get(name=self.role)
user = anvil.users.get_user()
if user is not None and role_row in user['roles']:
return func(*args, **kwargs)
else:
raise anvil.users.AuthenticationFailed(f'user does not have the role: {self.role}')
return wrapper
@anvil.server.callable
@access_control('admin')
def get_data_for_admin():
user = anvil.users.get_user()
return app_tables.data.search(user=user)
or as a function rather than a class
def access_control(role):
def decorator(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
role_row = app_tables.roles.get(name=role)
user = anvil.users.get_user()
if user is not None and role_row in user['roles']:
return func(*args, **kwargs)
else:
raise anvil.users.AuthenticationFailed(f'user does not have the role: {role}')
return wrapper
return decorator
You can also further customise the @anvil.server.callable decorator. require_user can take a boolean value, or a function. That function will receive the currently logged-in user (the return value of anvil.users.get_user()). For example, if your Users table has a boolean column called āadminā, you could restrict your server functions to admin users like this: