Simple Permissions Solution

There are lots of ways to handle permissions (This example being one), so I thought I’d share a very simple solution I’ve used in a couple of places:

Create a permissions table with a user column linked to the Users table and a True/False column for each permission you want:

Create auth_required and permission_required decorators. I tend to do that in a separate decorators server module:

    import functools
    import anvil.users
    from anvil.tables import app_tables

    def auth_required(func):

        @functools.wraps(func)
        def wrapped(*args, **kwargs):
            if not anvil.users.get_user():
                raise ValueError("Authentication required")
            else:
                return func(*args, **kwargs)

        return wrapped

    def permission_required(permissions):
        def permission_required_decorator(func):

            @functools.wraps(func)
            def wrapped(*args, **kwargs):
                user = anvil.users.get_user()
                
                #  Handle the fact that permissions might be a string or a list
                if isinstance(permissions, str):
                    all_permissions = [permissions]
                else:
                    all_permissions = permissions
                
                #  Get the relevant data table record or create one if it doesn't exist
                permissions_row = (app_tables.permissions.get(user=user)) or (
                    app_tables.permissions.add_row(user=user)
                )
                
                #  Raise an error if any of the required permissions is missing
                has_permission = all(
                    [permissions_row[permission] for permission in all_permissions]
                )
                if not has_permission:
                    raise ValueError("Permission required")
                else:
                    return func(*args, **kwargs)

            return wrapped
        return permission_required_decorator

Finally, use the decorators on any server function where permission is required. You can pass it a single permission or a list if more than one is necessary (using the same names as the True/False columns in your permissions table):

    import anvil.server
    from decorators import permission_required

    @anvil.server.callable
    @permission_required("access_ui")
    @auth_required
    def some_function:
        do_something()

    @anvil.server.callable
    @permission_required(["access_ui", "edit_redacted_things"])
    @auth_required
    def some_other_function:
        do_something_else()

The order of the decorators mattters here - first check that the user exists and is authorised, then check that it has the necessary permissions and only then make the function callable.

6 Likes

Note there used to be an issue whereby you needed to put the name of the function as a parameter to the first decorator when stacking decorators like this, eg :

@anvil.server.callable("funcname")
...other decorators ..
def funcname():
  etc.

Not sure if that still needs to be done?

I don’t think so, but I’d forgotten about that one, so I’ll bear it in mind. Ta!

(I have this working in several apps without any problem - possibly the issue has been fixed?)

Here’s the link to the topic where @daviesian discusses it -

I have this working in several apps without any problem

[Nerd hat: ON] Your code works because you use the @functools.wraps() decorator, which returns a wrapper function with the correct __name__. This causes @anvil.server.callable's default behaviour to work, so you don’t have to specify the name manually.

(Anvil’s autocompleter is not [yet?] clever enough to work out whether a decorator preserves __name__ or not. So it happily assumes all decorators preserve __name__, whether or not they actually do!)

3 Likes

There was a point in time when I knew that and used the functools library for precisely that reason, but I have lost many grey cells since then.

Thanks so much for posting this! I needed to do some fairly app specific permissions checking, and was getting errors because I wasn’t using functools.wraps. Everything works nicely after adding that.

I am so lost on this. :frowning_face:
Trying - Authorisation — Anvil Extras documentation

What’s your question? That error is exactly what the decorator raises if the server function is called without there being a logged in user.

How was the function called? Was a user logged in at the time?

1 Like

Thank you Owen for responding,
I placed sensitive_server_function() under the →

class StoringDisplayingData(StoringDisplayingDataTemplate):
  def __init__(self, **properties):

while not being logged in so this True the error is appropriate, I’m just confused on how to allow certain pages to be access by users with the role of admin.

right now I’m using this -

def require_account():
  user = anvil.users.get_user()
  if user:
    return user
  user = anvil.users.login_with_form(allow_cancel=True)
  form = get_form()
  form.set_account_state(user)
  return user   

to make sure a user is present to access the pages - but I’m stuck on how to only permit admins to certain pages

Well, I’m going this route, I hope it’s the correct way of doing thing