"""The dashboard's entry tickets, made by itsdangerous itself (plan §8.5). The dashboard signs a ticket with itsdangerous' URLSafeTimedSerializer (the same as Scratch's), server F checks it in Node. This writes tickets as the dashboard makes them, with a fixed clock, and what itsdangerous answers when it reads each one back at a given moment: the payload, or which error. packages/server/test/tickets.python.json holds the result; the server's tests read every ticket with the Node check and expect the same answer. """ import json import sys import itsdangerous from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer from itsdangerous.timed import TimestampSigner SECRET = "filius-test-secret" SALT = "mdg-filius-ticket" TTL = 60 SIGNED_AT = 1_790_000_000 clock = {"now": SIGNED_AT} TimestampSigner.get_timestamp = lambda self: clock["now"] PERSON = "0b8f5f2e-1a3c-4d5e-8f90-123456789abc" PROJECT = "6f1d2c3b-4a59-4e8d-9c7b-a1b2c3d4e5f6" def claims(**more): base = {"u": PERSON, "n": "ab12", "d": "Ada Beispiel", "c": "#3366cc", "lang": "de", "p": PROJECT, "lvl": "write"} base.update(more) return base def sign(payload, secret=SECRET, salt=SALT, at=SIGNED_AT): clock["now"] = at return URLSafeTimedSerializer(secret, salt=salt).dumps(payload) def answer(ticket, now): clock["now"] = now try: return {"payload": URLSafeTimedSerializer(SECRET, salt=SALT).loads(ticket, max_age=TTL)} except SignatureExpired: return {"error": "expired"} except BadSignature: return {"error": "badSignature"} def tampered(ticket): # One letter of the payload changed (the first after its first two). letter = ticket[2] return ticket[:2] + ("A" if letter != "A" else "B") + ticket[3:] def main(out): plain = sign(claims()) long_name = "Maximiliane Sophie Charlotte von und zu Beispielhausen-Musterstein" cases = [ ("plain", plain, SIGNED_AT), ("readLevel", sign(claims(lvl="read")), SIGNED_AT), ("umlautsAndEmoji", sign(claims(d="Jörg Ömer Weiß 🐧", lang="en")), SIGNED_AT), ("compressed", sign(claims(d=long_name, n=long_name.lower().replace(" ", "."))), SIGNED_AT), ("sixtySecondsOld", plain, SIGNED_AT + 60), ("sixtyOneSecondsOld", plain, SIGNED_AT + 61), ("fromTheFuture", plain, SIGNED_AT - 1), ("fiveSecondsAhead", plain, SIGNED_AT - 5), ("sixSecondsAhead", plain, SIGNED_AT - 6), ("otherSecret", sign(claims(), secret="another-secret"), SIGNED_AT), ("scratchSalt", sign(claims(), salt="mdg-scratch-ticket"), SIGNED_AT), ("tamperedPayload", tampered(plain), SIGNED_AT), ("noSignature", plain.rsplit(".", 1)[0], SIGNED_AT), ("garbage", "not a ticket", SIGNED_AT), ("empty", "", SIGNED_AT), ("listPayload", sign(["u", "p"]), SIGNED_AT), ("withoutLevel", sign({k: v for k, v in claims().items() if k != "lvl"}), SIGNED_AT), ("unknownLevel", sign(claims(lvl="admin")), SIGNED_AT), ("projectNotAUuid", sign(claims(p="../etc")), SIGNED_AT), ("withoutName", sign({k: v for k, v in claims().items() if k not in ("n", "d")}), SIGNED_AT), ("onlyRequired", sign({"u": PERSON, "n": "ab12", "p": PROJECT, "lvl": "read"}), SIGNED_AT), ] result = { "itsdangerous": __import__("importlib.metadata").metadata.version("itsdangerous"), "python": sys.version.split()[0], "secret": SECRET, "salt": SALT, "ttl": TTL, "signedAt": SIGNED_AT, "cases": [{"name": name, "ticket": ticket, "now": now, **answer(ticket, now)} for name, ticket, now in cases], } with open(out, "w", encoding="utf-8") as file: json.dump(result, file, ensure_ascii=False, indent=2) file.write("\n") if __name__ == "__main__": main(sys.argv[1])