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:
+22
-3
@@ -25,10 +25,16 @@ import datetime
|
||||
import hashlib
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
import uuid
|
||||
|
||||
UID_SUFFIX = "@site73.greenlanescouts73.org"
|
||||
# The 2026-08-29 seed stamped its 32 events site73-<sha1>@greenlanescouts73.org
|
||||
# before this namespace existed. They are the site's own data too; the rule
|
||||
# exists to keep the site off @band.us and off anything a person adds in a
|
||||
# calendar client, not off itself.
|
||||
SEED_PREFIX, SEED_SUFFIX = "site73-", "@greenlanescouts73.org"
|
||||
TZID = "America/New_York"
|
||||
UNITS = ("pack", "troop", "both")
|
||||
DEFAULT_TIMED_MINUTES = 90
|
||||
@@ -70,7 +76,19 @@ def configured():
|
||||
|
||||
|
||||
def owns(uid):
|
||||
return bool(uid) and str(uid).endswith(UID_SUFFIX)
|
||||
u = str(uid or "")
|
||||
return bool(u) and (u.endswith(UID_SUFFIX) or (u.startswith(SEED_PREFIX) and u.endswith(SEED_SUFFIX)))
|
||||
|
||||
|
||||
def object_name(uid):
|
||||
"""The CalDAV object the UID lives in. Seeded events sit at <uid>.ics as
|
||||
the seeder wrote them; the site's own at a hashed site-*.ics. Getting
|
||||
this wrong would create a second object with the same UID, which Radicale
|
||||
refuses, so the two forms are tested separately."""
|
||||
u = str(uid)
|
||||
if u.endswith(UID_SUFFIX):
|
||||
return slug(u)
|
||||
return urllib.parse.quote(u + ".ics", safe="")
|
||||
|
||||
|
||||
def new_uid():
|
||||
@@ -219,8 +237,9 @@ def _request(method, uid, data=None):
|
||||
if not configured():
|
||||
raise CalendarRejected(503, "calendar write-back is not configured (RADICALE_URL/USER/PASS)")
|
||||
if not owns(uid):
|
||||
raise CalendarRejected(403, "the site only manages events it created (UIDs ending %s)" % UID_SUFFIX)
|
||||
url = RADICALE_URL.rstrip("/") + "/" + slug(uid)
|
||||
raise CalendarRejected(403, "the site only manages its own events (%s or the seeded %s...%s)"
|
||||
% (UID_SUFFIX, SEED_PREFIX, SEED_SUFFIX))
|
||||
url = RADICALE_URL.rstrip("/") + "/" + object_name(uid)
|
||||
req = urllib.request.Request(url, data=data, method=method)
|
||||
req.add_header("Authorization", "Basic " + base64.b64encode(
|
||||
("%s:%s" % (RADICALE_USER, RADICALE_PASS)).encode()).decode())
|
||||
|
||||
Reference in New Issue
Block a user