Authorization and RBAC

Authentication answers the question, “Who is the user?” while authorization determines, “Does the authenticated user have the required rights?”.

By adding an authentication provider via XLWINGS_AUTH_PROVIDERS, only logged in users will be able to call Custom Functions and Custom Scripts.

If you want to further differentiate between users, you need to implement authorization, either on a global level or for specific custom functions or custom scripts. You can do this by using role-based access-control (RBAC), which allows you to restrict access to only users who possess the required roles.

Global authorization

To add global authorization, you can implement the User.is_authorized() method under app/models/user.py. For example, if you have set XLWINGS_AUTH_ENTRAID_MULTITENANT=true but only want to allow users with the domain mydomain.com to run custom scripts and custom functions, you could do:

class User(BaseUser):
    async def is_authorized(self):
        return self.domain == "mydomain.com" if self.domain else False

Or, for role-based access control (RBAC), i.e., only authorize users with certain roles, you could write:

class User(BaseUser):
    async def is_authorized(self):
        return await self.has_required_roles(["xlwings.admin", "xlwings.user"])

Function-specific RBAC

If you have individual custom functions or custom scripts where you want to require specific roles, you can use required_roles like so:

import xlwings as xw
from xlwings.server import script, func

@script(required_roles=["xlwings.admin", "xlwings.user"])
def myscript(book: xw.Book):
    ...

@func(required_roles=["xlwings.admin", "xlwings.user"])
def myfunction(name: str):
    ...

Custom user model

To customize how xlwings Server performs authorization, you can modify the User class in app/models/user.py. The User class inherits from BaseUser, which offers the following attributes by default:

id
name
domain
email
roles
claims
ip_address

For example, if you use Entra ID, all claims are added in the form of a dictionary to the claims attribute. So if you want to turn a specific claim into a direct user attribute, you could write:

class User(BaseUser):
    @property
    def mycompany_id(self) -> str:
        return self.claims.get("mycompany_id")

Another example is the handling of roles: many identity providers (such as Entra ID) support role definitions for users. However, if you prefer to use the identity provider solely for authentication while managing user roles through other means (like a database), you can customize this behavior by implementing the roles property.

# Fake database to demonstrate the concept
db = {"1000": ["admin"], "1001": ["user"]}

class User(BaseUser):
    @property
    def roles(self) -> list[str]:
        return db[self.id]

If your needs are completely different from the BaseUser and you want to implement the User class from scratch, inherit from BaseModel instead of BaseUser:

from pydantic import BaseModel

class User(BaseModel):
    id: str
    name: str

This will define the user model as a Pydantic model. Alternatively, you could also implement it as a dataclass or an SQLAlchemy model, etc.