Skip to content

Commit

Permalink
cacheable_prefixes setting, closes #47
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Oct 7, 2019
2 parents bf01f8f + 211f820 commit e4334d7
Show file tree
Hide file tree
Showing 4 changed files with 67 additions and 10 deletions.
22 changes: 21 additions & 1 deletion README.md
Expand Up @@ -206,4 +206,24 @@ The middleware adds a new `"auth"` key to the scope containing details of the si
```
The `"ts"` value is an integer `time.time()` timestamp representing when the user last signed in.

If the user is not signed in (and you are not using required authentication) the `"auth"` scope key will be set to `None`.
If the user is not signed in (and you are not using required authentication) the `"auth"` scope key will be set to `None`.

### cacheable_prefixes

By default, the middleware marks all returned responses as `cache-control: private`. This is to ensure that content which was meant to be private to an individual user is not accidentally stored and re-transmitted by any intermediary proxying caches.

This means even static JavaScript and CSS assets will not be cached by the user's browser, which can have a negative impact on performance.

You can specify path prefixes that should NOT be marked as `cache-control: private` using the `cacheable_prefixes` constructor argument:

```python
app = GitHubAuth(
asgi_app,
client_id="github_client_id",
client_secret="github_client_secret",
require_auth=True,
cacheable_prefixes=["/static/"],
)
```

Now any files within the `/static/` directory will not have the `cache-control: private` header added by the middleware.
1 change: 1 addition & 0 deletions datasette_auth_github/__init__.py
Expand Up @@ -35,6 +35,7 @@ def wrap_with_asgi_auth(app):
allow_users=allow_users,
allow_orgs=allow_orgs,
allow_teams=allow_teams,
cacheable_prefixes=["/-/static/", "/-/static-plugins/"],
)

return wrap_with_asgi_auth
Expand Down
24 changes: 15 additions & 9 deletions datasette_auth_github/github_auth.py
Expand Up @@ -66,6 +66,7 @@ def __init__(
allow_users=None,
allow_orgs=None,
allow_teams=None,
cacheable_prefixes=None,
):
self.app = app
self.client_id = client_id
Expand All @@ -76,6 +77,7 @@ def __init__(
self.allow_users = allow_users
self.allow_orgs = allow_orgs
self.allow_teams = allow_teams
self.cacheable_prefixes = cacheable_prefixes or []
self.team_to_team_id = {}

cookie_version = cookie_version or "default"
Expand All @@ -101,7 +103,7 @@ def oauth_scope(self):
async def __call__(self, scope, receive, send):
if scope.get("type") != "http":
return await self.app(scope, receive, send)
send = self.wrapped_send(send)
send = self.wrapped_send(send, scope)

if scope.get("path") == self.logout_path:
return await self.logout(scope, receive, send)
Expand Down Expand Up @@ -129,21 +131,25 @@ async def logout(self, scope, receive, send):
headers.append(["set-cookie", output_cookies.output(header="").lstrip()])
await send_html(send, "", 302, headers)

def wrapped_send(self, send):
def wrapped_send(self, send, scope):
async def wrapped_send(event):
# We only wrap http.response.start with headers
if not (event["type"] == "http.response.start" and event.get("headers")):
await send(event)
return
# Rebuild headers to include cache-control: private
path = scope.get("path")
original_headers = event.get("headers") or []
new_headers = [
[key, value]
for key, value in original_headers
if key.lower() != b"cache-control"
]
new_headers.append([b"cache-control", b"private"])
await send({**event, **{"headers": new_headers}})
if any(path.startswith(prefix) for prefix in self.cacheable_prefixes):
await send(event)
else:
new_headers = [
[key, value]
for key, value in original_headers
if key.lower() != b"cache-control"
]
new_headers.append([b"cache-control", b"private"])
await send({**event, **{"headers": new_headers}})

return wrapped_send

Expand Down
30 changes: 30 additions & 0 deletions test_datasette_auth_github.py
Expand Up @@ -466,6 +466,36 @@ async def test_require_auth_false(require_auth_app):
assert {"hello": "world", "auth": None} == json.loads(body["body"].decode("utf8"))


@pytest.mark.asyncio
async def test_cacheable_assets(require_auth_app):
# Anything with a path matching cacheable_prefixes should not
# have a cache-control: private header
require_auth_app.cacheable_prefixes = ["/-/static/"]
scope = {
"type": "http",
"http_version": "1.0",
"method": "GET",
"path": "/-/static/blah.js",
"headers": [[b"cookie", signed_auth_cookie_header(require_auth_app)]],
}
instance = ApplicationCommunicator(require_auth_app, scope)
await instance.send_input({"type": "http.request"})
output = await instance.receive_output(1)
assert [
[b"content-type", b"text/html; charset=UTF-8"],
[b"cache-control", b"max-age=123"],
] == output["headers"]
# BUT... if we reset cacheable_prefixes to [] it should behave as default:
require_auth_app.cacheable_prefixes = []
instance = ApplicationCommunicator(require_auth_app, scope)
await instance.send_input({"type": "http.request"})
output = await instance.receive_output(1)
assert [
[b"content-type", b"text/html; charset=UTF-8"],
[b"cache-control", b"private"],
] == output["headers"]


@pytest.mark.asyncio
async def test_datasette_plugin_installed():
instance = ApplicationCommunicator(
Expand Down

0 comments on commit e4334d7

Please sign in to comment.