Files
pdf/tests/security/test_permissions.py
2026-06-22 15:18:47 +05:30

96 lines
3.7 KiB
Python

"""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
RESTRICTED = ROOT / "corpus" / "edge-cases" / "restricted.pdf"
NORMAL = ROOT / "corpus" / "fonts" / "utf-8.pdf"
def _ensure_fixture():
if RESTRICTED.exists():
return
import pikepdf
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
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
from app.routers.documents import make_document_response
from app.routers.edits import apply_edits_impl, EditsRequest
from fastapi import HTTPException
data = RESTRICTED.read_bytes()
doc = pdfengine.PdfDocument.load_from_memory(data, "")
info = document_store.add_document("restricted.pdf", data, doc)
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 = {"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 = {"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())