Files
shopdb-flask/plugins/tools/plugin.py
cproudlock d4b49b91b4
Some checks failed
CI / backend (push) Failing after 7m10s
CI / naming (push) Failing after 7m14s
CI / frontend (push) Failing after 7m10s
CI / migrations-mysql (push) Failing after 7m10s
Move secure send into Tech Tools, where somebody would look for it
9845867 gave 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 in 4242028 - 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.
2026-08-21 14:17:57 -04:00

84 lines
3.0 KiB
Python

"""Tech Tools plugin.
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.
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
from typing import List, Optional, Type
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 and the one tool that needs a server."""
@property
def meta(self) -> PluginMeta:
return PluginMeta(
name='tools',
version='1.0.0',
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]:
# Secure send only. Every other tool is still browser-side.
return tools_bp
def get_models(self) -> List[Type]:
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 [
{
'name': 'Tech Tools',
'icon': 'wrench',
'route': '/tools',
'position': 47,
},
]
def on_install(self, app: Flask) -> None:
logger.info('Tools plugin installed')