Prima Get an API key

Module kit / Calendar and booking

Calendar Connection

calendar-booking/calendar-connection

Kind backend
Domain calendar-booking
Maturity prod
Reusable extract
Stack fastapi-0.118, sqlalchemy-2.0-sync, postgres, cryptography-46, google-auth-oauthlib-1.2, google-auth-2.41, google-api-python-client-2.154, icalendar-6.1, recurring-ical-events-3.5, caldav-1.4, requests-2.32, pydantic-2.12
Depends on _shared/fastapi-project-layout, _shared/auth-dependency-pattern, _shared/alembic-migrations, _shared/error-envelope
Frontends calendar-booking/ui/calendar-settings-panel
Reference implementation
  • alchemy_labs/backend/services/calendar_integration_service.py
  • alchemy_labs/backend/api/routers/calendar_router.py
  • alchemy_labs/backend/models/calendar_integration_model.py
  • alchemy_labs/backend/services/calendar/ical_provider.py
  • alchemy_labs/backend/services/calendar/caldav_provider.py
  • alchemy_labs/backend/migrations/versions/20260427b_add_ical_caldav.py
  • alchemy_labs/backend/tests/v2/coverage/test_booking_g3_workspace_tenant_integration_endpoints_ical_caldav_connect_access_control.py
  • alchemy_labs/backend/tests/v2/coverage/test_booking_g2_admin_platform_integration_meeting_type_blocked_time_booking_management.py
  • alchemy_labs/backend/tests/v2/test_f4_oauth_encryption.py
Measured on alchemy_labs main 62ba4fc41009b454e1c033e5036c7e8577db2061 2026-09-12
Version 1

Purpose

An admin or a workspace member connects a bookable calendar (Google via OAuth, a read-only iCal subscription URL, or a read+write CalDAV calendar) or disconnects one, so the booking system this domain also covers has a source of busy periods and, for Google/CalDAV, somewhere to write a confirmed appointment. The invariant that makes it trustworthy: every credential that can reach a calendar (Google's OAuth token pair, an iCal feed's bearer/basic/query credential, a CalDAV app password) is written through one shared Fernet cipher before it touches the database (CalendarIntegrationService.cipher, calendar_integration_service.py 70, 137 to 143), and a saved iCal or CalDAV connection can always be checked, synchronously and statelessly, against the real feed or server before or after saving (/integrations/test-ical, /integrations/test-caldav).

When to use

Trigger phrases: "connect Google Calendar", "connect iCal feed", "connect CalDAV", "calendar OAuth", "disconnect calendar", "platform integration" vs "workspace integration", feed_auth_type, CALENDAR_ENCRYPTION_KEY, test-ical, test-caldav, provider_account_id, is_platform_level, "why does the calendar say disconnected but the feed URL is still there".

Not this recipe: availability computation (get_available_slots/get_availability_slots, calendar_integration_service.py 517 to 1044) and the booking/cancel flow (create_booking/cancel_booking, 1050 to 1320, 1566 to 1621) that CONSUME an already-connected integration, a neighbouring recipe on the same service file and router; meeting-type CRUD (BookingMeetingType, /admin/meeting-types, /workspace/meeting-types); blocked times (CalendarBlockedTime, /admin/blocked-times); booking message dispatch (_dispatch_booking_messages, services/messaging_service); the OAuth callback handler is IN this recipe (it is the second half of "connect"), but the HTML popup page it returns and the settings UI that drives all of this are the frontend module below, not this file.

Stack assumptions

At 62ba4fc: fastapi==0.118.2, synchronous handlers on sqlalchemy.orm.Session (SQLAlchemy==2.0.43), cryptography==46.0.0 (Fernet), google-auth==2.41.1 / google-auth-oauthlib==1.2.1 / google-api-python-client==2.154.0 for the OAuth flow and the Calendar/oauth2 API clients, requests==2.32.5 for the iCal feed fetch and connectivity test, icalendar==6.1.0 for VEVENT parsing (recurring-ical-events==3.5.2 is an optional RRULE-expansion import used by the availability path, out of this recipe's scope), caldav==1.4.0 (RFC 4791 client, lazy-imported so the rest of the app doesn't pay its import cost), pydantic==2.12.0 for every request body, alembic==1.16.5, Postgres 15 in prod.

Foundations assumed: S4 (fastapi-project-layout) for the app and mount_router, which the router is mounted through at /api/calendar with surfaces=LABS_SURFACES (app_fastapi.py 845); S5 (auth-dependency-pattern) for CurrentUser/AdminUser (api/dependencies.py 565, 568, both built on get_current_user/get_current_admin_user); S8 (alembic-migrations) for the one schema delta this recipe owns; S7 (error-envelope) - every HTTPException this router raises is plain (raise HTTPException(status_code=..., detail=...)), formatted into the standard envelope only by the app-level @app.exception_handler(HTTPException) (app_fastapi.py 724 to 725), the router does no formatting of its own. Deliberately NOT depended on: _shared/workspace-scoping. Tenancy here is a private, inlined raw-SQL helper (_has_workspace_access, calendar_router.py 959 to 979) rather than the shared middleware pattern, and its own docstring gives the reason: api.dependencies re-exports via import *, which drops underscore-prefixed helpers, so a shared helper of that shape could not be imported the normal way (962 to 963).

Environment, read two different ways: GOOGLE_CLIENT_ID / GOOGLE_CLIENT_SECRET (SecretStr) / GOOGLE_CALENDAR_REDIRECT_URI are settings fields (core/config.py 619 to 624); unset makes _validate_google_config only log a warning at service construction (calendar_integration_service.py 114 to 135), the actual failure is deferred to get_oauth_authorization_url raising ValueError when either is falsy (149 to 159), which the router turns into a 400. CALENDAR_ENCRYPTION_KEY is NOT a settings field, it is read directly with os.getenv inside _get_or_create_key (78 to 112); see Gotchas for its fallback order. ENVIRONMENT is a settings field defaulting to "development" (core/config.py 30) and gates whether a missing key auto-creates a local file or raises.

Data model

calendar_integrations, one row per connected calendar, OAuth and iCal and CalDAV credential columns living side by side on the same table (models/calendar_integration_model.py 82 to 155; the only migration in this recipe's scope, 20260427b_add_ical_caldav.py, is the delta that added the iCal/CalDAV columns on top of the frozen baseline_prod_schema_001.py snapshot that already carried the OAuth columns):

calendar_integrations (
  id                          uuid PRIMARY KEY,
  workspace_id                uuid REFERENCES workspaces(id) ON DELETE CASCADE,   -- NULL = platform-level (admin's calendar)
  user_id                     uuid REFERENCES users(id) ON DELETE SET NULL,       -- who last completed the Google OAuth flow

  provider                    varchar(50) NOT NULL DEFAULT 'google',              -- google | outlook | apple | ical_feed | caldav
  provider_account_id         varchar(255),                                       -- Google: account email; iCal/CalDAV: the account_email param
  provider_account_name       varchar(255),

  access_token_encrypted      text,               -- Fernet ciphertext; nullable (only OAuth uses it); "" (not NULL) after disconnect
  refresh_token_encrypted     text,               -- Fernet ciphertext; NULL after disconnect
  token_expires_at            timestamptz,
  scopes                      jsonb DEFAULT '[]',

  default_messages            jsonb,               -- meeting-type message inheritance; out of this recipe's scope

  ical_feed_url                text,
  feed_auth_type                 varchar(20) DEFAULT 'none',  -- none | basic | bearer | query_token
  feed_auth_value_encrypted       text,              -- Fernet ciphertext of the bearer token / basic-auth password / query token
  feed_auth_username                varchar(255),
  feed_etag                           varchar(255),   -- conditional-GET cache, not a credential
  feed_last_modified                    varchar(255),
  feed_last_fetched_at                    timestamptz,
  feed_last_event_count                     integer,

  caldav_url                   text,
  caldav_username                varchar(255),
  caldav_password_encrypted        text,              -- Fernet ciphertext; an APP password, never the account's main password

  calendar_id                  varchar(255) DEFAULT 'primary',
  timezone                     varchar(100) DEFAULT 'Europe/Zurich',
  is_active                    boolean DEFAULT true,

  connected_at timestamptz DEFAULT now(), last_sync_at timestamptz, last_error text,
  created_at timestamptz DEFAULT now(), updated_at timestamptz
);
-- No CHECK constraints on this table. No UNIQUE on (workspace_id, provider) since
-- ical_caldav_001 dropped it: a workspace can hold multiple iCal feeds or CalDAV
-- calendars (one per doctor), model comment 148-149.

What is NOT stored: the Google OAuth app's own client credential (GOOGLE_CLIENT_SECRET is a process-level env var, not per-row); a CalDAV account's real login password (the column name and the module docstring both insist on an app password, caldav_provider.py 11 to 12); the Fernet key itself (lives outside this table, see Deploy & ops). Disconnecting an integration does NOT null feed_auth_value_encrypted or caldav_password_encrypted - only the two OAuth columns are cleared (Gotchas).

API surface

All routes mounted at /api/calendar (calendar_router.py, surfaces=LABS_SURFACES, i.e. ordinary Labs session auth, not surface_exempt).

RouteAuthTenancyCodesNotes
POST /admin/connect (273 to 302)AdminUserplatform (workspace_id=None)200 {authorization_url, state, redirect_url}; 400 ValueError (Google creds not configured); 500starts Google OAuth for the platform integration
GET /callback (866 to 952)none - trusts an optional ?user_id= query param, else falls back to the first is_admin user (886 to 898)derived from the encrypted state param (202 to 203)always 200 HTML (a popup-closer page); success/failure is signalled to the opener via postMessage, never via HTTP statusshared by both admin and workspace OAuth; see Security & tenancy
DELETE /admin/disconnect (305 to 319)AdminUserplatform200 {success:true, message}; 404 no platform integration
POST /workspace/connect (1056 to 1084)CurrentUser_assert_calendar_connect_access200 same shape as admin/connect; 400 missing workspace_id / ValueError
DELETE /integrations/{integration_id} (471 to 495)CurrentUseradmin OR _has_workspace_access on the row's workspace_id200 {success:true}; 403; 404generic disconnect, any provider
POST /integrations/test-ical (362 to 371)CurrentUsernone200 {ok:true,...} or {ok:false, error} - never a 4xx for a bad feedstateless; no _assert_calendar_connect_access call (Security & tenancy)
POST /integrations/test-caldav (374 to 378)CurrentUsernonesame shapestateless; same gap
POST /integrations/ical (381 to 421)CurrentUser_assert_calendar_connect_access200 IntegrationResponse; 400 invalid auth_type (394 to 395)encrypts auth_value only if provided
POST /integrations/caldav (424 to 452)CurrentUser_assert_calendar_connect_access200 IntegrationResponsealways encrypts password, no optional branch
GET /integrations (455 to 468)AdminUsernone beyond admin200 list, filterable by workspace_id/provider query paramscross-tenant admin directory
GET /admin/integration (249 to 270)AdminUserplatform200 IntegrationResponse or null
GET /workspace/integration (1017 to 1027)CurrentUseradmin OR _has_workspace_access200 or null; 403Google-only lookup, kept for legacy callers
GET /workspace/integrations (1030 to 1054, not fully cited above)CurrentUsersame200 list of active integrations, any providerplural; superset of the Google-only singular route

IntegrationResponse (45 to 59) never serializes a credential or token field: only id, workspace_id, provider, provider_account_id/name, calendar_id, timezone, is_active, connected_at, last_sync_at, is_platform_level.

Application logic

  1. Google OAuth start (get_oauth_authorization_url, 149 to 188): builds a Flow from a literal client_config dict assembled from google_client_id/google_client_secret/redirect_uri; encrypts {"workspace_id": ...} as the OAuth state param with the SAME Fernet cipher used for tokens (177 to 179); requests access_type=offline, prompt=consent so a refresh token is reissued even on a re-consent (182 to 186).
  2. Google OAuth callback (handle_oauth_callback, 190 to 280): decrypts state to recover workspace_id (202 to 203); exchanges the code via flow.fetch_token; calls oauth2.userinfo().get() for the account email/name (226 to 229); looks up an existing row by (workspace_id, provider='google') only, not by which user is completing the flow (232 to 235) - a second person re-running OAuth for the same scope overwrites the first row's tokens rather than creating a second (see Gotchas); encrypts the access token unconditionally, the refresh token only if Google returned one, otherwise keeps the existing encrypted refresh token (239 to 240, 267); on a brand-new PLATFORM-level integration only (workspace_id is None), seeds the three fixed meeting types (254 to 255, 277 to 278, _create_default_meeting_types, 286 to 367) - a workspace-level connect never seeds anything.
  3. iCal connect (connect_ical_feed, 381 to 421): _assert_calendar_connect_access first (393); validates auth_type against the FeedAuthType enum, 400 if not a member (394 to 395); encrypts auth_value only when one is given (397 to 400 - a none-auth feed stores no ciphertext column at all); inserts the row directly. Nothing server-side forces /integrations/test-ical to have been called first; the connectivity check is a frontend-driven convention, not a backend precondition.
  4. CalDAV connect (connect_caldav, 424 to 452): same access check (431); ALWAYS encrypts password (432, no optional branch - even an empty-string password is stored as a Fernet ciphertext of the empty string); same "no forced test before save" note as step 3.
  5. Connectivity tests (test_ical_feed/test_caldav_connection, 362 to 378, delegating to ical_provider.test_feed/caldav_provider.test_connection): stateless, write nothing to the database, meant to run from the UI before step 3/4, not enforced after the fact.
    • test_feed (ical_provider.py 265 to 307): builds the same auth header/query the real fetch path would use, GETs the URL with a 12s timeout, requires the literal substring "BEGIN:VCALENDAR" in the response body, returns {ok, status, bytes, event_count, etag, last_modified}; a network error, non-2xx, or missing VCALENDAR marker all become {ok:false, error/status} - the function never raises.
    • test_connection (caldav_provider.py 305 to 329): opens a caldav.DAVClient, tries the given URL as a calendar directly, falls back to principal discovery on failure, returns {ok:true, calendar_url, name} or, for a bare discovery URL, {ok:true, ..., available:[...]} listing every calendar found on the account; any exception becomes {ok:false, error}.
  6. Disconnect (disconnect_integration, 459 to 472, called by both /admin/disconnect and the generic /integrations/{id}): sets is_active=False, access_token_encrypted="", refresh_token_encrypted=None, commits. Provider-agnostic - run on an iCal or CalDAV row exactly the same way, even though neither of those two columns holds that row's actual credential (Gotchas).

Invariants

  • Every credential column that can reach a live calendar is written through self.cipher, never plaintext: access_token_encrypted/refresh_token_encrypted (239 to 240, 266 to 267), feed_auth_value_encrypted (397 to 399), caldav_password_encrypted (432) - confirmed by test_encrypt_decrypt_round_trip, test_connect_ical_encrypts_auth_value, test_connect_caldav_encrypts_password.
  • Fernet encryption is non-deterministic: two encryptions of the same credential never produce identical ciphertext, so ciphertext can never be used as an equality lookup key (test_encrypt_token_nondeterministic).
  • A platform-level integration (workspace_id IS NULL) is reachable, for every mutating route this recipe covers, only by an admin (_assert_calendar_connect_access 989 to 993, AdminUser on every /admin/* route); a workspace-scoped integration is reachable by any ACTIVE member of that workspace, not only its owner (_has_workspace_access 959 to 979; test_has_workspace_access_true_for_member, test_has_workspace_access_false_for_inactive_membership).
  • get_platform_integration and get_workspace_integration only ever return an active GOOGLE row (413 to 419, 421 to 436); a platform-level or workspace-level iCal/CalDAV row exists in the table but is invisible to both (test_service_get_platform_integration_ignores_non_google).
  • Disconnecting never deletes a row; is_active=False is the only durable "gone" signal, and every lookup helper this recipe covers filters on it (test_primary_integration_skips_inactive, test_service_get_platform_integration_ignores_inactive).

Frontend

The module that would render this, by id:

calendar-booking/ui/calendar-settings-panel   the connect/disconnect/test UI for all three
                                                providers, at admin (platform) and workspace
                                                scope; not written yet in this kit
                                                (frontend_modules is required non-empty per
                                                AUTHORING.md, this is the placeholder id)

Alchemy Labs' own (pre-Prima) frontend already implements exactly this surface, cited here only as what a rebuild would replace, not as this recipe's subject: alchemy_labs/frontend/src/pages/AdminCalendarSettings.tsx (969 lines) calls POST /api/calendar/admin/connect and DELETE /api/calendar/admin/disconnect; alchemy_labs/frontend/src/pages/CalendarSettings.tsx (1369 lines) calls POST /api/calendar/workspace/connect; alchemy_labs/frontend/src/components/calendar/AlternativeCalendarConnect.tsx (414 lines) calls all four of test-ical, integrations/ical, test-caldav, integrations/caldav in that connect-then-test-then-save order.

Minimum admin/debug surface with no dedicated route: GET /integrations?workspace_id=&provider= (admin-only, 455 to 468) is the only cross-tenant directory; there is no route that surfaces a decrypted credential or even confirms one exists beyond is_active/provider_account_id/provider_account_name - by design, IntegrationResponse never carries a credential field.

Security & tenancy

Auth dependency: CurrentUser/AdminUser (api/dependencies.py 565, 568), JWT bearer or httpOnly cookie or X-API-Key, resolved by get_current_user (187 to ~230) then, for admin routes, get_current_admin_user requiring is_admin (364 to 375). Workspace derivation is NOT the shared _shared/workspace-scoping middleware pattern; this router inlines its own raw-SQL primitive, _has_workspace_access (959 to 979, owner-or-active-member), with the reason given in its own docstring: api.dependencies re-exports via import *, which drops underscore-prefixed helpers (962 to 963). _assert_calendar_connect_access (982 to 998) is the connect-time gate actually used by /integrations/ical, /integrations/caldav, and /workspace/connect: an admin bypasses everything; a non-admin caller with no workspace_id is refused outright (990 to 993, the reason a "platform-only" 403 and a "no workspace supplied" 403 read identically in the tests); a non-admin caller naming a workspace_id they are not an active member of is refused (994 to 998).

Fail direction: closed for every route that calls _assert_calendar_connect_access - nothing is written before that check passes. NOT closed for /integrations/test-ical and /integrations/test-caldav (362 to 378): both require only user: CurrentUser, no _assert_calendar_connect_access call and no workspace_id parameter at all, one line above the connect endpoints that DO gate on it. Any authenticated user on the platform, regardless of workspace membership, can use either as a one-shot outbound-request primitive: /integrations/test-ical makes the Labs backend issue an HTTPS GET to an attacker-chosen URL carrying attacker-chosen bearer/basic/query credentials, and /integrations/test-caldav opens an attacker-chosen CalDAV connection; neither validates the target host or scheme. auth_type is validated against the FeedAuthType enum only on the SAVE endpoint (connect_ical_feed, 394 to 395) - test_ical_feed passes body.auth_type straight into test_feed with no such check (362 to 371).

Credentials at rest: the four columns under Data model, Fernet-encrypted with one process-wide cipher; losing the key makes every stored credential permanently undecryptable, there is no re-encryption or key-rotation path anywhere in this recipe. Rate limits: none dedicated to any route here, including the two unauthenticated-outbound-request test endpoints above. Attacker-controlled inputs, all behind ordinary session auth: feed_url/caldav_url (free-form string, no scheme or host allow-list on either the test or the save path - the fetch always happens from the Labs backend, an SSRF-shaped surface); auth_type/auth_value/username/password (used to build outbound request auth, encrypted only on the save path); account_email/account_name (free text, stored and displayed, never itself used as a credential).

Tests

Read in full, not executed live for this recipe (the shared scratch Postgres on :5434 was not migrated or written to; every count below is grep -c "^def test_", not a pytest report):

  • test_booking_g3_workspace_tenant_integration_endpoints_ical_caldav_connect_access_control.py (542 lines, 31 def test_, 0 @pytest.mark.parametrize): FastAPI TestClient against app_fastapi:app with dependency_overrides for auth and DB, following tests/v2/test_f1_scribe_leak.py's pattern per its own docstring (11 to 13). Covers exactly this recipe's surface: _has_workspace_access (owner/member/outsider/inactive-member, 4 tests); encrypt_token/decrypt_token round-trip and non-determinism (2 tests); get_primary_workspace_integration provider-preference and active-filter (4 tests); GET /workspace/integration(s) with cross_tenant_client asserting 403 (tenant B against tenant A's workspace); test_connect_ical_requires_admin, test_connect_ical_400_invalid_auth_type, test_connect_ical_encrypts_auth_value, test_connect_caldav_requires_admin, test_connect_caldav_encrypts_password.
  • test_booking_g2_admin_platform_integration_meeting_type_blocked_time_booking_management.py (650 lines, 44 def test_): covers the admin half of this recipe - /admin/integration, /integrations (list), /admin/disconnect - plus meeting-type/blocked-time/booking management that is out of this recipe's scope. This recipe's own tests: test_disconnect_platform_404_when_none, test_disconnect_platform_deactivates, test_disconnect_platform_requires_admin, test_service_disconnect_wipes_tokens_and_returns_true (asserts exactly access_token_encrypted=="" and refresh_token_encrypted is None; asserts nothing about an iCal/CalDAV row's credential columns), test_service_disconnect_unknown_returns_false, and the four test_service_get_platform_integration_* tests. Both this file and G3 patch app_fastapi.init_db = lambda: None before constructing any TestClient (40 to 57 in G2), because the real app lifespan's init_db() drops the transactional test schema mid-session.
  • test_f4_oauth_encryption.py (135 lines, 9 def test_, named as a source file for this task): read in full, it does not test this recipe. Its own module docstring (1 to 27) states its target is services/security/oauth_token_crypto.py, the separate encryption path for Meta / Google Ads / LinkedIn ad-platform tokens; its imports (line 37) confirm this. It shares no code, no Fernet key, and no table with CalendarIntegrationService. This is a genuine gap in the task's own source list, stated plainly rather than papered over: no test file in this repo hermetically exercises CalendarIntegrationService.encrypt_token/decrypt_token in isolation from a live TestClient request; that coverage exists only inside G3's test_encrypt_decrypt_round_trip/test_encrypt_token_nondeterministic and G2's disconnect assertions above.

What a new app adds and this recipe's own source lacks: no test asserts that disconnecting an iCal or CalDAV integration clears (or deliberately preserves) its own credential column, only that it clears the two OAuth columns (see Gotchas); no test hits /integrations/test-ical or /integrations/test-caldav with a cross-tenant or non-member caller to confirm the missing access check (Security & tenancy) is intentional rather than an oversight.

Deploy & ops

Config keys: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, GOOGLE_CALENDAR_REDIRECT_URI (settings fields, core/config.py 619 to 624); CALENDAR_ENCRYPTION_KEY (not a settings field, raw os.getenv, calendar_integration_service.py 90 to 93); ENVIRONMENT (core/config.py 30, default "development", gates whether a missing key auto-creates a local file or raises).

Migration: one revision in this recipe's scope, ical_caldav_001 (migrations/versions/20260427b_add_ical_caldav.py, down-revision content_review_001) - adds every iCal/CalDAV column, drops access_token_encrypted's NOT NULL, drops the (workspace_id, provider) unique constraint (all three checked idempotently via inspect() before altering, 30 to 82). The base calendar_integrations table (OAuth columns, the id/workspace_id/provider shape) predates this recipe's source list - it ships in the frozen baseline_prod_schema_001.py snapshot, not a migration cited here. Downgrade (84 to 101) re-adds the dropped unique constraint and unconditionally re-tightens access_token_encrypted to NOT NULL; both connect_ical_feed and connect_caldav insert rows with access_token_encrypted=None, so running this downgrade after either endpoint has ever been used fails the NOT NULL alter unless those rows are cleaned up by hand first - the migration's own comment only warns about deduping for the unique constraint, not about this.

No scheduler job belongs to this recipe. ical_provider.py's module-level _FEED_CACHE dict (line 110) is an in-process, unbounded-lifetime cache of raw .ics text keyed by integration id; nothing evicts it and a process restart is what clears it (fresh fetch on next read). No Argus-style check is defined specifically for this recipe; the general assertion available is that GET /admin/integration and GET /workspace/integration answer without a 5xx. Rollback of the code alone needs no data migration; a disconnected row (is_active=False) is never deleted by any path in this recipe, so a code rollback does not need to unwind data.

Gotchas

  • 2026-09-12 (measured at 62ba4fc, disconnect_integration, calendar_integration_service.py 459 to 472): disconnect clears only access_token_encrypted/refresh_token_encrypted. An iCal integration's feed_auth_value_encrypted (plus its plaintext feed_auth_username/ical_feed_url) and a CalDAV integration's caldav_password_encrypted are left in the row, still Fernet-encrypted but still present, after disconnect. is_active=False hides the row from every lookup helper this recipe covers, but the credential itself is not removed. No test in any of the three files this recipe was measured against checks an iCal or CalDAV row's credential columns post-disconnect.
  • 2026-09-12 (measured, calendar_router.py 362 to 378): /integrations/test-ical and /integrations/test-caldav require CurrentUser only, no _assert_calendar_connect_access call and no workspace_id - one line above the real connect endpoints that DO gate on it. Any authenticated user can use either as an outbound-request oracle against an arbitrary URL/CalDAV server with attacker-supplied credentials, with no host allow-list.
  • 2026-09-12 (measured, _get_or_create_key, calendar_integration_service.py 78 to 112): CALENDAR_ENCRYPTION_KEY is read via raw os.getenv, not through settings/pydantic like every neighbouring credential (GOOGLE_CLIENT_SECRET is a SecretStr field). If the env var is unset AND a .calendar_encryption_key file already exists on disk, that file is used regardless of ENVIRONMENT - the production-only raise ValueError (109 to 112) is reached only when BOTH the env var and the file are absent. A key file copied from a dev checkout into a prod image would be used silently, with no warning distinguishing it from a properly-provisioned production key.
  • 2026-09-12 (measured, handle_oauth_callback, 232 to 257): the existing-integration lookup is (workspace_id, provider='google') only, not scoped by which user completes the flow. A second person re-running OAuth for the same scope (platform, or the same workspace) overwrites the first person's tokens on the same row; user_id is silently reassigned to whoever finished OAuth most recently.
  • 2026-09-12 (measured, oauth_callback, 866 to 898): the callback trusts an optional ?user_id= query param verbatim, with no signature, and falls back to "the first is_admin user" if it's absent; nothing cross-checks it against the encrypted state, which carries only workspace_id. Inherent to a browser-redirect OAuth callback (the backend cannot control what query params Google's redirect carries) and predates this recipe, not a regression to fix here, but worth knowing before trusting user_id as an audit trail.
  • 2026-09-12 (measured, 20260427b_add_ical_caldav.py downgrade, 84 to 101): downgrading this migration after any iCal or CalDAV row has ever been created fails on the unconditional access_token_encrypted NOT NULL re-tightening, since both connect endpoints insert that column as None. The migration's own comment warns about deduplicating for the unique constraint it also re-adds, but says nothing about this.

Reference implementation

Paths relative to the repo root; all at main 62ba4fc41009b454e1c033e5036c7e8577db2061 unless noted.

  • alchemy_labs/backend/services/calendar_integration_service.py (1716 lines): module docstring (1 to 11); _get_or_create_key (78 to 112); _validate_google_config (114 to 135); encrypt_token/decrypt_token (137 to 143); get_oauth_authorization_url (149 to 188); handle_oauth_callback (190 to 280); _create_default_meeting_types (286 to 367, cited for context - only its call sites at 254 to 255/277 to 278 are in scope); get_credentials/get_calendar_service (369 to 407, out of scope, cited for context); get_platform_integration/get_workspace_integration/get_primary_workspace_integration (413 to 457); disconnect_integration (459 to 472).
  • alchemy_labs/backend/api/routers/calendar_router.py (2485 lines): schemas IntegrationResponse/ConnectRequest/ICalFeedConnect/CalDAVConnect/FeedTestRequest/CalDAVTestRequest (45 to 66, 326 to 360); /admin/integration (249 to 270); /admin/connect (273 to 302); /admin/disconnect (305 to 319); /integrations/test-ical (362 to 371); /integrations/test-caldav (374 to 378); /integrations/ical (381 to 421); /integrations/caldav (424 to 452); /integrations list (455 to 468); /integrations/{id} delete (471 to 495); /callback (866 to 952); _has_workspace_access (959 to 979); _assert_calendar_connect_access (982 to 998); _integration_to_response (1001 to 1014); /workspace/integration (1017 to 1027); /workspace/integrations (1030 to 1054); /workspace/connect (1056 to 1084).
  • alchemy_labs/backend/models/calendar_integration_model.py (439 lines): CalendarProvider/FeedAuthType enums (17 to 31); CalendarIntegration (82 to 155, all columns in Data model, is_platform_level property 151 to 154).
  • alchemy_labs/backend/services/calendar/ical_provider.py (307 lines, whole file in scope): _build_request (61 to 96); fetch_feed_text and _FEED_CACHE (107 to 157); test_feed (265 to 307).
  • alchemy_labs/backend/services/calendar/caldav_provider.py (335 lines): _decrypt_password (31 to 35); _open_calendar (38 to 73, used by the write path, out of this recipe's scope, cited for the connectivity-test's own calendar-open logic); test_connection (305 to 329).
  • alchemy_labs/backend/migrations/versions/20260427b_add_ical_caldav.py (whole file, ~101 lines): upgrade (39 to 81); downgrade (84 to 101).
  • alchemy_labs/backend/core/config.py: GOOGLE_CLIENT_ID/GOOGLE_CLIENT_SECRET/GOOGLE_CALENDAR_REDIRECT_URI (619 to 624); ENVIRONMENT (30).
  • alchemy_labs/backend/app_fastapi.py: the router mount (845); the global HTTPException handler (724 to 725).
  • Tests as listed under Tests: alchemy_labs/backend/tests/v2/coverage/test_booking_g3_workspace_tenant_integration_endpoints_ical_caldav_connect_access_control.py, test_booking_g2_admin_platform_integration_meeting_type_blocked_time_booking_management.py, alchemy_labs/backend/tests/v2/test_f4_oauth_encryption.py (the last does not test this recipe, see Tests).

Deviations of the reference from an ideal implementation, all dated 2026-09-12: disconnect leaves the iCal/CalDAV credential columns populated (Gotchas); the two connectivity-test endpoints carry no tenancy check at all; the OAuth callback trusts an unsigned user_id query param; the encryption key can silently fall back to a stale on-disk file in production; no dedicated hermetic test targets CalendarIntegrationService's own encrypt/decrypt functions outside a live TestClient request.

Acceptance checks

Run from alchemy_labs/backend at 62ba4fc. These are static/structural checks (grep against the pinned commit), not a pytest run - this recipe's Tests section states plainly that the suites were read, not executed, for this measurement.

  1. sed -n '459,472p' services/calendar_integration_service.py shows exactly two assignments to a credential-shaped column (access_token_encrypted, refresh_token_encrypted) inside disconnect_integration, confirming Gotcha 1 (no feed_auth_value_encrypted or caldav_password_encrypted write).
  2. sed -n '362,379p' api/routers/calendar_router.py shows test_ical_feed/test_caldav_connection declared with only user: CurrentUser as a dependency and no call to _assert_calendar_connect_access, confirming Gotcha 2.
  3. grep -n "CALENDAR_ENCRYPTION_KEY" core/config.py returns exactly one line (1123), inside STONE_TOKEN_ENCRYPTION_KEY's own description warning callers not to reuse it, not a settings field of its own; grep -n "env_key = os.getenv" services/calendar_integration_service.py returns line 90, confirming the calendar key is read directly, bypassing settings entirely.
  4. grep -c "^def test_" tests/v2/coverage/test_booking_g3_workspace_tenant_integration_endpoints_ical_caldav_connect_access_control.py prints 31; the same on the G2 file prints 44.
  5. sed -n '1,27p' tests/v2/test_f4_oauth_encryption.py shows the module docstring naming services/security/oauth_token_crypto.py as its target, not calendar_integration_service.py, confirming the Tests-section gap.
  6. grep -n "down_revision" migrations/versions/20260427b_add_ical_caldav.py prints down_revision = 'content_review_001'.
  7. grep -n "op.alter_column('calendar_integrations', 'access_token_encrypted', nullable=False)" migrations/versions/20260427b_add_ical_caldav.py matches inside downgrade(), confirming the rollback hazard in Gotchas.
  8. grep -n "surfaces=LABS_SURFACES" app_fastapi.py | grep calendar_router matches line 845, confirming this router carries no surface_exempt and no feature flag.

Content synced from the prima-modules repository at 0df2ec8 on 2026-09-14. The recipe file is the source; this page is a rendering of it.