151 lines
6.1 KiB
Python
151 lines
6.1 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.
|
|
"""
|
|
return ['tools.secrets']
|
|
|
|
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')
|