Skip to content

Commit

Permalink
restrict_access configuration mechanism, closes #5
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Nov 15, 2020
1 parent a1dab40 commit c063f6b
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 1 deletion.
31 changes: 31 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,37 @@ Ensure you have a website with a domain that supports IndieAuth or RelMeAuth.

Visit `/-/indieauth` to begin the sign-in progress.

When a user signs in using IndieAuth they will be recieve a signed `ds_actor` cookie identifying them as an actor that looks like this:

```json
{
"me": "https://simonwillison.net/",
"display": "https://simonwillison.net/"
}
```

## Restricting access with the restrict_access plugin configuration

You can use [Datasette's permissions system](https://docs.datasette.io/en/stable/authentication.html#permissions) to control permissions of authenticated users - by default, an authenticated user will be able to perform the same actions as an unauthenticated user.

As a shortcut if you want to lock down access to your instance entirely to just specific users, you can use the `restrict_access` plugin configuration option like this:

```json
{
"plugins": {
"datasette-indieauth": {
"restrict_access": "https://simonwillison.net/"
}
}
}
```

This can be a string or a list of user identifiers. It can also be a space separated list, which means you can use it with the [datasette publish](https://docs.datasette.io/en/stable/publish.html#datasette-publish) `--plugin-secret` configuration option to set permissions as part of a deployment, like this:
```
datasette publish vercel mydb.db --project my-secret-db \
--install datasette-indieauth \
--plugin-secret datasette-indieauth restrict_access https://simonwillison.net/
```
## Development

To set up this plugin locally, first checkout the code. Then create a new virtual environment:
Expand Down
30 changes: 29 additions & 1 deletion datasette_indieauth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@


async def indieauth(request, datasette):
return await indieauth_page(request, datasette)


async def indieauth_page(request, datasette, initial_status=200):
client_id = datasette.absolute_url(request, datasette.urls.instance())
redirect_uri = datasette.absolute_url(request, request.path)

error = None
status = 200
status = initial_status

if request.args.get("code") and request.args.get("me"):
ok, extra = await verify_code(request.args["code"], client_id, redirect_uri)
Expand Down Expand Up @@ -86,3 +90,27 @@ def menu_links(datasette, actor):
"label": "Sign in IndieAuth",
},
]


@hookimpl
def permission_allowed(datasette, actor, action):
if action != "view-instance":
return None
plugin_config = datasette.plugin_config("datasette-indieauth") or {}
if plugin_config.get("restrict_access") is None:
return None
# Only actors in the list are allowed
if not actor:
return False
allowed_actors = plugin_config["restrict_access"]
if isinstance(allowed_actors, str):
allowed_actors = allowed_actors.split()
return actor.get("me") in allowed_actors


@hookimpl
def forbidden(request, datasette):
async def inner():
return await indieauth_page(request, datasette, 403)

return inner
40 changes: 40 additions & 0 deletions tests/test_indieauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,3 +52,43 @@ async def test_auth_fails(httpx_mock):
# Should return error
assert response.status_code == 403
assert "An error of some sort" in response.text


@pytest.mark.asyncio
async def test_restrict_access(httpx_mock):
httpx_mock.add_response(
url="https://indieauth.com/auth", data=b"me=https://simonwillison.net/"
)
datasette = Datasette(
[],
memory=True,
metadata={
"plugins": {
"datasette-indieauth": {"restrict_access": "https://simonwillison.net/"}
}
},
)
app = datasette.app()
paths = ("/", "/:memory:", "/-/metadata")
async with httpx.AsyncClient(app=app) as client:
# All pages should 403 and show login form
for path in paths:
response = await client.get("http://localhost{}".format(path))
assert response.status_code == 403
assert '<form action="https://indieauth.com/auth"' in response.text
assert "https://simonwillison.net/" not in response.text

# Now do the login and try again
response2 = await client.get(
"http://localhost/-/indieauth?code=code&me=example.com",
allow_redirects=False,
)
assert response2.status_code == 302
ds_actor = response2.cookies["ds_actor"]
# Everything should 200 now
for path in paths:
response = await client.get(
"http://localhost{}".format(path), cookies={"ds_actor": ds_actor}
)
assert response.status_code == 200
assert "https://simonwillison.net/" in response.text

0 comments on commit c063f6b

Please sign in to comment.