Get API key management wired up

This commit is contained in:
Reid 'arrdem' McKenzie 2023-05-26 23:54:36 -06:00
parent aacc9f5c1c
commit bedad7d86b
6 changed files with 87 additions and 17 deletions

View file

@ -8,6 +8,7 @@ from pathlib import Path
import click import click
from flask import Flask, request, session, current_app from flask import Flask, request, session, current_app
import tomllib import tomllib
from datetime import datetime
from tentacles.blueprints import user_ui, printer_ui, api from tentacles.blueprints import user_ui, printer_ui, api
from tentacles.store import Store from tentacles.store import Store
@ -30,6 +31,7 @@ def commit_db(resp):
def create_j2_request_global(): def create_j2_request_global():
current_app.jinja_env.globals["request"] = request current_app.jinja_env.globals["request"] = request
current_app.jinja_env.globals["datetime"] = datetime
def user_session(): def user_session():

View file

@ -2,9 +2,11 @@
"""Blueprints for HTML serving 'ui'.""" """Blueprints for HTML serving 'ui'."""
import datetime
import logging import logging
from datetime import timedelta from datetime import timedelta, datetime
from importlib.resources import files from importlib.resources import files
import re
from click import group from click import group
from flask import ( from flask import (
@ -113,11 +115,29 @@ def logout():
@BLUEPRINT.route("/user", methods=["GET", "POST"]) @BLUEPRINT.route("/user", methods=["GET", "POST"])
def settings(): def settings():
if is_logged_in(request): if not is_logged_in(request):
return redirect("/") return redirect("/")
elif request.method == "POST": elif request.method == "POST":
pass if request.form["action"] == "add":
ttl_spec = request.form.get("ttl")
if ttl_spec == "forever":
ttl = None
elif m := re.fullmatch(r"(\d+)d", ttl_spec):
ttl = timedelta(days=int(m.group(1)))
else:
flash("Bad request", category="error")
return render_template("user.html.j2"), 400
request.db.create_key(request.sid, ttl, request.form.get("name"))
flash("Key created", category="success")
elif request.form["action"] == "revoke":
request.db.delete_key(request.uid, request.form.get("id"))
flash("Key revoked", category="success")
else: else:
flash("Bad request", category="error")
return render_template("user.html.j2"), 400
return render_template("user.html.j2") return render_template("user.html.j2")

View file

@ -37,6 +37,7 @@ CREATE TABLE IF NOT EXISTS users (
CREATE TABLE IF NOT EXISTS user_keys ( CREATE TABLE IF NOT EXISTS user_keys (
id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(48)))) id TEXT PRIMARY KEY DEFAULT (lower(hex(randomblob(48))))
, user_id INTEGER , user_id INTEGER
, name TEXT
, expiration TEXT , expiration TEXT
); );

View file

@ -139,10 +139,12 @@ class Store(object):
@fmap(one) @fmap(one)
@requires_conn @requires_conn
def _create_session(self, uid: int, ttl: Optional[timedelta]): def _create_session(
self, uid: int, ttl: Optional[timedelta] = None, name: Optional[str] = None
):
return self._conn.execute( return self._conn.execute(
"INSERT INTO user_keys (user_id, expiration) VALUES (?, ?) RETURNING (id)", "INSERT INTO user_keys (user_id, name, expiration) VALUES (?1, ?2, ?3) RETURNING (id)",
[uid, (datetime.now() + ttl).isoformat() if ttl else None], [uid, name, (datetime.now() + ttl).isoformat() if ttl else None],
).fetchone() ).fetchone()
@requires_conn @requires_conn
@ -166,14 +168,14 @@ class Store(object):
uid, status = res uid, status = res
if status > 0: if status > 0:
return self._create_session(uid, ttl) return self._create_session(uid, ttl, "web session")
else: else:
_, status = self.fetch_user_status(status) _, status = self.fetch_user_status(status)
raise LoginError(status) raise LoginError(status)
@requires_conn @requires_conn
def create_key(self, kid: str, ttl) -> Optional[str]: def create_key(self, kid: str, ttl, name: Optional[str] = None) -> Optional[str]:
"""Given an _existing_ login session, create a new key. """Given an _existing_ login session, create a new key.
This allows the user to create more or less permanent API keys associated with their identity. This allows the user to create more or less permanent API keys associated with their identity.
@ -181,11 +183,14 @@ class Store(object):
""" """
if uid := self.try_key(kid): if uid := self.try_key(kid):
return self._create_session(uid, ttl) return self._create_session(uid, ttl, name)
@requires_conn @requires_conn
def list_keys(self): def list_keys(self, uid: int):
return self._conn.execute("SELECT id, user_id FROM user_keys").fetchall() for id, name, exp in self._conn.execute(
"SELECT id, name, expiration FROM user_keys WHERE user_id = ?1", [uid]
).fetchall():
yield id, name, datetime.fromisoformat(exp) if exp else None
@requires_conn @requires_conn
def fetch_key(self, kid) -> tuple: def fetch_key(self, kid) -> tuple:
@ -217,10 +222,12 @@ class Store(object):
) )
@requires_conn @requires_conn
def delete_key(self, kid: str): def delete_key(self, uid: int, kid: str):
"""Remove a session/key; equivalent to logout.""" """Remove a session/key; equivalent to logout."""
self._conn.execute("DELETE FROM user_keys WHERE id = ?", [kid]) self._conn.execute(
"DELETE FROM user_keys WHERE user_id = ?1 and id = ?2", [uid, kid]
)
################################################################################ ################################################################################
# Printers # Printers

View file

@ -12,8 +12,10 @@
<body> <body>
<nav class="navbar"> <nav class="navbar">
<span class="logo"> <span class="logo">
<a href="https://tentacles.tirefireind.us"><img src="/static/tentacles.svg" alt="Tentacles"></a> <a href="/">
<img src="/static/tentacles.svg" alt="Tentacles">
<span class="color-yellow">Tentacles</span> <span class="color-yellow">Tentacles</span>
</a>
</span> </span>
<input id="menu-toggle" type="checkbox" /> <input id="menu-toggle" type="checkbox" />

View file

@ -0,0 +1,38 @@
{% extends "base.html.j2" %}
{% block content %}
<h1>User settings</h1>
<div class="">
<h2>API keys</h2>
{% with keys = request.db.list_keys(request.uid) %}
<ul>
{% for id, name, exp in keys if name != 'web session' %}
<li>
<span class="key-name">{{ name }}</span>
<span class="key-key">{{ id }}</span>
<span class="key-expiration">{{ 'Expires in' if exp else ''}} {{ exp - datetime.now() if exp else 'Never' }}</span>
<form method="post">
<input type="hidden" name="action" value="revoke">
<input type="hidden" name="id" value="{{ id }}">
<span><input id="submit" type="submit" value="Revoke"/></span>
</form>
</li>
{% endfor %}
</ul>
{% endwith %}
<h2>Add a key</h2>
<form method="post">
<input type="hidden" name="action" value="add">
<span class="form-input"><span class="form-label">API key name</span><input type="text" name="name" /></span>
<span class="form-input"><span class="form-label">Key lifetime</span>
<select name="ttl">
<option value="30d">30 days</option>
<option value="90d">90 days</option>
<option value="365d">1y</option>
<option value="forever">Forever (not recommended)</option>
</select>
</span>
<span><input id="submit" type="submit" value="Add" onclick="maybeSubmit();" /></span>
</form>
</div>
{% endblock %}