Permissions

Flexible, transparent role-based access control (RBAC) system for FastHTML applications

Imports and utils

Quick Start

Ship Kit’s permissions module provides simple, transparent access control for your FastHTML applications:

from ship_kit.permissions import *
from fasthtml.common import *

app, rt = fast_app()

# 1. Protect routes with simple functions
@rt("/admin")
def get(req, sess):
    if not require_role("admin", req, sess):
        return RedirectResponse('/login', status_code=303)
    return "Welcome to admin area!"

# 2. Or use decorators for cleaner code
@rt("/moderator/dashboard")
@role_required("moderator")
def get(req, sess):
    return "Moderator Dashboard"

# 3. Check granular permissions
@rt("/users/delete")
@permission_required("delete_users")
def post(req, sess):
    # Delete user logic
    pass

# 4. Manual permission checks for conditional UI
@rt("/api/sensitive")
def get(req, sess):
    if not require_permission("view_sensitive_data", req, sess):
        return JSONResponse({"error": "Forbidden"}, status_code=403)
    return {"data": "sensitive information"}

That’s it! Your routes are now protected with role-based access control.

Overview

This module provides a complete RBAC (Role-Based Access Control) system:

Core Functions

Function Purpose When to Use
require_auth Check if user is authenticated Manual auth checks
require_role Check if user has specific role Manual role checks
require_permission Check if user has permission Manual permission checks

Decorators

Decorator Purpose When to Use
@auth_required Require authentication Protect any authenticated route
@role_required Require specific role Admin/moderator areas
@permission_required Require specific permission Granular access control

Permission Management

Function Purpose When to Use
get_user_permissions Get all user permissions Display user capabilities
register_permission Register new permission Add custom permissions
set_role_permissions Set permissions for role Configure roles
clear_permission_cache Clear cached permissions After role changes

Default Configuration

Launch Kit provides sensible defaults for role hierarchy and permissions:

Core Permission Functions

Simple boolean functions for checking authentication, roles, and permissions:


source

require_auth

 require_auth (req, sess)

*Check if user is authenticated.

This is the simplest permission check - just verifies that a user is logged in.*

Type Details
req The FastHTML Request object
sess The FastHTML Session object
Returns bool True if user is authenticated

source

check_role_hierarchy

 check_role_hierarchy (user_role:Optional[str], required_role:str)

*Check if user’s role meets or exceeds the required role in hierarchy.

Uses ROLE_HIERARCHY to determine if a user’s role has sufficient privileges. For example, an ‘admin’ can access ‘moderator’ areas.*

Type Details
user_role Optional The user’s current role
required_role str The required role
Returns bool True if user role >= required role

source

require_role

 require_role (role:str, req, sess)

*Check if user has the required role or higher in the hierarchy.

Uses role hierarchy so admins can access moderator areas, etc.*

Type Details
role str The required role
req The FastHTML Request object
sess The FastHTML Session object
Returns bool True if user has the required role or higher

source

get_user_permissions

 get_user_permissions (user:Dict[str,Any])

*Get all permissions for a user based on their role.

Returns a set of permission strings. Admins get ’’ which means all permissions.

Type Details
user Dict The user dictionary from session
Returns Set Set of permission strings

source

has_permission

 has_permission (user:Dict[str,Any], permission:str)

*Check if user has a specific permission.

Handles the special case where admins have ’’ meaning all permissions.

Type Details
user Dict The user dictionary
permission str The permission to check
Returns bool True if user has permission

source

require_permission

 require_permission (permission:str, req, sess)

*Check if user has a specific permission.

This is for granular permission checking beyond roles.*

Type Details
permission str The required permission
req The FastHTML Request object
sess The FastHTML Session object
Returns bool True if user has permission

Permission Decorators

Decorators provide a clean way to protect FastHTML routes:


source

auth_required

 auth_required (func:Callable)

*Decorator that requires authentication for a route.

Redirects to /login if user is not authenticated. Works with FastHTML route functions that accept req and sess parameters.

Example: @rt(‘/dashboard’) @auth_required def get(req, sess): return “Dashboard content”*

Type Details
func Callable The route function to protect
Returns Callable The wrapped function

source

role_required

 role_required (role:str)

*Decorator that requires a specific role for a route.

Returns 403 Forbidden if user doesn’t have the required role.

Example: @rt(‘/admin’) @role_required(‘admin’) def get(req, sess): return “Admin panel”*

Type Details
role str The required role
Returns Callable Decorator function

source

permission_required

 permission_required (permission:str)

*Decorator that requires a specific permission for a route.

Returns 403 Forbidden if user doesn’t have the required permission.

Example: @rt(‘/users/delete’) @permission_required(‘delete_users’) def post(req, sess, user_id: int): # Delete user logic pass*

Type Details
permission str The required permission
Returns Callable Decorator function

Permission Management

Functions for managing and configuring permissions:


source

register_permission

 register_permission (name:str, description:Optional[str]=None)

*Register a new permission in the system.

This is optional but helps with documentation and validation.*

Type Default Details
name str Permission identifier
description Optional None Human-readable description
Returns None

source

get_permissions_for_role

 get_permissions_for_role (role:str)

Get all permissions assigned to a role.

Type Details
role str The role name
Returns Set Set of permissions

source

set_role_permissions

 set_role_permissions (role:str, permissions:Union[Set[str],List[str]])

Set permissions for a role, replacing any existing permissions.

Type Details
role str The role name
permissions Union Permissions to assign
Returns None

source

add_role_permission

 add_role_permission (role:str, permission:str)

Add a single permission to a role.

Type Details
role str The role name
permission str Permission to add
Returns None

source

remove_role_permission

 remove_role_permission (role:str, permission:str)

Remove a single permission from a role.

Type Details
role str The role name
permission str Permission to remove
Returns None

Session Integration

Utilities for caching permissions in the session for performance:


source

clear_permission_cache

 clear_permission_cache (sess)

*Clear cached permissions from session.

Call this after changing a user’s role or permissions.*

Type Details
sess The FastHTML Session object
Returns None

Examples

Basic Authentication Protection

from fasthtml.common import *
from ship_kit.permissions import *

app, rt = fast_app()

# Simple authentication check
@rt('/dashboard')
@auth_required
def get(req, sess):
    user = get_user_from_session(sess)
    return Div(
        H1(f"Welcome {user['username']}!"),
        P("This is your dashboard.")
    )

Role-Based Access Control

# Admin-only area
@rt('/admin')
@role_required('admin')
def get(req, sess):
    return Div(
        H1("Admin Panel"),
        P("Only administrators can see this.")
    )

# Moderator area (admins can also access)
@rt('/moderate')
@role_required('moderator')  
def get(req, sess):
    return Div(
        H1("Moderation Queue"),
        P("Moderators and admins can see this.")
    )

Granular Permission Checks

# Check specific permission
@rt('/users/{user_id}/delete', methods=['POST'])
@permission_required('delete_users')
def delete_user(req, sess, user_id: int):
    # Delete user logic here
    return {"status": "deleted", "user_id": user_id}

# Manual permission check for conditional UI
@rt('/users/{user_id}')
@auth_required
def get(req, sess, user_id: int):
    user = get_user_from_session(sess)
    can_delete = has_permission(user, 'delete_users')
    
    return Div(
        H1(f"User Profile #{user_id}"),
        Button(
            "Delete User",
            hx_post=f"/users/{user_id}/delete",
            hx_confirm="Are you sure?"
        ) if can_delete else None
    )

Custom Permission Configuration

# Register custom permissions
register_permission('export_data', 'Export system data to CSV')
register_permission('view_analytics', 'View analytics dashboard')

# Create a custom role
set_role_permissions('analyst', {
    'read_all_data',
    'view_analytics', 
    'export_data'
})

# Add permission to existing role
add_role_permission('moderator', 'view_analytics')

# Remove permission from role
remove_role_permission('user', 'delete_own_data')

API Endpoints with Permissions

# JSON API with permission checks
@rt('/api/users')
def get(req, sess):
    if not require_permission('read_all_data', req, sess):
        return JSONResponse(
            {"error": "Forbidden", "message": "Insufficient permissions"},
            status_code=403
        )
    
    # Return user data
    return JSONResponse({"users": []})

# Using decorators with JSON responses
@rt('/api/admin/stats')
@role_required('admin')
def get(req, sess):
    # Admin-only statistics
    return JSONResponse({
        "total_users": 1000,
        "active_sessions": 42
    })

Testing Permissions

Interactive Demo

# Complete demo app showing all permission features
from fasthtml.common import *
from ship_kit.auth import user_auth_before
from ship_kit.permissions import *

# Configure auth beforeware
beforeware = Beforeware(
    user_auth_before,
    skip=['/login', '/public', '/']
)

app, rt = fast_app(before=beforeware)

# Mock user database
users = {
    'admin@example.com': {'id': 1, 'email': 'admin@example.com', 'role': 'admin'},
    'mod@example.com': {'id': 2, 'email': 'mod@example.com', 'role': 'moderator'},
    'user@example.com': {'id': 3, 'email': 'user@example.com', 'role': 'user'}
}

# Public home page
@rt('/')
def get():
    return Div(
        H1("Permissions Demo"),
        P("Test different user roles:"),
        Ul(
            Li("admin@example.com - Admin role"),
            Li("mod@example.com - Moderator role"),
            Li("user@example.com - User role")
        ),
        A("Login", href="/login", cls="button")
    )

# Login page
@rt('/login')
def get():
    return Div(
        H2("Login"),
        Form(
            Input(name="email", type="email", placeholder="Email", required=True),
            Button("Login", type="submit"),
            method="post"
        )
    )

@rt('/login', methods=['POST'])
async def post(req, sess):
    form = await req.form()
    email = form.get('email')
    if email in users:
        sess['auth'] = True
        sess['user'] = users[email]
        return RedirectResponse('/dashboard', status_code=303)
    return "Invalid email"

# User dashboard - requires authentication
@rt('/dashboard')
@auth_required
def get(req, sess):
    user = get_user_from_session(sess)
    permissions = get_user_permissions(user)
    
    return Div(
        H1(f"Welcome {user['email']}"),
        P(f"Role: {user['role']}"),
        H3("Your Permissions:"),
        Ul(*[Li(perm) for perm in sorted(permissions)]) if '*' not in permissions else P("All permissions"),
        H3("Test Areas:"),
        Ul(
            Li(A("Admin Area", href="/admin")),
            Li(A("Moderator Area", href="/moderate")),
            Li(A("Delete Users", href="/users/delete"))
        ),
        A("Logout", href="/logout")
    )

# Admin only area
@rt('/admin')
@role_required('admin')
def get(req, sess):
    return Div(
        H1("Admin Area"),
        P("Only admins can see this!"),
        A("Back to Dashboard", href="/dashboard")
    )

# Moderator area (admins can also access)
@rt('/moderate')
@role_required('moderator')
def get(req, sess):
    user = get_user_from_session(sess)
    return Div(
        H1("Moderator Area"),
        P(f"Welcome {user['role']}! Moderators and admins can see this."),
        A("Back to Dashboard", href="/dashboard")
    )

# Permission-based access
@rt('/users/delete')
@permission_required('delete_users')
def get(req, sess):
    return Div(
        H1("Delete Users"),
        P("This requires the 'delete_users' permission."),
        P("Only admins have this by default."),
        A("Back to Dashboard", href="/dashboard")
    )

# Logout
@rt('/logout')
def get(sess):
    sess.clear()
    return RedirectResponse('/', status_code=303)

# Run the demo
from fasthtml.jupyter import JupyUvi
server = JupyUvi(app)
# View the app right here in the notebook by uncommenting the line below
from fasthtml.jupyter import HTMX
# HTMX()
# Stop the server gracefully
# Note: Always run this after testing to clean up otherwise there will be a dangling thread
# https://fastht.ml/docs/tutorials/jupyter_and_fasthtml.html#graceful-shutdowns
print("Stopping server...")
server.stop()
Stopping server...

Best Practices

1. Use Decorators for Clean Code

# Good: Clean and declarative
@rt('/admin')
@role_required('admin')
def get(req, sess):
    return admin_panel()

# Avoid: Manual checks in every route
@rt('/admin')
def get(req, sess):
    if not require_role('admin', req, sess):
        return RedirectResponse('/login')  
    return admin_panel()

2. Role Hierarchy for Flexibility

# Admins automatically get access to moderator areas
@role_required('moderator')  # Admins can also access

3. Granular Permissions for Sensitive Operations

# Use specific permissions for dangerous operations
@permission_required('delete_all_data')  # More specific than @role_required('admin')

4. Clear Permission Cache After Role Changes

def promote_to_admin(user_id, sess):
    # Update user role in database
    update_user_role(user_id, 'admin')
    # Clear cached permissions
    clear_permission_cache(sess)

5. Combine with Beforeware for Global Auth

# Use beforeware for site-wide auth
beforeware = Beforeware(user_auth_before, skip=['/login', '/public'])

# Then use decorators for specific permissions
@role_required('admin')

Security Considerations

🔒 Permission Design

Practice Implementation
Principle of Least Privilege Give users minimum required permissions
Role Separation Don’t combine unrelated permissions
Audit Trail Log permission changes and access attempts
Regular Review Periodically review role assignments

🛡️ Implementation Security

Practice Implementation
Session Security Use secure session configuration
CSRF Protection Verify state-changing operations
Rate Limiting Limit permission check attempts
Error Handling Don’t leak permission info in errors

Summary

Ship Kit’s permissions module provides:

  • Simple API - Boolean functions and clean decorators
  • Role Hierarchy - Admins can access moderator areas automatically
  • Granular Permissions - Beyond roles for specific operations
  • FastHTML Native - Works seamlessly with req/sess patterns
  • Transparent - No hidden middleware or magic
  • Flexible - Easy to extend with custom roles and permissions
  • Performance - Optional session caching for efficiency

The module follows Ship Kit’s philosophy of being simple, transparent, and flexible while providing all the features needed for production applications.

Breaking Changes in v2.0

  • Simplified decorators - Decorators now expect FastHTML standard function signatures (req, sess as first two parameters)
  • **Removed _extract_req_sess** - No more complex parameter extraction, decorators work with standard patterns only