100 lines
3.9 KiB
Python
100 lines
3.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Permission surfacing + enforcement tests for encrypted PDFs.
|
|
|
|
- Engine surfaces encryption + permission flags (FPDF_GetDocPermissions / revision).
|
|
- Gateway exposes them on the document response and ENFORCES forbidden edit ops (403).
|
|
|
|
Run: gateway/.venv/Scripts/python.exe tests/security/test_permissions.py
|
|
Requires the restricted fixture corpus/edge-cases/restricted.pdf (generated below if missing).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parents[2]
|
|
sys.path.insert(0, str(ROOT / "gateway"))
|
|
import pdfengine # noqa: E402
|
|
|
|
RESTRICTED = ROOT / "corpus" / "edge-cases" / "restricted.pdf"
|
|
NORMAL = ROOT / "corpus" / "fonts" / "utf-8.pdf"
|
|
|
|
|
|
def _ensure_fixture():
|
|
if RESTRICTED.exists():
|
|
return
|
|
import pikepdf # noqa: PLC0415
|
|
src = pikepdf.open(NORMAL)
|
|
perm = pikepdf.Permissions(extract=False, print_highres=False, print_lowres=False,
|
|
modify_other=False, modify_annotation=False)
|
|
src.save(RESTRICTED, encryption=pikepdf.Encryption(owner="owner", user="", R=6, allow=perm))
|
|
|
|
|
|
def test_engine_surfaces_permissions():
|
|
p = pdfengine.PdfDocument.load_from_memory(RESTRICTED.read_bytes(), "").permissions
|
|
assert p.is_encrypted and p.encryption == "AES-256" and p.security_revision == 6
|
|
assert not p.owner_unlocked
|
|
assert p.can_copy is False and p.can_print is False and p.can_modify is False and p.can_annotate is False
|
|
assert p.can_fill_forms is True # not denied in the fixture
|
|
|
|
n = pdfengine.PdfDocument.load_from_memory(NORMAL.read_bytes(), "").permissions
|
|
assert not n.is_encrypted and n.encryption == "None"
|
|
assert n.can_copy and n.can_print and n.can_modify and n.can_annotate
|
|
print(" ok engine surfaces AES-256 + correct flags (restricted) and all-true (normal)")
|
|
|
|
|
|
def test_gateway_surface_and_enforce():
|
|
from app.services.store import document_store # noqa: PLC0415
|
|
from app.routers.documents import make_document_response # noqa: PLC0415
|
|
from app.routers.edits import apply_edits_impl, EditsRequest # noqa: PLC0415
|
|
from fastapi import HTTPException # noqa: PLC0415
|
|
|
|
data = RESTRICTED.read_bytes()
|
|
doc = pdfengine.PdfDocument.load_from_memory(data, "")
|
|
info = document_store.add_document("restricted.pdf", data, doc)
|
|
|
|
# surface
|
|
resp = make_document_response(info)
|
|
assert resp.permissions.isEncrypted and resp.permissions.encryption == "AES-256"
|
|
assert resp.permissions.canModify is False and resp.permissions.canAnnotate is False
|
|
|
|
def apply(op):
|
|
apply_edits_impl(info["id"], EditsRequest.model_validate({"version": "1.0", "operations": [op]}))
|
|
|
|
# forbidden -> 403
|
|
forbidden = {"id": "a", "type": "replace_text", "pageIndex": 0,
|
|
"data": {"objectIndices": [0], "text": "x", "internalFontId": "F", "fontSize": 12.0}}
|
|
try:
|
|
apply(forbidden)
|
|
raise AssertionError("forbidden replace_text should have been rejected")
|
|
except HTTPException as ex:
|
|
assert ex.status_code == 403, f"expected 403, got {ex.status_code}"
|
|
|
|
# allowed (fill forms) -> not a 403
|
|
allowed = {"id": "b", "type": "update_field", "pageIndex": 0,
|
|
"data": {"value": "hi", "annotationId": "missing"}}
|
|
try:
|
|
apply(allowed)
|
|
except HTTPException as ex:
|
|
assert ex.status_code != 403, "fill-forms is allowed; should not be 403"
|
|
print(" ok gateway surfaces permissions + enforces 403 on forbidden op, allows permitted op")
|
|
|
|
|
|
def main() -> int:
|
|
_ensure_fixture()
|
|
tests = [test_engine_surfaces_permissions, test_gateway_surface_and_enforce]
|
|
failed = 0
|
|
for t in tests:
|
|
try:
|
|
t()
|
|
except AssertionError as exc:
|
|
print(f" FAIL {t.__name__}: {exc}")
|
|
failed += 1
|
|
print(f"\n{len(tests) - failed}/{len(tests)} passed.")
|
|
return 1 if failed else 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|