Event System API Reference¶
With track_events=True, CRUDAdmin records what admins do: logins, logouts, refused logins, lockouts, and every create, update and delete made through the interface. Superusers read the log on the Event Logs page.
How Events Are Recorded¶
Authentication events come from crudauth hooks:
| Event | Type | Status |
|---|---|---|
| Successful login | LOGIN |
SUCCESS |
| Logout | LOGOUT |
SUCCESS |
| Wrong password, unknown or inactive admin | FAILED_LOGIN |
FAILURE |
| Login refused by the lockout | FAILED_LOGIN |
FAILURE, with reason: "lockout" in the details |
The username tried is stored under details.username, which is what EventService.get_security_alerts groups failed logins by. A hook that fails is logged and doesn't affect the login.
Model events are recorded by the create, update and bulk-delete routes through admin.events, once they know the outcome:
SUCCESSonce the change is committed;FAILUREwhen it is refused (invalid input, a record that doesn't exist, a constraint the database enforces). A refused change writes no audit rows, since nothing changed.- A create writes one audit row with the new record; an update, one with the record before and after and the fields that changed. Both are keyed by the model's primary key, whatever its name.
- A delete writes one audit row per deleted record, holding the record as it was.
- The event and its audit rows are written in one transaction. If any write fails, they're rolled back together, logged, and the admin's change itself still stands.
Stored values:
session_idholds the session's public handle, never the session id itself.- Values under credential-like keys (any key containing
password,secret,token,session_idorapi_key) are stored as[redacted], in event details, audit snapshots, change sets and audit metadata alike. A changed password still shows up as a change. - Request details are stored in the event's
detailsand in the audit row'saudit_metadata. - Before 0.7, event details and audit metadata weren't redacted. The Event Logs page redacts the details of those older events when it shows them; the rows themselves are unchanged.
Core Components¶
Event Types and Status¶
Model Creation Functions¶
Source code in crudadmin/event/models.py
Source code in crudadmin/event/models.py
Event Service¶
The service that writes and queries events and audit rows.
Source code in crudadmin/event/service.py
70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 | |
cleanup_old_logs(db, retention_days=90)
async
¶
Clean up old logs based on retention policy.
Source code in crudadmin/event/service.py
get_resource_history(db, resource_type, resource_id, limit=50, offset=0)
async
¶
Get audit history for a specific resource.
Source code in crudadmin/event/service.py
get_security_alerts(db, lookback_hours=24)
async
¶
Get security alerts based on event patterns.
Source code in crudadmin/event/service.py
get_user_activity(db, user_id, start_time=None, end_time=None, limit=50, offset=0)
async
¶
Get user activity logs.
Source code in crudadmin/event/service.py
Event System Integration¶
Writes an event together with its audit rows.
Source code in crudadmin/event/integration.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 | |
log_auth_event(db, event_type, user_id, session_id, request, success, details=None)
async
¶
Record an authentication event; a failure is logged, never raised.
It runs inside login and logout, which must not fail because the event log can't be written.
Source code in crudadmin/event/integration.py
log_model_event(db, event_type, model, user_id, session_id, request, resource_id=None, previous_state=None, new_state=None, details=None, succeeded=True, deleted_records=None, primary_key_name='id')
async
¶
Record a model change: one event, and its audit rows if the change happened.
A failed action is recorded as a FAILURE event without audit rows, since nothing changed. A delete gets one audit row per deleted record, keyed by the model's primary key. The event and its audit rows are written in one transaction, rolled back together if any write fails.
Source code in crudadmin/event/integration.py
log_security_event(db, event_type, user_id, session_id, request, details)
async
¶
Log security-related events with high priority.
Source code in crudadmin/event/integration.py
Recording Changes¶
Records changes made through the admin in the event log.
The admin's create, update and delete routes call [record][] once they know
the outcome, with the records as they were and as they are; a route of your
own mounted behind the admin's login can do the same. With event tracking
off, record does nothing.
Secrets in the records are redacted before they are stored. A failure to write the event is logged and absorbed, since the change it describes is already committed.
Example
Source code in crudadmin/event/recorder.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 | |
record(request, admin_db, event_type, model, *, record_id=None, before=None, after=None, deleted=None, succeeded=True)
async
¶
Record one create, update or delete of model by the logged-in admin.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
request
|
Request
|
The admin's request; the admin and the session handle are read from it. |
required |
admin_db
|
AsyncSession
|
A session on the admin database, where events are kept. |
required |
event_type
|
EventType
|
|
required |
model
|
type[DeclarativeBase]
|
The model that changed. |
required |
record_id
|
Any
|
The changed record's primary key. |
None
|
before
|
dict[str, Any] | None
|
The record before an update. |
None
|
after
|
dict[str, Any] | None
|
The record after a create or an update. |
None
|
deleted
|
list[dict[str, Any]] | None
|
The records a delete removed, as they were. |
None
|
succeeded
|
bool
|
False records a refused change, without audit rows. |
True
|
Source code in crudadmin/event/recorder.py
Event Schemas¶
Event Log Schemas¶
Bases: AdminEventLogBase
Audit Log Schemas¶
Bases: AdminAuditLogBase
Usage Examples¶
Enabling Event Tracking¶
from crudadmin import CRUDAdmin
admin = CRUDAdmin(
session=get_session,
SECRET_KEY=os.environ["ADMIN_SECRET_KEY"],
track_events=True,
)
The event tables are created in the admin database by admin.initialize(), whichever session backend you use. admin.event_service and admin.event_integration give access to the service and the integration.
Logging an Event Yourself¶
from crudadmin.event import EventStatus, EventType
await admin.event_service.log_event(
db=admin_db,
event_type=EventType.CREATE,
status=EventStatus.SUCCESS,
user_id=user.id,
session_id=session_handle,
request=request,
resource_type="Product",
resource_id="123",
details={"import_batch_id": "batch_001"},
)
log_event commits unless you pass commit=False; create_audit_log takes the same flag, so an event and its audit rows can share a transaction.
Logging a Model Change with Its Audit Row¶
await admin.event_integration.log_model_event(
db=admin_db,
event_type=EventType.UPDATE,
model=Product,
user_id=current_user.id,
session_id=session_handle,
request=request,
resource_id=str(product.id),
previous_state={"name": "Old Name", "price": 10.00},
new_state={"name": "New Name", "price": 15.00},
)
For a delete, pass the deleted records and the primary key's name instead of resource_id: deleted_records=[...], primary_key_name="sku". Pass succeeded=False to record a failed action without audit rows.
Recording Changes from Your Own Routes¶
A route of your own can record the changes it makes the way the admin's routes do. admin.events.record reads the admin and the session from the request, which the admin's login dependency sets:
from fastapi import Depends, Request
from sqlalchemy.ext.asyncio import AsyncSession
from crudadmin.event import EventType
@router.post(
"/products/{sku}/restock",
dependencies=[Depends(admin.admin_authentication.get_current_user())],
)
async def restock(
sku: str,
request: Request,
db: AsyncSession = Depends(get_session),
admin_db: AsyncSession = Depends(admin.db_config.get_admin_db),
):
before = await product_as_dict(db, sku)
await add_stock(db, sku, 10)
await db.commit()
await admin.events.record(
request,
admin_db,
EventType.UPDATE,
Product,
record_id=sku,
before=before,
after=await product_as_dict(db, sku),
)
Pass after= for a create, before= and after= for an update, and deleted=[...] with the removed records for a delete; succeeded=False records a refused change. With track_events=False, record does nothing.
Querying Event History¶
activity = await admin.event_service.get_user_activity(
db=admin_db,
user_id=user.id,
start_time=datetime.now(timezone.utc) - timedelta(days=7),
limit=100,
)
history = await admin.event_service.get_resource_history(
db=admin_db,
resource_type="Product",
resource_id="123",
limit=50,
)
alerts = await admin.event_service.get_security_alerts(db=admin_db, lookback_hours=24)
Event Model Fields¶
AdminEventLog Fields¶
| Field | Type | Description |
|---|---|---|
id |
int | Primary key |
timestamp |
datetime | When the event occurred |
event_type |
EventType | create, update, delete, login, logout or failed_login |
status |
EventStatus | success, failure or warning |
user_id |
int | The admin who acted; 0 when a refused login named no account |
session_id |
str | The session's public handle |
ip_address |
str | Client IP address |
user_agent |
str | User agent string from the request |
resource_type |
str | Model name, for model events |
resource_id |
str | Primary key of the record, for creates and updates |
details |
dict | Event-specific details |
AdminAuditLog Fields¶
| Field | Type | Description |
|---|---|---|
id |
int | Primary key |
event_id |
int | The AdminEventLog row this belongs to |
timestamp |
datetime | When the audit row was written |
resource_type |
str | Model name |
resource_id |
str | Primary key of the record |
action |
str | create, update or delete |
previous_state |
dict | The record before the change (redacted) |
new_state |
dict | The record after the change (redacted) |
changes |
dict | Fields that changed, with old and new values (redacted) |
audit_metadata |
dict | Request details |
Retention¶
EventService.cleanup_old_logs(db, retention_days=90) deletes older events. Schedule it if the log shouldn't grow without bound.