seeded events are the site's; every write lands in the action log
calendar_write.owns() now accepts both site namespaces: the 32 events the 2026-08-29 seed stamped site73-<sha1>@greenlanescouts73.org and the site's own @site73.greenlanescouts73.org. The rule exists to keep the site off @band.us and off anything a person adds in a calendar client, not off its own data. Seeded objects are written back at the seeder's object name so an edit replaces in place; proven on a throwaway against the real store (edit, still 32 objects, restored byte-for-byte). auth_events becomes the one action log. Every P2 write - nearby create, update, deactivate; unit meeting edit; setting change; announcement post and take-down - now records who, when, and a one-line before -> after for the fields that changed, alongside the login, key and calendar entries already there. GET /api/admin/history (history:read, admin and above, not scopable on a key) reads it newest first with a kind prefix filter and before= paging; rowid breaks second-resolution ties. tests/smoke_admin.py 102 -> 112.
This commit is contained in:
+41
-2
@@ -238,7 +238,7 @@ check("timed multi-day with no end_time ends at noon on the end date, as seed.py
|
||||
"DTEND;TZID=America/New_York:20261018T120000" in multi)
|
||||
longt = C.build_ics(uid, C.clean({"title": "A" * 120, "date": "2026-10-03"}), stamp="20260904T000000Z").decode()
|
||||
check("long lines are folded at 75 octets", all(len(l.encode()) <= 75 for l in longt.split("\r\n")))
|
||||
check("ownership by suffix", C.owns("x" + C.UID_SUFFIX) and not C.owns("site73-abc@greenlanescouts73.org")
|
||||
check("ownership by suffix", C.owns("x" + C.UID_SUFFIX) and not C.owns("abc@greenlanescouts73.org")
|
||||
and not C.owns("x@band.us") and not C.owns(None))
|
||||
check("new uids are owned and unique", C.owns(C.new_uid()) and C.new_uid() != C.new_uid())
|
||||
check("slug is stable and marked", C.slug("a" + C.UID_SUFFIX).startswith("site-") and C.slug("a" + C.UID_SUFFIX) == C.slug("a" + C.UID_SUFFIX))
|
||||
@@ -255,13 +255,52 @@ C.RADICALE_URL = ""; C.RADICALE_USER = ""; C.RADICALE_PASS = ""
|
||||
raises("unconfigured put is 503", 503, C.CalendarRejected, C.put_event, uid, ev)
|
||||
check("and nothing was sent", calls == [])
|
||||
C.RADICALE_URL = "http://radicale.test/scouts/site73/"; C.RADICALE_USER = "scoutsite"; C.RADICALE_PASS = "p"
|
||||
raises("foreign uid put is 403", 403, C.CalendarRejected, C.put_event, "site73-abc@greenlanescouts73.org", ev)
|
||||
raises("foreign uid put is 403", 403, C.CalendarRejected, C.put_event, "abc@greenlanescouts73.org", ev)
|
||||
raises("foreign uid delete is 403", 403, C.CalendarRejected, C.delete_event, "x@band.us")
|
||||
check("still nothing sent for foreign uids", calls == [])
|
||||
check("owned put goes to the slug with auth", C.put_event(uid, ev) == 201 and calls[-1][0] == "PUT"
|
||||
and calls[-1][1] == "http://radicale.test/scouts/site73/" + C.slug(uid) and calls[-1][2])
|
||||
check("owned delete", C.delete_event(uid) == 201 and calls[-1][0] == "DELETE")
|
||||
|
||||
print("\nseeded events are the site's too")
|
||||
check("both namespaces owned, nothing else", C.owns("x" + C.UID_SUFFIX) and C.owns("site73-abc123@greenlanescouts73.org")
|
||||
and not C.owns("abc@greenlanescouts73.org") and not C.owns("x@band.us") and not C.owns("site73-x@site73.example"))
|
||||
check("object name per namespace", C.object_name("a" + C.UID_SUFFIX) == C.slug("a" + C.UID_SUFFIX)
|
||||
and C.object_name("site73-abc@greenlanescouts73.org") == "site73-abc%40greenlanescouts73.org.ics")
|
||||
calls.clear(); C.put_event("site73-abc@greenlanescouts73.org", ev)
|
||||
check("seeded put goes to the seeder's object name", calls[-1][1].endswith("/site73-abc%40greenlanescouts73.org.ics"))
|
||||
|
||||
print("\naction log")
|
||||
class _Sess:
|
||||
def __init__(self, tok):
|
||||
self.headers = {"x-forwarded-for": "10.0.0.5"}; self.cookies = {"s73_session": tok}; self.client = None
|
||||
req = _Sess(I.start_session(pid))
|
||||
n0 = len(I.list_events(limit=500))
|
||||
A.update_unit(req, "pack73", {"meets_time": "18:15"}, None)
|
||||
A.update_unit(req, "pack73", {"meets_time": "18:00"}, None)
|
||||
A.put_setting(req, "nearby_source_name", {"value": "Test source"}, None)
|
||||
A.put_setting(req, "nearby_source_name", {"value": None}, None)
|
||||
nb = A.create_nearby(req, {"unit_type": "pack", "unit_number": "ZZ1", "town": "A"}, None)
|
||||
A.update_nearby(req, nb["id"], {"town": "B"}, None)
|
||||
A.deactivate_nearby(req, nb["id"], None)
|
||||
an = A.create_announcement(req, {"message": "log me", "ends_at": "2036-01-02T00:00:00+00:00"}, None)
|
||||
A.revoke_announcement(req, an["id"], None)
|
||||
ev_rows = I.list_events(limit=500)
|
||||
kinds = [e["kind"] for e in ev_rows[:len(ev_rows) - n0]]
|
||||
check("every P2 write lands in the log", set(kinds) >= {"unit.updated", "setting.updated", "nearby.created",
|
||||
"nearby.updated", "nearby.deactivated", "announcement.created", "announcement.revoked"})
|
||||
check("attributed to the person", all(e["actor_email"] == "lanrule@example.test" for e in ev_rows[:len(ev_rows) - n0]))
|
||||
unit_ev = [e for e in ev_rows if e["kind"] == "unit.updated"][-1]
|
||||
check("diff is before -> after", "-> '18:15'" in unit_ev["detail"] and "meets_time:" in unit_ev["detail"])
|
||||
nb_ev = [e for e in ev_rows if e["kind"] == "nearby.updated"][0]
|
||||
check("nearby diff names the field", "town: 'A' -> 'B'" in nb_ev["detail"])
|
||||
set_ev = [e for e in ev_rows if e["kind"] == "setting.updated"][0]
|
||||
check("clearing a setting is said so", "cleared to default" in set_ev["detail"])
|
||||
check("kind prefix filter and limit", all(e["kind"].startswith("nearby.") for e in I.list_events(kind_prefix="nearby."))
|
||||
and len(I.list_events(limit=2)) == 2)
|
||||
check("history is admin and above, and not scopable on a key", "history:read" in I.CAPS["admin"]
|
||||
and "history:read" not in I.CAPS["leader"] and "history:read" in I.KEY_UNSCOPABLE)
|
||||
|
||||
print("\napi docs registry")
|
||||
import admin_api as A
|
||||
reg = A.describe_routes()
|
||||
|
||||
Reference in New Issue
Block a user