Add an endpoint letting a community auth site subscribe to updates
authorMagnus Hagander <magnus@hagander.net>
Mon, 7 Mar 2022 20:25:52 +0000 (21:25 +0100)
committerMagnus Hagander <magnus@hagander.net>
Mon, 7 Mar 2022 20:29:08 +0000 (21:29 +0100)
When a user logs into a community auth site, that account is
automatically subscribed to receive updates from the system whenever any
changes are made to the user, such as name/email/ssh keys. However, when
a site imports a user without them being directly logging in, that
subscription is not set up, so any changes made are lost until the user
first logs in.

This commnit adds an endpoint to the auth system so that a site can
expliciltly request updates are sent about a user. This will create a
"fake login" on that site, which will enable the normal system to start
sending data. The access to the endpoint is protected with a hmac
authentication using the existing community auth key.

pgweb/account/urls.py
pgweb/account/views.py

index a5c647d51d832ccb740be369b0172ad9520067cf..5670d5b7a6dfc732a59ace9b61458ac582a69388 100644 (file)
@@ -15,6 +15,7 @@ urlpatterns = [
     url(r'^auth/(\d+)/consent/$', pgweb.account.views.communityauth_consent),
     url(r'^auth/(\d+)/search/$', pgweb.account.views.communityauth_search),
     url(r'^auth/(\d+)/getkeys/(\d+/)?$', pgweb.account.views.communityauth_getkeys),
+    url(r'^auth/(\d+)/subscribe/$', pgweb.account.views.communityauth_subscribe),
 
     # Profile
     url(r'^profile/$', pgweb.account.views.profile),
index 0b381ff64eaf628ebca330b82a19235d1db4b762..2381180c9f785144035df11ddc2ef9c648564425 100644 (file)
@@ -22,6 +22,7 @@ import time
 import json
 from datetime import datetime, timedelta
 import itertools
+import hmac
 
 from pgweb.util.contexts import render_pgweb
 from pgweb.util.misc import send_template_mail, generate_random_token, get_client_ip
@@ -828,3 +829,47 @@ def communityauth_getkeys(request, siteid, since=None):
     j = json.dumps([{'u': k.user.username, 's': k.sshkey.replace("\r", "\n")} for k in keys])
 
     return HttpResponse(_encrypt_site_response(site, j))
+
+
+@csrf_exempt
+def communityauth_subscribe(request, siteid):
+    if 'X-pgauth-sig' not in request.headers:
+        return HttpResponse("Missing signature header!", status=400)
+
+    try:
+        sig = base64.b64decode(request.headers['X-pgauth-sig'])
+    except Exception:
+        return HttpResponse("Invalid signature header!", status=400)
+
+    site = get_object_or_404(CommunityAuthSite, pk=siteid)
+
+    h = hmac.digest(
+        base64.b64decode(site.cryptkey),
+        msg=request.body,
+        digest='sha512',
+    )
+    if not hmac.compare_digest(h, sig):
+        return HttpResponse("Invalid signature!", status=401)
+
+    try:
+        j = json.loads(request.body)
+    except Exception:
+        return HttpResponse("Invalid JSON!", status=400)
+
+    if 'u' not in j:
+        return HttpResponse("Missing parameter", status=400)
+
+    u = get_object_or_404(User, username=j['u'])
+
+    with connection.cursor() as curs:
+        # We handle the subscription by recording a fake login on this site
+        curs.execute("INSERT INTO account_communityauthlastlogin (user_id, site_id, lastlogin, logincount) VALUES (%(userid)s, %(siteid)s, CURRENT_TIMESTAMP, 1) ON CONFLICT (user_id, site_id) DO UPDATE SET lastlogin=CURRENT_TIMESTAMP, logincount=account_communityauthlastlogin.logincount+1", {
+            'userid': u.id,
+            'siteid': site.id,
+        })
+
+        # And when we've done that, we also trigger a sync on this particular site
+        curs.execute("INSERT INTO account_communityauthchangelog (user_id, site_id, changedat) VALUES (%(userid)s, %(siteid)s, CURRENT_TIMESTAMP) ON CONFLICT (user_id, site_id) DO UPDATE SET changedat=greatest(account_communityauthchangelog.changedat, CURRENT_TIMESTAMP)", {
+            'userid': u.id,
+            'siteid': site.id,
+        })