Files
shopdb-flask/plugins/tools/plugin.py

157 lines
6.5 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:
"""Who may CREATE a secret, when guests are not allowed.
The default access model: signed in, and holding this. Reading needs no
permission at all - the recipient usually has no account here and the
unguessable token is the authorisation.
A site that turns on securesend_allow_guests bypasses this for people
with no account; it still applies to anyone who IS signed in, so a
member without it cannot create one while a guest can. That is
deliberate: opening the door to guests is a decision about the door, not
about revoking what a role does or does not hold.
"""
return [
# Reaching the tools page at all. The tools themselves run in the
# browser and touch no data, so this is about whether the entry
# appears - which is exactly the question a site answers when it
# decides whether people without an account may use them.
('tools.view', 'Use the browser tools (codes, conversions)', 'tools'),
('tools.secrets', 'Create secure send links', 'tools'),
]
def get_guest_grantable_permissions(self) -> List[str]:
"""Creating a secure send link is meant to work without an account.
Somebody standing at a bench needs to send a password to a vendor. The
recipient never had an account, and often neither does the sender - the
unguessable token is the authorisation, and the link is one-view.
A guest-created link records no creator. The audit trail says when and
from where, and never who. That is the cost, and it is why this is the
only write in the product offered to guests at all.
tools.view rides along because tools.secrets without it is a page a
guest holds permission to use and no way to reach: it gates the nav
entry, and the browser tools behind it (codes, conversions) run client
side and touch no data at all. Declared explicitly rather than inferred
from the .view suffix, which is what the grantable rule now asks for.
"""
return ['tools.secrets', 'tools.view']
def get_settings_defaults(self) -> List[dict]:
"""The one switch this plugin owns.
Public so the create page can read it BEFORE login and show a guest the
form rather than a sign-in wall. It says nothing sensitive: whether the
door is open is visible to anyone who tries the door.
"""
return [
{
'key': 'securesend_allow_guests',
'value': 'false',
'valuetype': 'boolean',
'category': 'tools',
'public': True,
'description': ('Let people with no account create secure send '
'links. Off by default: a guest secret has no '
'creator, so the audit trail can say when and '
'from where but never who. Receiving a secret '
'never needs an account either way.'),
},
]
def get_settings_cards(self) -> List[dict]:
"""A page for the one switch this plugin owns (ADR-010).
get_settings_defaults SEEDS the row; this is what gives somebody a place
to change it. Declaring only the first left the setting in the database
with no way to reach it, which is indistinguishable from the feature not
existing.
"""
return [
{
'group': 'Tools',
'to': '/settings/secure-send',
# From the settings iconMap; an unknown key falls back to a
# puzzle piece, which reads as "nobody chose this".
'icon': 'wrench',
'title': 'Secure Send',
'description': 'Whether people without an account can create '
'one-view password links',
'position': 60,
},
]
def get_navigation_items(self) -> List[dict]:
return [
{
'name': 'Tech Tools',
'icon': 'wrench',
'route': '/tools',
'position': 47,
'section': 'fleet',
# Either right reaches the page: somebody granted only Secure
# Send still needs the entry that leads to it.
'permission': ['tools.view', 'tools.secrets'],
},
]
def on_install(self, app: Flask) -> None:
logger.info('Tools plugin installed')