Server F checks the dashboard's entry ticket as itsdangerous does (tools/ticket-oracle: tickets made by itsdangerous 2.2.0 itself), redeems it at /__auth for a session cookie (only the key's SHA-256 is kept) and decides at /sync by the session's grant: none, read or write; a frozen project is read by everyone; every refusal names its reason (one list in packages/protocol/src/sync.ts). A WebSocket from a page of another origin is refused. /svc/* with the service key makes, deletes, freezes and thaws projects, ends a person's grant or all their sessions (connections let go) and lists the sessions. Snapshots, restoring and copying move to F6 (they need the canonical doc.json and an epoch). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
96 lines
3.7 KiB
Python
96 lines
3.7 KiB
Python
"""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),
|
|
("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])
|