Skip to content

Commit 0d7a805

Browse files
committed
Add TB_ACCOUNTS_CALDAV_URL and /oidc/auth for token based CalDAV autodiscover
1 parent 8ef26bf commit 0d7a805

File tree

8 files changed

+116
-5
lines changed

8 files changed

+116
-5
lines changed

backend/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ TB_ACCOUNTS_HOST=http://localhost:8087
8989
TB_ACCOUNTS_CALLBACK=http://localhost:5000/accounts/callback
9090
TB_ACCOUNTS_CLIENT_ID
9191
TB_ACCOUNTS_SECRET
92+
TB_ACCOUNTS_CALDAV_URL=https://stage-thundermail.com
9293

9394
# -- GOOGLE AUTH --
9495
GOOGLE_AUTH_CLIENT_ID=

backend/.env.test

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ CALDAV_TEST_USER=hello-world
9090
CALDAV_TEST_PASS=fake-pass
9191
GOOGLE_TEST_USER=
9292
GOOGLE_TEST_PASS=
93+
TB_ACCOUNTS_CALDAV_URL=https://stage-thundermail.com
9394

9495
TEST_USER_EMAIL=[email protected]
9596

backend/src/appointment/controller/calendar.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from dns.exception import DNSException
1818
from redis import Redis, RedisCluster
1919
from caldav import DAVClient
20+
from caldav.requests import HTTPBearerAuth
2021
from fastapi import BackgroundTasks
2122
from google.oauth2.credentials import Credentials
2223
from icalendar import Calendar, Event, vCalAddress, vText
@@ -320,7 +321,15 @@ def delete_events(self, start):
320321

321322
class CalDavConnector(BaseConnector):
322323
def __init__(
323-
self, db: Session, subscriber_id: int, calendar_id: int, redis_instance, url: str, user: str, password: str
324+
self,
325+
db: Session,
326+
subscriber_id: int,
327+
calendar_id: int,
328+
redis_instance,
329+
url: str,
330+
user: str | None = None,
331+
password: str | None = None,
332+
token: str | None = None,
324333
):
325334
super().__init__(subscriber_id, calendar_id, redis_instance)
326335

@@ -336,7 +345,10 @@ def __init__(
336345
sentry_sdk.set_tag('caldav_host', parsed_url.hostname)
337346

338347
# connect to the CalDAV server
339-
self.client = DAVClient(url=self.url, username=self.user, password=self.password)
348+
if token:
349+
self.client = DAVClient(url=self.url, auth=HTTPBearerAuth(token))
350+
else:
351+
self.client = DAVClient(url=self.url, username=self.user, password=self.password)
340352

341353
def get_busy_time(self, calendar_ids: list, start: str, end: str):
342354
"""Retrieve a list of { start, end } dicts that will indicate busy time for a user

backend/src/appointment/database/schemas.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -285,8 +285,8 @@ class CalendarConnection(CalendarConnectionOut):
285285

286286

287287
class CalendarConnectionIn(CalendarConnection):
288-
url: str = Field(min_length=1)
289-
user: str = Field(min_length=1)
288+
url: str = Optional[str]
289+
user: str = Optional[str]
290290
password: Optional[str]
291291

292292

backend/src/appointment/routes/caldav.py

Lines changed: 92 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import os
12
import json
23
import urllib
34
from typing import Optional
@@ -11,7 +12,7 @@
1112
from appointment import utils
1213
from appointment.controller.calendar import CalDavConnector, Tools
1314
from appointment.database import models, schemas, repo
14-
from appointment.dependencies.auth import get_subscriber
15+
from appointment.dependencies.auth import get_subscriber, oauth2_scheme
1516
from appointment.dependencies.database import get_db, get_redis
1617
from appointment.exceptions.calendar import TestConnectionFailed
1718
from appointment.exceptions.misc import UnexpectedBehaviourWarning
@@ -128,6 +129,96 @@ def caldav_autodiscover_auth(
128129
return True
129130

130131

132+
@router.post('/oidc/auth')
133+
def oidc_autodiscover_auth(
134+
db: Session = Depends(get_db),
135+
subscriber: models.Subscriber = Depends(get_subscriber),
136+
redis_client: Redis = Depends(get_redis),
137+
token: str = Depends(oauth2_scheme),
138+
):
139+
"""Connects a principal caldav server through oidc token auth"""
140+
141+
connection_url = os.getenv('TB_ACCOUNTS_CALDAV_URL')
142+
dns_lookup_cache_key = f'dns:{utils.encrypt(connection_url)}'
143+
lookup_url = None
144+
145+
if redis_client:
146+
lookup_url = redis_client.get(dns_lookup_cache_key)
147+
148+
if lookup_url and 'http' not in lookup_url:
149+
debug_obj = {'url': lookup_url, 'branch': 'CACHE'}
150+
# Raise and catch the unexpected behaviour warning so we can get proper stacktrace in sentry...
151+
try:
152+
sentry_sdk.set_extra('debug_object', debug_obj)
153+
raise UnexpectedBehaviourWarning(message='Cache incorrect', info=debug_obj)
154+
except UnexpectedBehaviourWarning as ex:
155+
sentry_sdk.capture_exception(ex)
156+
157+
# Clear cache for that key
158+
redis_client.delete(dns_lookup_cache_key)
159+
160+
# Ignore cached result and look it up again
161+
lookup_url = None
162+
163+
# Do a dns lookup first
164+
if lookup_url is None:
165+
parsed_url = urlparse(connection_url)
166+
lookup_url, ttl = Tools.dns_caldav_lookup(parsed_url.hostname, secure=True)
167+
# set the cached lookup for the remainder of the dns ttl
168+
if redis_client and lookup_url:
169+
redis_client.set(dns_lookup_cache_key, utils.encrypt(lookup_url), ex=ttl)
170+
else:
171+
# Extract the cached value
172+
lookup_url = utils.decrypt(lookup_url)
173+
174+
# If we have a lookup_url then apply it
175+
if lookup_url and 'http' not in lookup_url:
176+
connection_url = urllib.parse.urljoin(connection_url, lookup_url)
177+
elif lookup_url:
178+
connection_url = lookup_url
179+
180+
con = CalDavConnector(
181+
db=db,
182+
redis_instance=None,
183+
url=connection_url,
184+
subscriber_id=subscriber.id,
185+
calendar_id=None,
186+
token=token,
187+
)
188+
189+
try:
190+
if not con.test_connection():
191+
raise RemoteCalendarConnectionError()
192+
except TestConnectionFailed as ex:
193+
raise RemoteCalendarConnectionError(reason=ex.reason)
194+
195+
caldav_name = subscriber.email
196+
caldav_id = json.dumps([connection_url, caldav_name])
197+
198+
external_connection = repo.external_connection.get_by_type(
199+
db, subscriber.id, models.ExternalConnectionType.caldav, caldav_id
200+
)
201+
202+
# Create or update the external connection
203+
if not external_connection:
204+
external_connection_schema = schemas.ExternalConnection(
205+
name=caldav_name,
206+
type=models.ExternalConnectionType.caldav,
207+
type_id=caldav_id,
208+
owner_id=subscriber.id,
209+
token=token,
210+
)
211+
212+
external_connection = repo.external_connection.create(db, external_connection_schema)
213+
else:
214+
external_connection = repo.external_connection.update_token(
215+
db, token, subscriber.id, models.ExternalConnectionType.caldav, caldav_id
216+
)
217+
218+
con.sync_calendars(external_connection_id=external_connection.id)
219+
return True
220+
221+
131222
@router.post('/', response_model=schemas.CalendarOut)
132223
def create_my_calendar(
133224
calendar: schemas.CalendarConnection,

pulumi/config.dev.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -226,6 +226,8 @@ resources:
226226
value: "60"
227227
- name: OIDC_FALLBACK_MATCH_BY_EMAIL
228228
value: "True"
229+
- name: TB_ACCOUNTS_CALDAV_URL
230+
value: https://stage-thundermail.com
229231

230232
tb:autoscale:EcsServiceAutoscaler:
231233
backend:

pulumi/config.prod.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ resources:
296296
value: https://appointment.tb.pro/api/v1/zoom/callback
297297
- name: ZOOM_API_NEW_APP
298298
value: "False"
299+
- name: TB_ACCOUNTS_CALDAV_URL
300+
value: https://thundermail.com
299301

300302
tb:autoscale:EcsServiceAutoscaler:
301303
backend:

pulumi/config.stage.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,8 @@ resources:
297297
value: "False"
298298
- name: ZOOM_AUTH_CALLBACK
299299
value: https://appointment-stage.tb.pro/api/v1/zoom/callback
300+
- name: TB_ACCOUNTS_CALDAV_URL
301+
value: https://stage-thundermail.com
300302

301303
tb:autoscale:EcsServiceAutoscaler:
302304
backend:

0 commit comments

Comments
 (0)