from fasthtml.common import *
from ship_kit.permissions import *
= fast_app()
app, rt
# Simple authentication check
@rt('/dashboard')
@auth_required
def get(req, sess):
= get_user_from_session(sess)
user return Div(
f"Welcome {user['username']}!"),
H1("This is your dashboard.")
P( )
Permissions
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 *
= fast_app()
app, rt
# 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:
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 |
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 |
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 |
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 |
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 |
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:
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 |
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 |
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:
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 |
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 |
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 |
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 |
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:
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
Role-Based Access Control
# Admin-only area
@rt('/admin')
@role_required('admin')
def get(req, sess):
return Div(
"Admin Panel"),
H1("Only administrators can see this.")
P(
)
# Moderator area (admins can also access)
@rt('/moderate')
@role_required('moderator')
def get(req, sess):
return Div(
"Moderation Queue"),
H1("Moderators and admins can see this.")
P( )
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):
= get_user_from_session(sess)
user = has_permission(user, 'delete_users')
can_delete
return Div(
f"User Profile #{user_id}"),
H1(
Button("Delete User",
=f"/users/{user_id}/delete",
hx_post="Are you sure?"
hx_confirmif can_delete else None
) )
Custom Permission Configuration
# Register custom permissions
'export_data', 'Export system data to CSV')
register_permission('view_analytics', 'View analytics dashboard')
register_permission(
# Create a custom role
'analyst', {
set_role_permissions('read_all_data',
'view_analytics',
'export_data'
})
# Add permission to existing role
'moderator', 'view_analytics')
add_role_permission(
# Remove permission from role
'user', 'delete_own_data') remove_role_permission(
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"},
{=403
status_code
)
# 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,=['/login', '/public', '/']
skip
)
= fast_app(before=beforeware)
app, rt
# 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(
"Permissions Demo"),
H1("Test different user roles:"),
P(
Ul("admin@example.com - Admin role"),
Li("mod@example.com - Moderator role"),
Li("user@example.com - User role")
Li(
),"Login", href="/login", cls="button")
A(
)
# Login page
@rt('/login')
def get():
return Div(
"Login"),
H2(
Form(="email", type="email", placeholder="Email", required=True),
Input(name"Login", type="submit"),
Button(="post"
method
)
)
@rt('/login', methods=['POST'])
async def post(req, sess):
= await req.form()
form = form.get('email')
email if email in users:
'auth'] = True
sess['user'] = users[email]
sess[return RedirectResponse('/dashboard', status_code=303)
return "Invalid email"
# User dashboard - requires authentication
@rt('/dashboard')
@auth_required
def get(req, sess):
= get_user_from_session(sess)
user = get_user_permissions(user)
permissions
return Div(
f"Welcome {user['email']}"),
H1(f"Role: {user['role']}"),
P("Your Permissions:"),
H3(*[Li(perm) for perm in sorted(permissions)]) if '*' not in permissions else P("All permissions"),
Ul("Test Areas:"),
H3(
Ul("Admin Area", href="/admin")),
Li(A("Moderator Area", href="/moderate")),
Li(A("Delete Users", href="/users/delete"))
Li(A(
),"Logout", href="/logout")
A(
)
# Admin only area
@rt('/admin')
@role_required('admin')
def get(req, sess):
return Div(
"Admin Area"),
H1("Only admins can see this!"),
P("Back to Dashboard", href="/dashboard")
A(
)
# Moderator area (admins can also access)
@rt('/moderate')
@role_required('moderator')
def get(req, sess):
= get_user_from_session(sess)
user return Div(
"Moderator Area"),
H1(f"Welcome {user['role']}! Moderators and admins can see this."),
P("Back to Dashboard", href="/dashboard")
A(
)
# Permission-based access
@rt('/users/delete')
@permission_required('delete_users')
def get(req, sess):
return Div(
"Delete Users"),
H1("This requires the 'delete_users' permission."),
P("Only admins have this by default."),
P("Back to Dashboard", href="/dashboard")
A(
)
# Logout
@rt('/logout')
def get(sess):
sess.clear()return RedirectResponse('/', status_code=303)
# Run the demo
from fasthtml.jupyter import JupyUvi
= JupyUvi(app) server
# 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
'admin')
update_user_role(user_id, # Clear cached permissions
clear_permission_cache(sess)
5. Combine with Beforeware for Global Auth
# Use beforeware for site-wide auth
= Beforeware(user_auth_before, skip=['/login', '/public'])
beforeware
# 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