From fe9dfbffa596c4d97e9d7e2629093eda8b6ded53 Mon Sep 17 00:00:00 2001 From: Simon Willison Date: Wed, 31 Jan 2024 14:01:55 -0800 Subject: [PATCH] CreateTokenEvent, refs #2240 --- datasette/events.py | 28 +++++++++++++++++++++++++++- datasette/views/special.py | 11 ++++++++++- tests/test_auth.py | 7 +++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/datasette/events.py b/datasette/events.py index 5eb930b4da..39d0de32e0 100644 --- a/datasette/events.py +++ b/datasette/events.py @@ -1,6 +1,7 @@ from abc import ABC, abstractproperty from dataclasses import asdict, dataclass from datasette.hookspecs import hookimpl +from typing import Optional @dataclass @@ -39,6 +40,31 @@ class LogoutEvent(Event): name = "logout" +@dataclass +class CreateTokenEvent(Event): + """ + Event name: ``create-token`` + + A user created an API token. + + :ivar expires_after: Number of seconds after which this token will expire. + :type expires_after: int or None + :ivar restrict_all: Restricted permissions for this token. + :type restrict_all: list + :ivar restrict_database: Restricted database permissions for this token. + :type restrict_database: dict + :ivar restrict_resource: Restricted resource permissions for this token. + :type restrict_resource: dict + """ + + name = "create-token" + + expires_after: Optional[int] + restrict_all: list + restrict_database: dict + restrict_resource: dict + + @dataclass class CreateTableEvent(Event): """ @@ -62,4 +88,4 @@ class CreateTableEvent(Event): @hookimpl def register_events(): - return [LoginEvent, LogoutEvent, CreateTableEvent] + return [LoginEvent, LogoutEvent, CreateTableEvent, CreateTokenEvent] diff --git a/datasette/views/special.py b/datasette/views/special.py index 492b7c0d4f..4088a1f9c9 100644 --- a/datasette/views/special.py +++ b/datasette/views/special.py @@ -1,5 +1,5 @@ import json -from datasette.events import LogoutEvent, LoginEvent +from datasette.events import LogoutEvent, LoginEvent, CreateTokenEvent from datasette.utils.asgi import Response, Forbidden from datasette.utils import ( actor_matches_allow, @@ -351,6 +351,15 @@ async def post(self, request): restrict_resource=restrict_resource, ) token_bits = self.ds.unsign(token[len("dstok_") :], namespace="token") + await self.ds.track_event( + CreateTokenEvent( + actor=request.actor, + expires_after=expires_after, + restrict_all=restrict_all, + restrict_database=restrict_database, + restrict_resource=restrict_resource, + ) + ) context = await self.shared(request) context.update({"errors": errors, "token": token, "token_bits": token_bits}) return await self.render(["create_token.html"], request, context) diff --git a/tests/test_auth.py b/tests/test_auth.py index 157fd9957a..f2359df7da 100644 --- a/tests/test_auth.py +++ b/tests/test_auth.py @@ -193,6 +193,13 @@ def test_auth_create_token( for error in errors: assert '

{}

'.format(error) in response2.text else: + # Check create-token event + event = last_event(app_client.ds) + assert event.name == "create-token" + assert event.expires_after == expected_duration + assert isinstance(event.restrict_all, list) + assert isinstance(event.restrict_database, dict) + assert isinstance(event.restrict_resource, dict) # Extract token from page token = response2.text.split('value="dstok_')[1].split('"')[0] details = app_client.ds.unsign(token, "token")