test(web): verify creator and administrator permissions
Cover ownership, comment removal, moderation, channel management, settings and CSRF rejection.
This commit is contained in:
1 parent
425da88711
commit
ba7578e8ba
1 file changed
+188
@@ -0,0 +1,188 @@
|
|||||||
|
"""Creator and administrator tools enforce the same visibility on every route."""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from contextlib import ExitStack
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
from unittest.mock import patch
|
||||||
|
import io
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
import database
|
||||||
|
from database import DatabaseBackend
|
||||||
|
import server_config
|
||||||
|
import web_features
|
||||||
|
import webapp
|
||||||
|
|
||||||
|
|
||||||
|
TOKEN = "a" * 64
|
||||||
|
|
||||||
|
|
||||||
|
class Request:
|
||||||
|
def __init__(self, path, *, ip="192.0.2.9", fields=None, query=None, csrf=TOKEN):
|
||||||
|
self.path = path.encode("utf-8")
|
||||||
|
self._ip = ip
|
||||||
|
self.args = {key.encode(): [str(value).encode()] for key, value in (query or {}).items()}
|
||||||
|
if fields is not None:
|
||||||
|
self.args.update({key.encode(): [str(value).encode()] for key, value in fields.items()})
|
||||||
|
self.args[b"csrf"] = [csrf.encode()]
|
||||||
|
self.content = io.BytesIO()
|
||||||
|
self.headers = {b"host": b"localhost:8080", b"cookie": ("ose_csrf=" + TOKEN).encode()}
|
||||||
|
self.response_headers = {}
|
||||||
|
self.response_code = 200
|
||||||
|
|
||||||
|
def getClientIP(self):
|
||||||
|
return self._ip
|
||||||
|
|
||||||
|
def getAllHeaders(self):
|
||||||
|
return self.headers
|
||||||
|
|
||||||
|
def setHeader(self, key, value):
|
||||||
|
self.response_headers[key.lower()] = value
|
||||||
|
|
||||||
|
def setResponseCode(self, code):
|
||||||
|
self.response_code = code
|
||||||
|
|
||||||
|
def isSecure(self):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
class WebFeatureTests(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.stack = ExitStack()
|
||||||
|
self.addCleanup(self.stack.close)
|
||||||
|
self.root = Path(self.stack.enter_context(TemporaryDirectory()))
|
||||||
|
self.stack.enter_context(patch.dict("os.environ", {"REXIMEMO_CONFIG": str(self.root / "settings.json")}))
|
||||||
|
self.creators = self.root / "Creators"
|
||||||
|
self.comments = self.root / "Comments"
|
||||||
|
self.creators.mkdir()
|
||||||
|
self.comments.mkdir()
|
||||||
|
self.stack.enter_context(patch.object(database, "CREATORS_DIR", self.creators))
|
||||||
|
self.stack.enter_context(patch.object(database, "COMMENTS_DIR", self.comments))
|
||||||
|
self.db = DatabaseBackend(self.root / "test.sqlite3")
|
||||||
|
self.stack.callback(self.db.close)
|
||||||
|
self.stack.enter_context(patch.object(webapp, "Database", self.db))
|
||||||
|
self.stack.enter_context(patch.object(web_features, "Database", self.db))
|
||||||
|
self.router = webapp.WebRoot()
|
||||||
|
self.alice = self.db.create_account("Alice", "test password")
|
||||||
|
self.bob = self.db.create_account("Bob", "test password")
|
||||||
|
self.admin = self.db.get_account_by_username("reximemo")["id"]
|
||||||
|
for account, address in ((self.alice, "192.0.2.1"), (self.bob, "192.0.2.2"), (self.admin, "192.0.2.3")):
|
||||||
|
self.db.login_ip(address, account)
|
||||||
|
room = self.db.get_or_create_creator("192.0.2.1", self.alice, fsid="A000000000000001")
|
||||||
|
with self.db.conn:
|
||||||
|
cur = self.db.conn.execute(
|
||||||
|
"INSERT INTO flipnotes(public_id,creator_room_id,creator_fsid,filename,title,channel_id,posted_at) VALUES(?,?,?,?,?,?,?)",
|
||||||
|
("f_test", room["id"], room["fsid"], "sample", "Original title", 1, 100),
|
||||||
|
)
|
||||||
|
self.note_id = cur.lastrowid
|
||||||
|
path = self.db.FlipnotePath(room["fsid"], "sample")
|
||||||
|
path.parent.mkdir(parents=True)
|
||||||
|
path.write_bytes(b"Original PPM bytes")
|
||||||
|
|
||||||
|
def get(self, path, *, ip="192.0.2.9", query=None):
|
||||||
|
request = Request(path, ip=ip, query=query)
|
||||||
|
body = self.router.render_GET(request)
|
||||||
|
return request, body
|
||||||
|
|
||||||
|
def post(self, path, fields=None, *, ip="192.0.2.3", csrf=TOKEN):
|
||||||
|
request = Request(path, ip=ip, fields=fields or {}, csrf=csrf)
|
||||||
|
body = self.router.render_POST(request)
|
||||||
|
return request, body
|
||||||
|
|
||||||
|
def test_creator_can_edit_but_another_account_cannot(self):
|
||||||
|
fields = {"title": "Edited title", "description": "A description", "channel_id": 1}
|
||||||
|
denied, _ = self.post("/watch/f_test/manage", fields, ip="192.0.2.2")
|
||||||
|
self.assertEqual(denied.response_code, 403)
|
||||||
|
self.assertEqual(self.db.get_flipnote_by_id(self.note_id)["title"], "Original title")
|
||||||
|
allowed, _ = self.post("/watch/f_test/manage", fields, ip="192.0.2.1")
|
||||||
|
self.assertEqual(allowed.response_code, 303)
|
||||||
|
self.assertEqual(self.db.get_flipnote_by_id(self.note_id)["title"], "Edited title")
|
||||||
|
self.assertEqual(self.db.get_flipnote_by_id(self.note_id)["description"], "A description")
|
||||||
|
self.assertEqual(self.get("/watch/f_test/manage", ip="192.0.2.2")[0].response_code, 403)
|
||||||
|
self.assertEqual(self.get("/watch/f_test/manage", ip="192.0.2.1")[0].response_code, 200)
|
||||||
|
|
||||||
|
def test_only_commenter_or_administrator_can_remove_comment(self):
|
||||||
|
comment = self.db.add_text_comment(self.note_id, "Bob's comment", account_id=self.bob)
|
||||||
|
denied, _ = self.post(f"/comments/{comment}/delete", ip="192.0.2.1")
|
||||||
|
self.assertEqual(denied.response_code, 403)
|
||||||
|
self.assertIsNotNone(self.db.get_comment(comment))
|
||||||
|
allowed, _ = self.post(f"/comments/{comment}/delete", ip="192.0.2.2")
|
||||||
|
self.assertEqual(allowed.response_code, 303)
|
||||||
|
self.assertIsNone(self.db.get_comment(comment))
|
||||||
|
|
||||||
|
def test_moderation_hides_public_pages_and_media_but_staff_can_review(self):
|
||||||
|
note = self.db.get_flipnote_by_id(self.note_id)
|
||||||
|
request, _ = self.post("/admin/flipnote-visibility", {"id": self.note_id, "hidden": 1, "reason": "Review"})
|
||||||
|
self.assertEqual(request.response_code, 303)
|
||||||
|
with patch.object(webapp, "ppm_thumbnail_png", return_value=b"PNG preview"):
|
||||||
|
for path in ("/watch/f_test", "/media/f_test.ppm", "/flipnote/f_test.ppm", "/thumb/f_test.png"):
|
||||||
|
with self.subTest(path=path):
|
||||||
|
public, _ = self.get(path)
|
||||||
|
owner, _ = self.get(path, ip="192.0.2.1")
|
||||||
|
reviewer, body = self.get(path, ip="192.0.2.3")
|
||||||
|
self.assertEqual(public.response_code, 404)
|
||||||
|
self.assertEqual(owner.response_code, 404)
|
||||||
|
self.assertEqual(reviewer.response_code, 200)
|
||||||
|
self.assertIn(b"no-store", reviewer.response_headers[b"cache-control"])
|
||||||
|
self.assertTrue(body)
|
||||||
|
public, body = self.get("/browse")
|
||||||
|
self.assertNotIn(note["public_id"].encode(), body)
|
||||||
|
admin, body = self.get("/browse", ip="192.0.2.3")
|
||||||
|
self.assertIn(b"hidden-content", body)
|
||||||
|
self.assertIn(b"f_test", body)
|
||||||
|
request, _ = self.post("/admin/flipnote-visibility", {"id": self.note_id, "hidden": 0})
|
||||||
|
self.assertEqual(request.response_code, 303)
|
||||||
|
self.assertEqual(self.get("/media/f_test.ppm")[1], b"Original PPM bytes")
|
||||||
|
|
||||||
|
def test_administrator_tabs_render_and_regular_accounts_are_denied(self):
|
||||||
|
self.db.add_text_comment(self.note_id, "Example comment", account_id=self.bob)
|
||||||
|
for tab, heading in (("flipnotes", "Flipnotes"), ("comments", "Example comment"),
|
||||||
|
("channels", "New channel"), ("server", "Server appearance"), ("bans", "Block an IP")):
|
||||||
|
with self.subTest(tab=tab):
|
||||||
|
admin, body = self.get("/admin", ip="192.0.2.3", query={"tab": tab})
|
||||||
|
self.assertEqual(admin.response_code, 200)
|
||||||
|
self.assertIn(heading.encode(), body)
|
||||||
|
self.assertIn(b'name="csrf"', body)
|
||||||
|
regular, _ = self.get("/admin", ip="192.0.2.1", query={"tab": tab})
|
||||||
|
self.assertEqual(regular.response_code, 403)
|
||||||
|
|
||||||
|
def test_channel_removal_moves_notes_and_preserves_default_channel(self):
|
||||||
|
channel = self.db.create_channel("Animations", "Drawn stories")
|
||||||
|
self.db.update_flipnote(self.note_id, "Film", channel_id=channel)
|
||||||
|
response, _ = self.post("/admin/channel-delete", {"id": channel, "target_channel_id": 1})
|
||||||
|
self.assertEqual(response.response_code, 303)
|
||||||
|
self.assertIsNone(self.db.get_channel(channel))
|
||||||
|
self.assertEqual(self.db.get_flipnote_by_id(self.note_id)["channel_id"], 1)
|
||||||
|
protected, _ = self.post("/admin/channel-delete", {"id": 1, "target_channel_id": 1})
|
||||||
|
self.assertEqual(protected.response_code, 400)
|
||||||
|
self.assertIsNotNone(self.db.get_channel(1))
|
||||||
|
|
||||||
|
def test_server_settings_save_and_guest_posting_setting_is_enforced(self):
|
||||||
|
fields = {"site_name": "My Flipnote Server", "tagline": "Our drawings", "welcome_message": "Welcome!",
|
||||||
|
"accent_colour": "#123456", "max_upload_kib": 2048, "registration_enabled": 1}
|
||||||
|
request, _ = self.post("/admin/server", fields)
|
||||||
|
self.assertEqual(request.response_code, 303)
|
||||||
|
config = server_config.load_config()
|
||||||
|
self.assertEqual(config["site_name"], "My Flipnote Server")
|
||||||
|
self.assertFalse(config["guest_posting"])
|
||||||
|
public, body = self.get("/")
|
||||||
|
self.assertIn(b"My Flipnote Server", body)
|
||||||
|
rejected, _ = self.post("/watch/f_test/comment", {"text": "Guest comment"}, ip="192.0.2.9")
|
||||||
|
self.assertEqual(rejected.response_code, 403)
|
||||||
|
allowed, _ = self.post("/watch/f_test/comment", {"text": "Signed in comment"}, ip="192.0.2.1")
|
||||||
|
self.assertEqual(allowed.response_code, 303)
|
||||||
|
self.assertEqual(self.db.count_comments(self.note_id), 1)
|
||||||
|
|
||||||
|
def test_invalid_csrf_rejects_creator_and_staff_mutations(self):
|
||||||
|
fields = {"title": "Bad change", "channel_id": 1}
|
||||||
|
rejected, _ = self.post("/watch/f_test/manage", fields, ip="192.0.2.1", csrf="b" * 64)
|
||||||
|
self.assertEqual(rejected.response_code, 403)
|
||||||
|
rejected, _ = self.post("/admin/flipnote-visibility", {"id": self.note_id, "hidden": 1}, csrf="")
|
||||||
|
self.assertEqual(rejected.response_code, 403)
|
||||||
|
self.assertFalse(self.db.get_flipnote_by_id(self.note_id)["hidden"])
|
||||||
|
self.assertEqual(self.db.get_flipnote_by_id(self.note_id)["title"], "Original title")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in new issue
Block a user
