Skip to content

Commit

Permalink
WIP for max expires feature, refs #33
Browse files Browse the repository at this point in the history
  • Loading branch information
simonw committed Sep 3, 2024
1 parent 4854626 commit 6539d32
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 29 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,23 @@ datasette \
-s permissions.auth-tokens-create.id '*' # to enable token creation
```

By default users will be able to create tokens with any expiry, including tokens that never expire at all.

You can configure a maximum expiry time for tokens by adding a `max_expiry` setting to your plugin configuration:

```json
{
"plugins": {
"datasette-auth-tokens": {
"manage_tokens": true,
"max_expiry_seconds": 86400
}
}
}
```

The value should be specified in seconds.

### Viewing tokens

By default, users can only view tokens that they themselves have created on the `/-/api/tokens` page.
Expand Down
1 change: 1 addition & 0 deletions datasette_auth_tokens/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ async def inner():

@hookimpl
def startup(datasette):
print("STARTUP")
config = Config(datasette)
if not config.enabled:
return
Expand Down
12 changes: 7 additions & 5 deletions datasette_auth_tokens/templates/create_api_token.html
Original file line number Diff line number Diff line change
Expand Up @@ -53,13 +53,15 @@ <h2>Create another token</h2>
</div>
<div class="select-wrapper" style="width: unset">
<select name="expire_type">
<option value="">Token never expires</option>
<option value="minutes">Expires after X minutes</option>
<option value="hours">Expires after X hours</option>
<option value="days">Expires after X days</option>
{% if not max_expiry %}
<option value="">Token never expires</option>
{% endif %}
<option value="minutes"{% if max_expiry.minutes %} selected{% endif %}>Expires after X minutes</option>
<option value="hours"{% if max_expiry.hours %} selected{% endif %}>Expires after X hours</option>
<option value="days"{% if max_expiry.days %} selected{% endif %}>Expires after X days</option>
</select>
</div>
<input type="text" name="expire_duration" style="width: 10%">
<input type="text" name="expire_duration" style="width: 10%" value="{{ max_expiry.minutes or max_expiry.hours or max_expiry.days or "" }}">
<input type="hidden" name="csrftoken" value="{{ csrftoken() }}">
<input type="submit" value="Create token">

Expand Down
74 changes: 50 additions & 24 deletions datasette_auth_tokens/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,28 +14,31 @@

async def create_api_token(request, datasette):
await check_permission(datasette, request.actor)
config = Config(datasette)

if request.method == "GET":
return Response.html(
await datasette.render_template(
"create_api_token.html",
await _shared(datasette, request),
await _shared(datasette, request, config),
request=request,
)
)
elif request.method == "POST":
post = await request.post_vars()
errors = []
expires_after = None
if post.get("expire_type"):
if post.get("expire_duration") or config.max_expiry:
duration_string = post.get("expire_duration")
if (
not duration_string
or not duration_string.isdigit()
or not int(duration_string) > 0
):
errors.append("Invalid expire duration")
if duration_string:
errors.append("Invalid expiry duration")
else:
unit = post["expire_type"]
unit = post.get("expire_type")
if unit == "minutes":
expires_after = int(duration_string) * 60
elif unit == "hours":
Expand All @@ -45,6 +48,17 @@ async def create_api_token(request, datasette):
else:
errors.append("Invalid expire duration unit")

# Enforce max_expiry if set
if config.max_expiry:
if not expires_after:
errors.append("Tokens must have an expiry")
elif expires_after > config.max_expiry:
errors.append(
"Token expiry must be less than {} seconds".format(
config.max_expiry
)
)

# Are there any restrictions?
restrict_all = []
restrict_database = {}
Expand Down Expand Up @@ -80,27 +94,28 @@ async def create_api_token(request, datasette):
)
permissions = token_bits.get("_r") or None

config = Config(datasette)
db = config.db
cursor = await db.execute_write(
"""
insert into _datasette_auth_tokens
(secret_version, description, permissions, actor_id, created_timestamp, expires_after_seconds)
values
(:secret_version, :description, :permissions, :actor_id, :created_timestamp, :expires_after_seconds)
""",
{
"secret_version": 0,
"permissions": json.dumps(permissions),
"description": post.get("description") or None,
"actor_id": request.actor["id"],
"created_timestamp": int(time.time()),
"expires_after_seconds": expires_after,
},
)
token = "dsatok_{}".format(datasette.sign(cursor.lastrowid, "dsatok"))
token = None
if not errors:
cursor = await db.execute_write(
"""
insert into _datasette_auth_tokens
(secret_version, description, permissions, actor_id, created_timestamp, expires_after_seconds)
values
(:secret_version, :description, :permissions, :actor_id, :created_timestamp, :expires_after_seconds)
""",
{
"secret_version": 0,
"permissions": json.dumps(permissions),
"description": post.get("description") or None,
"actor_id": request.actor["id"],
"created_timestamp": int(time.time()),
"expires_after_seconds": expires_after,
},
)
token = "dsatok_{}".format(datasette.sign(cursor.lastrowid, "dsatok"))

context = await _shared(datasette, request)
context = await _shared(datasette, request, config)
context.update({"errors": errors, "token": token, "token_bits": token_bits})
return Response.html(
await datasette.render_template(
Expand All @@ -120,7 +135,7 @@ async def check_permission(datasette, actor):
raise Forbidden("You do not have permission to create a token")


async def _shared(datasette, request):
async def _shared(datasette, request, config):
await check_permission(datasette, request.actor)
db = Config(datasette).db

Expand Down Expand Up @@ -158,6 +173,15 @@ async def _shared(datasette, request):
database_with_tables.append(db_info)
if tables:
databases_with_at_least_one_table.append(db_info)

max_expiry = {}
if config.max_expiry:
max_expiry = {
"seconds": config.max_expiry,
"hours": None,
"days": None,
}

return {
"actor": request.actor,
"all_permissions": [
Expand All @@ -184,6 +208,7 @@ async def _shared(datasette, request):
"database_with_tables": database_with_tables,
"databases_with_at_least_one_table": databases_with_at_least_one_table,
"tokens_exist": tokens_exist,
"max_expiry": max_expiry,
}


Expand Down Expand Up @@ -377,6 +402,7 @@ def __init__(self, datasette):
self._plugin_config = datasette.plugin_config("datasette-auth-tokens") or {}
self._datasette = datasette
self.enabled = self._plugin_config.get("manage_tokens")
self.max_expiry = self._plugin_config.get("max_expiry")

def get(self, key):
return self._plugin_config.get(key)
Expand Down

0 comments on commit 6539d32

Please sign in to comment.