Setting Custom Permissions on Frappe Doctypes: Enabling Multi-Tenant Flexibility

Setting Custom Permissions on Frappe Doctypes: Enabling Multi-Tenant Flexibility

Learn how to implement custom permissions in Frappe Doctypes to enable precise control over user access. This guide covers dynamic permission logic, field-level restrictions, and multi-tenant flexibility for scalable applications. Master advanced techniques to build secure and adaptable systems in Frappe!

CleanShot_2024-12-22_at_19.07.36.png

Frappe, a powerful web application framework, provides built-in tools for managing permissions using the Role and Permission Manager. However, for complex scenarios, such as enabling multi-tenant flexibility, customizing permissions at the code level offers a more robust and adaptable solution. This article explores the methodology for setting custom permissions for Frappe Doctypes, using the User Doctype as an example.

Overview of Custom Permission Implementation

Key Requirements

  1. Controlled Access: Only specific roles, such as System Manager, can create or manage user records.

  2. Guest User Support: Guest users must be able to create their user records through a registration process.

  3. Field-Level Restrictions: Apply granular permissions to restrict access to specific fields.

  4. Multi-Tenant Support: Enable permission logic that scales across tenants in the application.

Steps to Implement Custom Permissions

1. Overriding the Default User Class

To implement custom permissions, the default User class is overridden with a custom class (./apps/frappe_toolkit/frappe_toolkit/overrides/user.py. This allows you to define the following:

has_permission Method: Controls access for specific actions like read, write, and create.

get_permission_query_conditions Method: Dynamically filters data access based on session or user context.

2. Defining Custom Permission Logic

Example: has_permission Method

The has_permission method is overridden to allow guest users to create user records. The logic is as follows:

Guest Users: If the session user is a guest and not logged in, permission is granted to create a user record.

Administrators: Administrators are granted full access.

Default Denial: All other cases deny permission.

def has_permission(self, permtype="read"):
    """
    Custom permission logic to allow or restrict access based on user and permission type.
    """
    frappe.logger().debug(
        f"CustomUser.has_permission called for doc: {self.name}, User: {frappe.session.user}, PermType: {permtype}"
    )

    # Allow if the user is not logged in (e.g., creating a new user)
    if not frappe.session.user or frappe.session.user == "Guest":
        frappe.logger().debug("Permission granted for unauthenticated user (Guest).")
        return permtype == "create"

    user = frappe.session.user

    # Allow if the user is Administrator
    if user == "Administrator":
        frappe.logger().debug("Permission granted for Administrator.")
        return True

    # Allow if the logged-in user matches the document
    if user == self.name:
        frappe.logger().debug(f"Permission granted for matching user: {user}")
        return permtype in ["read", "write"]

    # Deny for all other cases
    frappe.logger().debug(f"Permission denied for user: {user} on doc: {self.name}")
    return False

Example: get_permission_query_conditions

The getpermissionquery_conditions method restricts access to records:

• Non-administrators can only view their own records.

• All other access is denied.

def get_permission_query_conditions(self, user):
    """
    Custom query conditions to restrict access to user documents.
    """
    frappe.logger().debug(f"CustomUser.get_permission_query_conditions called for user: {user}")
    if user == "Administrator":
        return ""
    else:
        # Restrict access to the logged-in user only
        condition = f"(`tabUser`.name = {frappe.db.escape(user)})"
        frappe.logger().debug(f"Generated condition: {condition}")
        return condition

3. Field-Level Permissions

Field-level restrictions are implemented using a custom method, such as applyfieldlevelreadpermission. This ensures that sensitive fields are hidden or read-only based on user roles or contexts.

def apply_fieldlevel_read_permissions(self):
    """
    Customize field-level read permissions to restrict fields for all users, 
    including the valid user or Administrator.
    """
    frappe.logger().debug(f"Applying field-level read permissions for: {self.name}")
    user = frappe.session.user

    if user == self.name or user == "Administrator":
        # Allow full access for the user or Administrator
        frappe.logger().debug("Full field-level access granted.")
        return

    # Restrict fields for non-matching users
    restricted_fields = [
        # "firebase_uid",
        # "roles",
        # "api_key",
        "api_secret",
        "reset_password_key",
        "social_logins",
        "last_reset_password_key_generated_on",
        "last_password_reset_date",
        "last_ip",
        "restrict_ip",
        "last_active",
        "last_login",
        "simultaneous_sessions"
    ]

    for field in restricted_fields:
        if field in self.__dict__:
            frappe.logger().debug(f"Restricting field: {field}")
            del self.__dict__[field]

4. Validation for Consistency

The validate method is used to enforce business logic and ensure consistency in the user records before saving them to the database.

def validate(self):
    """
    Override validate to add any custom validation logic.
    """
    frappe.logger().debug(f"CustomUser.validate called for user: {self.name}")
    super().validate()

    # Additional custom validation logic
    if not self.enabled and self.name in STANDARD_USERS:
        frappe.throw(_("User {0} cannot be disabled").format(self.name))

    # Allow user creation without being logged in
    if frappe.session.user == "Guest" or not frappe.session.user:
        frappe.logger().debug("Validation allowed for unauthenticated user (Guest).")
        return

Advantages of Custom Permission Implementation

Enhanced Flexibility

Custom permissions allow developers to bypass the limitations of the Role and Permission Manager, offering more granular control over access.

Improved Multi-Tenancy

The methodology scales well in multi-tenant architectures, as permissions can be dynamically adjusted based on the tenant’s configuration.

Field-Level Security

Sensitive fields can be protected using programmatic restrictions, ensuring better data privacy and compliance.

Efficient Development Workflow

By centralizing permission logic in code, developers can create, test, and maintain complex permission systems without manual configuration.

Conclusion

Implementing custom permissions in Frappe Doctypes provides a powerful approach to manage access in multi-tenant applications. By leveraging custom classes, permission methods, and field-level restrictions, developers can build scalable and secure applications tailored to unique business requirements.

Let's Connect

Built with 1pg.notion.lol