Every tool on that page is usable without an account, on purpose - the routes carry no requiresAuth and say so - and Secure Send is the one write this product offers a guest. The sidebar entry still never appeared for them: it named no permission, so the filter that decides what a signed-out visitor may see had nothing to go on and dropped it. The plugin had no read permission to name, only tools.secrets, so it gains tools.view: reaching the page at all. The tools themselves run in the browser and touch no data, which is why this is about whether the entry appears rather than about what anybody can read. The item names either right. Somebody granted only Secure Send needs the entry that leads to it, and a manager who holds neither still does not see it.
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')
|