iCal (.ics) import / subscription for personal events #51

Closed
opened 2026-07-15 15:33:13 +00:00 by claude-bot · 1 comment
Contributor

Complement to #16 (iCal export): let users bring birthdays, anniversaries, and family events in from their own calendars as user-defined events (priority 100).

  • Upload a .ics file or subscribe to a URL (refreshed weekly by the scheduler)
  • Map VEVENTs → user events; derive stable event_key from the UID; handle yearly RRULEs via existing recurrence machinery
  • Imported events go through the normal candidate → review → approve flow
  • De-dup on re-import/refresh by UID
Complement to #16 (iCal *export*): let users bring birthdays, anniversaries, and family events in from their own calendars as user-defined events (priority 100). - [ ] Upload a `.ics` file or subscribe to a URL (refreshed weekly by the scheduler) - [ ] Map VEVENTs → user events; derive stable `event_key` from the UID; handle yearly RRULEs via existing recurrence machinery - [ ] Imported events go through the normal candidate → review → approve flow - [ ] De-dup on re-import/refresh by UID
claude-bot added this to the v1.1.0 milestone 2026-07-15 15:33:13 +00:00
Author
Contributor

Done — #121 merged, CI green.

  • Upload a .ics or subscribe to a URL — subscriptions refresh with the weekly candidate job, so an event added to a personal calendar this week gets a candidate scheme on the same run.
  • VEVENTs → user events, stable event_key from UID, yearly RRULEs
  • Normal candidate → review → approve flowgenerate_schemes_for_all_events already covers every event in the table regardless of source.
  • De-dup on re-import/refresh by UID

De-dup needed no schema change

Event ids are already {event_key}_{year}, so deriving event_key deterministically from the UID makes a re-import collide on the primary key by construction — no UID column, and no second uniqueness rule to keep in step with the first. UIDs are hashed rather than sanitised, since real ones carry characters that are not id-safe and no meaning worth preserving.

The detail that would have bitten quietly

priority_resolver._SOURCE_RANK defaults an unrecognised source to 0, below system. Adding "ical" without registering it there would have made every imported birthday lose a priority tie against a builtin holiday, silently. Registered level with user; two tests fail if the entry is removed, which I checked rather than assumed.

Upload and subscription differ on purpose

A subscription is authoritative — a refresh overwrites, because the remote calendar is where those events are edited. An upload never overwrites, because nothing will refresh it. Neither touches an event whose source is not ical. Deleting a subscription keeps its events: they may already have approved schemes.

Security review findings, all fixed in the same PR

A background review of the first commit found three issues, all real and all in new code:

  • SSRFfetch_ics fetched an operator-supplied URL server-side with redirects followed automatically, reaching sibling containers, host-only services and cloud metadata the operator may not be able to reach; parse failures land in last_error and render in the UI, so a blocked service's response could leak back through the error text. Now http(s) only, the host must resolve entirely to global addresses, and redirects are followed by hand so each hop is re-validated — an allowed host that 302s to a link-local address would otherwise walk straight past a check on the original URL. DNS rebinding remains possible; closing it needs the connection pinned to a vetted IP, which httpx does not expose cleanly. That limitation is stated in the docstring rather than implied away.
  • The size cap capped nothingMAX_ICS_BYTES was compared against resp.content, which materialises the whole body first. Now streamed and abandoned once the cap is passed.
  • Credential exposureGET /subscriptions was require_viewer. Private Google and Apple calendar links embed a secret in the path, so listing them handed viewers read access to someone's entire personal calendar. Now require_admin.

Resolution sits behind a seam so the guard is testable without the network. The stub resolves literals to themselves rather than mapping everything to a public address — a stub that mapped everything would let every private literal walk through the guard and make those tests worse than useless. Verified by neutering the guard: all seven SSRF cases fail without it.

Done — #121 merged, CI green. - [x] **Upload a `.ics` or subscribe to a URL** — subscriptions refresh with the weekly candidate job, so an event added to a personal calendar this week gets a candidate scheme on the same run. - [x] **VEVENTs → user events, stable `event_key` from UID, yearly RRULEs** - [x] **Normal candidate → review → approve flow** — `generate_schemes_for_all_events` already covers every event in the table regardless of source. - [x] **De-dup on re-import/refresh by UID** ### De-dup needed no schema change Event ids are already `{event_key}_{year}`, so deriving `event_key` deterministically from the UID makes a re-import collide on the **primary key by construction** — no UID column, and no second uniqueness rule to keep in step with the first. UIDs are hashed rather than sanitised, since real ones carry characters that are not id-safe and no meaning worth preserving. ### The detail that would have bitten quietly `priority_resolver._SOURCE_RANK` defaults an unrecognised source to **0**, below `system`. Adding `"ical"` without registering it there would have made every imported birthday lose a priority tie against a builtin holiday, silently. Registered level with `user`; two tests fail if the entry is removed, which I checked rather than assumed. ### Upload and subscription differ on purpose A subscription is authoritative — a refresh overwrites, because the remote calendar is where those events are edited. An upload never overwrites, because nothing will refresh it. Neither touches an event whose source is not `ical`. Deleting a subscription keeps its events: they may already have approved schemes. ### Security review findings, all fixed in the same PR A background review of the first commit found three issues, all real and all in new code: - **SSRF** — `fetch_ics` fetched an operator-supplied URL server-side with redirects followed automatically, reaching sibling containers, host-only services and cloud metadata the operator may not be able to reach; parse failures land in `last_error` and render in the UI, so a blocked service's response could leak back through the error text. Now http(s) only, the host must resolve entirely to global addresses, and redirects are followed by hand so **each hop is re-validated** — an allowed host that 302s to a link-local address would otherwise walk straight past a check on the original URL. DNS rebinding remains possible; closing it needs the connection pinned to a vetted IP, which httpx does not expose cleanly. That limitation is stated in the docstring rather than implied away. - **The size cap capped nothing** — `MAX_ICS_BYTES` was compared against `resp.content`, which materialises the whole body first. Now streamed and abandoned once the cap is passed. - **Credential exposure** — `GET /subscriptions` was `require_viewer`. Private Google and Apple calendar links embed a secret in the path, so listing them handed viewers read access to someone's entire personal calendar. Now `require_admin`. Resolution sits behind a seam so the guard is testable without the network. The stub resolves literals to themselves rather than mapping everything to a public address — a stub that mapped everything would let every private literal walk through the guard and make those tests worse than useless. Verified by neutering the guard: all seven SSRF cases fail without it.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Iris-WLED#51
No description provided.