Skip to content

Commit

Permalink
what even is this
Browse files Browse the repository at this point in the history
  • Loading branch information
cldellow committed Feb 4, 2023
1 parent 1cd4cdf commit 99b0b82
Show file tree
Hide file tree
Showing 5 changed files with 106 additions and 3 deletions.
29 changes: 27 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
[![Tests](https://github.com/cldellow/datasette-current-actor/workflows/Test/badge.svg)](https://github.com/cldellow/datasette-current-actor/actions?query=workflow%3ATest)
[![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/cldellow/datasette-current-actor/blob/main/LICENSE)

Adds a current_actor() function to SQLite that show's the current actor's ID.
Adds a `current_actor()` function to SQLite that show's the current actor's ID.

## Installation

Expand All @@ -15,7 +15,32 @@ Install this plugin in the same environment as Datasette.

## Usage

Usage instructions go here.
### Boring mode

`SELECT current_actor()` returns `NULL` if there's no currently-logged in actor,
or the id of the actor.

You could put this in a canned query to provide limited access to tables.

### Ludicrous mode

SQLite is _flexible_. It turns out you can refer to functions that don't exist
when issuing DDL statements. As long as they exist when they're needed, it all
works out.

#### Auditing

Create a trigger on a table that sets the `last_edited_by` column to
`current_actor()`.

#### Row-level security

Restrict what rows users can see:

```sql
CREATE VIEW rls AS
SELECT * FROM sensitive_data WHERE owner = current_actor()
```

## Development

Expand Down
59 changes: 59 additions & 0 deletions datasette_current_actor/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,60 @@
from datasette import hookimpl
from datasette.database import Database
import asyncio
import threading

# Adds a current_actor() function to SQLite that shows the current actor's
# ID.

original_execute_fn = Database.execute_fn
actor = threading.local()

async def patched_execute_fn(self, fn):
task = asyncio.current_task()

scope = None if not hasattr(task, '_dux_request') else task._dux_request.scope

def wrapped_fn(conn):
if scope and 'actor' in scope and scope['actor'] and 'id' in scope['actor']:
actor.actor = scope['actor']['id']
else:
actor.actor = None
rv = fn(conn)
actor.actor = None
return rv

return await original_execute_fn(self, wrapped_fn)

Database.execute_fn = patched_execute_fn
@hookimpl
def prepare_connection(conn):
try:
if getattr(conn, 'engine') == 'duckdb':
return
except AttributeError:
pass

def fn():
return actor.actor

conn.create_function(
"current_actor", 0, fn
)

# We always register an actor_from_request hook so that our
# actor_from_request hookwrapper is guaranteed to fire
# on every request.
@hookimpl
def actor_from_request(datasette, request):
return None
_id = None
if 'x-me' in request.headers:
_id = request.headers['x-me']
return {'id': _id}

@hookimpl(specname='actor_from_request', hookwrapper=True)
def sniff_actor_from_request(datasette, request):
asyncio.current_task()._dux_request = request

# all corresponding hookimpls are invoked here
outcome = yield
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@ def get_long_description():
packages=["datasette_current_actor"],
entry_points={"datasette": ["current_actor = datasette_current_actor"]},
install_requires=["datasette"],
extras_require={"test": ["pytest", "pytest-asyncio"]},
extras_require={"test": ["pytest", "pytest-asyncio", "pytest-watch"]},
python_requires=">=3.7",
)
5 changes: 5 additions & 0 deletions tests/plugins/auth.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from datasette import hookimpl

@hookimpl
def actor_from_request(request):
return {'id': 'someuser'}
14 changes: 14 additions & 0 deletions tests/test_current_actor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,17 @@ async def test_plugin_is_installed():
assert response.status_code == 200
installed_plugins = {p["name"] for p in response.json()}
assert "datasette-current-actor" in installed_plugins

@pytest.mark.asyncio
async def test_current_actor_none():
datasette = Datasette(memory=True)
response = await datasette.client.get("/_memory.json?sql=select+current_actor()+as+actor&_shape=array")
assert response.status_code == 200
assert response.json() == [{'actor': None}]

@pytest.mark.asyncio
async def test_current_actor_none():
datasette = Datasette(memory=True, plugins_dir='./tests/plugins/')
response = await datasette.client.get("/_memory.json?sql=select+current_actor()+as+actor&_shape=array")
assert response.status_code == 200
assert response.json() == [{'actor': 'someuser'}]

0 comments on commit 99b0b82

Please sign in to comment.