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:
@@ -27,11 +27,39 @@ def test_meta_matches_manifest():
|
||||
assert meta.core_version == manifest['core_version']
|
||||
|
||||
|
||||
def test_no_backend_surface():
|
||||
"""Client-side only: no blueprint to register, no tables to migrate."""
|
||||
def test_the_only_backend_surface_is_secure_send():
|
||||
"""Tech Tools used to have NO backend, and mostly still should.
|
||||
|
||||
This asserted `get_blueprint() is None` until secure send arrived: a link
|
||||
that expires and dies on first read cannot be browser-only, because
|
||||
something has to hold the ciphertext until it is claimed. The guard is kept
|
||||
rather than deleted, narrowed to the one exception - a future tool that
|
||||
quietly grows an endpoint or a table has to change this test, and therefore
|
||||
make the decision on purpose rather than by accident.
|
||||
"""
|
||||
from flask import Flask
|
||||
|
||||
from plugins.tools.models import SecureSecret
|
||||
|
||||
plugin = ToolsPlugin()
|
||||
assert plugin.get_blueprint() is None
|
||||
assert plugin.get_models() == []
|
||||
assert plugin.get_models() == [SecureSecret]
|
||||
|
||||
probe = Flask(__name__)
|
||||
probe.register_blueprint(plugin.get_blueprint(), url_prefix='/api/tools')
|
||||
served = sorted(
|
||||
(str(rule), sorted(rule.methods - {'HEAD', 'OPTIONS'}))
|
||||
for rule in probe.url_map.iter_rules()
|
||||
if not str(rule).startswith('/static'))
|
||||
|
||||
# Methods are asserted, not just paths: REDEEMING MUST BE 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 ever
|
||||
# opened the link. The GET on the same path reports availability only.
|
||||
assert served == [
|
||||
('/api/tools/secrets', ['POST']),
|
||||
('/api/tools/secrets/<token>', ['GET']),
|
||||
('/api/tools/secrets/<token>', ['POST']),
|
||||
], served
|
||||
|
||||
|
||||
def test_navigation_item():
|
||||
|
||||
@@ -7,8 +7,8 @@ fail in ways nobody notices until somebody needs the password.
|
||||
|
||||
import pytest
|
||||
|
||||
from plugins.securesend.models import SecureSecret
|
||||
from plugins.securesend.api.routes import MAX_CIPHERTEXT
|
||||
from plugins.tools.models import SecureSecret
|
||||
from plugins.tools.api.routes import MAX_CIPHERTEXT
|
||||
|
||||
CIPHER = 'ZmFrZS1jaXBoZXJ0ZXh0LWZvci10ZXN0cw=='
|
||||
IV = 'MTIzNDU2Nzg5MDEy'
|
||||
@@ -17,7 +17,7 @@ IV = 'MTIzNDU2Nzg5MDEy'
|
||||
def _create(client, headers, **overrides):
|
||||
body = {'ciphertext': CIPHER, 'iv': IV}
|
||||
body.update(overrides)
|
||||
return client.post('/api/securesend', json=body, headers=headers)
|
||||
return client.post('/api/tools/secrets', json=body, headers=headers)
|
||||
|
||||
|
||||
# ------------------------------------------------------------------- creating
|
||||
@@ -25,7 +25,7 @@ def _create(client, headers, **overrides):
|
||||
def test_creating_requires_a_login(client):
|
||||
"""Anonymous creation would be an untraceable way to move data out of a
|
||||
system that otherwise records who did what."""
|
||||
resp = client.post('/api/securesend', json={'ciphertext': CIPHER, 'iv': IV})
|
||||
resp = client.post('/api/tools/secrets', json={'ciphertext': CIPHER, 'iv': IV})
|
||||
assert resp.status_code in (401, 403, 422)
|
||||
|
||||
|
||||
@@ -74,29 +74,29 @@ def test_anyone_can_read_it_without_logging_in(client, auth_headers):
|
||||
"""The recipient usually has no account here. The unguessable token is the
|
||||
authorisation."""
|
||||
token = _create(client, auth_headers).get_json()['data']['token']
|
||||
resp = client.post('/api/securesend/%s' % token)
|
||||
resp = client.post('/api/tools/secrets/%s' % token)
|
||||
assert resp.status_code == 200
|
||||
assert resp.get_json()['data']['ciphertext'] == CIPHER
|
||||
|
||||
|
||||
def test_the_second_read_finds_nothing(client, auth_headers):
|
||||
token = _create(client, auth_headers).get_json()['data']['token']
|
||||
assert client.post('/api/securesend/%s' % token).status_code == 200
|
||||
assert client.post('/api/securesend/%s' % token).status_code == 404
|
||||
assert client.post('/api/tools/secrets/%s' % token).status_code == 200
|
||||
assert client.post('/api/tools/secrets/%s' % token).status_code == 404
|
||||
|
||||
|
||||
def test_a_spent_secret_is_deleted_not_just_marked(client, db, auth_headers):
|
||||
"""A secret that has been read has no reason to keep its ciphertext."""
|
||||
token = _create(client, auth_headers).get_json()['data']['token']
|
||||
client.post('/api/securesend/%s' % token)
|
||||
client.post('/api/tools/secrets/%s' % token)
|
||||
assert SecureSecret.query.filter_by(token=token).first() is None
|
||||
|
||||
|
||||
def test_more_than_one_view_can_be_allowed(client, auth_headers):
|
||||
token = _create(client, auth_headers, views=2).get_json()['data']['token']
|
||||
assert client.post('/api/securesend/%s' % token).status_code == 200
|
||||
assert client.post('/api/securesend/%s' % token).status_code == 200
|
||||
assert client.post('/api/securesend/%s' % token).status_code == 404
|
||||
assert client.post('/api/tools/secrets/%s' % token).status_code == 200
|
||||
assert client.post('/api/tools/secrets/%s' % token).status_code == 200
|
||||
assert client.post('/api/tools/secrets/%s' % token).status_code == 404
|
||||
|
||||
|
||||
# ------------------------------------------------------- link previews (Teams)
|
||||
@@ -111,19 +111,19 @@ def test_a_get_does_not_spend_the_secret(client, auth_headers):
|
||||
"""
|
||||
token = _create(client, auth_headers).get_json()['data']['token']
|
||||
|
||||
peek = client.get('/api/securesend/%s' % token)
|
||||
peek = client.get('/api/tools/secrets/%s' % token)
|
||||
assert peek.status_code == 200
|
||||
assert peek.get_json()['data']['available'] is True
|
||||
assert 'ciphertext' not in peek.get_json()['data']
|
||||
|
||||
# Still there for the person it was meant for.
|
||||
assert client.post('/api/securesend/%s' % token).status_code == 200
|
||||
assert client.post('/api/tools/secrets/%s' % token).status_code == 200
|
||||
|
||||
|
||||
def test_peeking_at_a_spent_link_says_so_without_leaking(client, auth_headers):
|
||||
token = _create(client, auth_headers).get_json()['data']['token']
|
||||
client.post('/api/securesend/%s' % token)
|
||||
peek = client.get('/api/securesend/%s' % token)
|
||||
client.post('/api/tools/secrets/%s' % token)
|
||||
peek = client.get('/api/tools/secrets/%s' % token)
|
||||
assert peek.get_json()['data']['available'] is False
|
||||
|
||||
|
||||
@@ -137,7 +137,7 @@ def test_an_expired_secret_is_gone(client, db, auth_headers):
|
||||
- timedelta(minutes=1))
|
||||
db.session.commit()
|
||||
|
||||
assert client.post('/api/securesend/%s' % token).status_code == 404
|
||||
assert client.post('/api/tools/secrets/%s' % token).status_code == 404
|
||||
assert SecureSecret.query.filter_by(token=token).first() is None
|
||||
|
||||
|
||||
@@ -152,10 +152,10 @@ def test_an_unknown_token_answers_the_same_as_a_spent_one(client, auth_headers):
|
||||
"""Distinguishing them would confirm to anyone holding a guessed token
|
||||
whether it ever named anything."""
|
||||
token = _create(client, auth_headers).get_json()['data']['token']
|
||||
client.post('/api/securesend/%s' % token)
|
||||
client.post('/api/tools/secrets/%s' % token)
|
||||
|
||||
spent = client.post('/api/securesend/%s' % token)
|
||||
never = client.post('/api/securesend/%s' % ('f' * 32))
|
||||
spent = client.post('/api/tools/secrets/%s' % token)
|
||||
never = client.post('/api/tools/secrets/%s' % ('f' * 32))
|
||||
assert spent.status_code == never.status_code == 404
|
||||
assert (spent.get_json()['data']['error']['message']
|
||||
== never.get_json()['data']['error']['message'])
|
||||
Reference in New Issue
Block a user