This guide shows how to implement access control using the on_event hooks combined with the as_user() context manager.
ic-python-db provides context management for tracking the current user:
db.as_user(user_id)- Context manager for running operations as a specific userget_caller_id()- Get the current user IDset_caller_id(user_id)- Set the current user ID
Note: The context system uses a simple module-level variable, which is safe for IC canisters since they are single-threaded. It is not thread-safe for multi-threaded environments outside the IC.
from ic_python_db import Database, Entity, String
from ic_python_db.mixins import TimestampedMixin
db = Database.get_instance()
class Document(Entity, TimestampedMixin):
title = String()
# Create documents as different users
with db.as_user("alice"):
doc1 = Document(title="Alice's Doc") # Owner: alice
with db.as_user("bob"):
doc2 = Document(title="Bob's Doc") # Owner: bobCombine as_user() with on_event hooks for access control:
from ic_python_db import ACTION_MODIFY, ACTION_DELETE, Entity, String
from ic_python_db.context import get_caller_id
from ic_python_db.mixins import TimestampedMixin
class ProtectedDocument(Entity, TimestampedMixin):
title = String()
content = String()
@staticmethod
def on_event(entity, field_name, old_value, new_value, action):
caller_id = get_caller_id()
# Only owner can modify or delete
if action in (ACTION_MODIFY, ACTION_DELETE):
if entity._owner != caller_id:
return False, None # Deny access
return True, new_valueContexts can be nested and automatically reset:
with db.as_user("admin"):
admin_doc = Document(title="Admin Doc") # Owner: admin
with db.as_user("user"):
user_doc = Document(title="User Doc") # Owner: user
# Back to admin context
another_doc = Document(title="Another Admin") # Owner: adminContext manager ensures caller ID is reset even if exceptions occur:
try:
with db.as_user("alice"):
doc = Document(title="Test")
raise RuntimeError("Error!")
except RuntimeError:
pass
# Caller is automatically reset to previous value
print(get_caller_id()) # 'system'Easy to see which operations run as which user:
# Everything in this block runs as alice
with db.as_user("alice"):
doc = Document(title="Doc 1")
doc.content = "Updated"
doc.save()class ProtectedEntity(Entity, TimestampedMixin):
value = String()
ADMINS = ["admin", "superuser"]
@staticmethod
def on_event(entity, field_name, old_value, new_value, action):
caller = get_caller_id()
# Admins can do anything
if caller in ProtectedEntity.ADMINS:
return True, new_value
# Others need ownership
if action in (ACTION_MODIFY, ACTION_DELETE):
if entity._owner != caller:
return False, None
return True, new_valueclass Employee(Entity, TimestampedMixin):
name = String()
salary = Integer()
@staticmethod
def on_event(entity, field_name, old_value, new_value, action):
caller = get_caller_id()
# Salary requires HR permission
if field_name == "salary" and action == ACTION_MODIFY:
if caller not in ["hr_manager", "admin"]:
return False, None
return True, new_valueclass AuditedEntity(Entity, TimestampedMixin):
value = String()
audit_log = []
@staticmethod
def on_event(entity, field_name, old_value, new_value, action):
caller = get_caller_id()
# Log all access attempts
AuditedEntity.audit_log.append({
"caller": caller,
"action": action,
"field": field_name,
"old": old_value,
"new": new_value
})
return True, new_valueIf you were using os.environ.get("CALLER_ID"), migrate to the new context approach:
import os
os.environ["CALLER_ID"] = "alice"
doc = Document(title="My Doc")
os.environ["CALLER_ID"] = "system" # Easy to forget!with db.as_user("alice"):
doc = Document(title="My Doc")
# Automatically resets- HOOKS.md - Complete hooks documentation
- examples/simple_access_control.py - Working example