Move secure send into Tech Tools, where somebody would look for it
9845867gave it its own plugin because Tech Tools is browser-only and this needs storage. That was the right reading of the code and the wrong answer for the person using it: Tech Tools is where you go for a utility, and a tool nobody can find is a tool nobody uses. So Tech Tools now has a blueprint, a table and a migration chain where it had none. Its docstring and manifest say why, because "every tool here runs in the browser" was load-bearing documentation and is no longer true. WHAT MOVED /api/securesend -> /api/tools/secrets securesend.create -> tools.secrets securesend0001baseline -> tools0001securesend PLUGIN_TABLE_OWNERS, EXPECTED_HEAD_REVISION, EXEMPT_ENDPOINTS, the universal profile and the api-inventory surface all follow it. THE OLD "NO BACKEND" TEST IS NARROWED, NOT DELETED. It asserted get_blueprint() is None, which is the sort of guard that gets removed when it becomes inconvenient. It now asserts the blueprint serves EXACTLY the three secret routes, with their methods - so a future tool that quietly grows an endpoint or a table has to edit this test, and therefore make the decision on purpose rather than by drift. Asserting methods rather than paths also pins the detail this design turns on: redeeming is POST-ONLY. Chat and mail clients prefetch GET urls to build previews, and a preview that could redeem would consume a one-view secret before its recipient opened the link. Previously that was a comment; now it fails a test. UNINSTALLING THE OLD PLUGIN COULD NOT BE DONE WITH THE PLUGIN TOOLING. `flask plugin uninstall securesend` loads a plugin before uninstalling it, so a plugin whose directory has been deleted cannot be removed by the command that manages plugins - the registry entry had to be edited by hand. Worth fixing separately; it is the same registry-versus-code divergence the device work hit from the other side. MIGRATING A CHAIN THAT HAS ALREADY RUN needs the old table and its version row dropped first, because the relocated chain has no stamp and tries to create a table that exists. Only relevant to anyone who installed securesend during the hour it existed - it did reach the publication repo in4242028- and the fix is DROP TABLE securesendsecrets plus DROP TABLE alembic_version_securesend before running plugin upgrade-all. 1829 tests green. Still backend only: there are no pages yet, so nothing here is reachable by a person.
This commit is contained in:
@@ -1,13 +1,19 @@
|
||||
"""Tech Tools plugin.
|
||||
|
||||
A section for small technician utilities. Every tool here runs entirely in the
|
||||
browser: no API, no tables, no state. That is deliberate - these are the things
|
||||
someone reaches for at a bench with no network guarantee, and an air-gapped
|
||||
site gets them for free.
|
||||
A section for small technician utilities. MOST tools here run entirely in the
|
||||
browser - no API, no tables, no state - which is deliberate: these are the
|
||||
things someone reaches for at a bench with no network guarantee, and an
|
||||
air-gapped site gets them for free.
|
||||
|
||||
The plugin exists so the section can carry a nav entry and be included in or
|
||||
left out of a per-site build (ADR-013). The tools themselves are frontend-only,
|
||||
which is why get_blueprint and get_models are both empty.
|
||||
SECURE SEND IS THE EXCEPTION, and it is why this plugin now has a blueprint, a
|
||||
table and a migration chain where it once had none. A link that expires and dies
|
||||
on first read cannot be browser-only: something has to hold the ciphertext until
|
||||
it is claimed, and forget it afterwards. The secret itself is still encrypted in
|
||||
the browser - the key travels in the URL fragment and never reaches this server -
|
||||
so what is stored is ciphertext nobody on this side can read.
|
||||
|
||||
The plugin also exists so the section can carry a nav entry and be included in or
|
||||
left out of a per-site build (ADR-013).
|
||||
"""
|
||||
|
||||
import logging
|
||||
@@ -17,30 +23,51 @@ from flask import Blueprint, Flask
|
||||
|
||||
from shopdb.plugins.base import BasePlugin, PluginMeta
|
||||
|
||||
from .models import SecureSecret
|
||||
from .api import tools_bp
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ToolsPlugin(BasePlugin):
|
||||
"""Registers the Tech Tools section. All tools are client-side."""
|
||||
"""Registers the Tech Tools section and the one tool that needs a server."""
|
||||
|
||||
@property
|
||||
def meta(self) -> PluginMeta:
|
||||
return PluginMeta(
|
||||
name='tools',
|
||||
version='1.0.0',
|
||||
description='Tech Tools: browser-side technician utilities',
|
||||
description=('Tech Tools: technician utilities, mostly browser-side '
|
||||
'(barcode/QR labels) plus secure send for handing '
|
||||
'somebody a credential by one-view link'),
|
||||
author='ShopDB Team',
|
||||
dependencies=[],
|
||||
core_version='>=0.16.0,<1.0.0',
|
||||
api_prefix='/api/tools',
|
||||
)
|
||||
|
||||
def get_blueprint(self) -> Optional[Blueprint]:
|
||||
# No API surface. The tools never leave the browser.
|
||||
return None
|
||||
# Secure send only. Every other tool is still browser-side.
|
||||
return tools_bp
|
||||
|
||||
def get_models(self) -> List[Type]:
|
||||
# No tables. Nothing here is persisted.
|
||||
return []
|
||||
return [SecureSecret]
|
||||
|
||||
def get_permissions(self) -> List:
|
||||
"""One permission, on CREATING a secret only.
|
||||
|
||||
There is deliberately no read permission: the recipient usually has no
|
||||
account here, and the unguessable token is what authorises the read.
|
||||
Gating that side would make the tool useless for the case it exists for -
|
||||
handing somebody a credential while you are on a call with them.
|
||||
|
||||
No delete permission either. A secret is spent by being read or by
|
||||
expiring, and both remove the row; a hand-delete route would only add a
|
||||
way to destroy the record of a secret that had not been read yet.
|
||||
"""
|
||||
return [
|
||||
('tools.secrets', 'Create secure send links', 'tools'),
|
||||
]
|
||||
|
||||
def get_navigation_items(self) -> List[dict]:
|
||||
return [
|
||||
|
||||
Reference in New Issue
Block a user