import hashlib import base64 import time from urllib.parse import urlencode, quote_plus, urlparse, parse_qsl from datetime import timedelta from django.db import transaction from django.conf import settings from django.urls import reverse from django.utils import timezone from django.utils.crypto import get_random_string from pingcap_sso.models import ( SSOCookieSyncToken, SSOCookieDiscourseAuthToken ) from pingcap_sso.discourse.models import ( DiscourseUser, DiscourseSingleSignOnRecord, DiscourseUserAuthToken, DiscourseUserAuthTokenLog, DiscourseUserEmail ) from pingcap_sso.discourse.api import sync_user_to_discourse from pingcap_sso.utils.requests import get_client_agent, get_client_ip SUBSYSTEM_ASKTUG_COM = 'asktug.com' SUBSYSTEM_TIDB_IO = 'tidb.io' SUBSYSTEMS = [SUBSYSTEM_ASKTUG_COM, SUBSYSTEM_TIDB_IO] def sync_cookie_url(redirect_to): payload = {'redirect_to': redirect_to} return '{}?{}'.format( reverse('sso-sync-cookie'), urlencode(payload, quote_via=quote_plus)) def generate_cookie_sync_token(session_id, user, redirect_to, ip=None, ua=None): return SSOCookieSyncToken.objects.create( session_id=session_id, user=user, user_openid=user.openid, redirect_to=redirect_to, ip=ip, ua=ua ).token def get_cookie_sync_token_instance(token, subsystem): assert subsystem in SUBSYSTEMS if subsystem == SUBSYSTEM_ASKTUG_COM: used_filed = 'asktug_com_used' elif subsystem == SUBSYSTEM_TIDB_IO: used_filed = 'tidb_io_used' with transaction.atomic(): try: token_instance = SSOCookieSyncToken.objects\ .select_for_update()\ .filter(**{used_filed: False})\ .get(token=token) # Token expires 2 minute after creation if token_instance.created_at < timezone.now() - timedelta(minutes=2): return None except SSOCookieSyncToken.DoesNotExist: return None token = token_instance.token setattr(token_instance, used_filed, True) token_instance.save() return token_instance def set_asktug_session_cookie(request, user, session_id): # auth_token must be 32 characters # https://github.com/discourse/discourse/blob/main/app/models/user_auth_token.rb#L75 asktug_auth_token = '{}{}'.format( get_random_string( length=20, allowed_chars='abcdefghijklmnopqrstuvwxyz0123456789' ), int(time.time() * 1e2) ) asktug_hashed_token = base64.b64encode( hashlib.sha1( (asktug_auth_token + settings.DISCOURSE_SECRET_TOKEN).encode('utf8') ).digest() ).decode('utf8') user_email = (user.email or user.fake_email).lower() _force_sync = False try: asktug_sso_record = DiscourseSingleSignOnRecord.objects.get(external_id=user.openid) asktug_user = DiscourseUser.objects.get(id=asktug_sso_record.user_id) if asktug_user.username.lower() != user.username.lower(): _force_sync = True else: _force_sync = not DiscourseUserEmail.objects.filter( user=asktug_user, email=user_email, primary=True ).exists() except DiscourseSingleSignOnRecord.DoesNotExist: _force_sync = True if _force_sync: sync_success = sync_user_to_discourse( user.openid, user.username, user_email, user.name, user.bio) if not sync_success: # try one more times sync_user_to_discourse( user.openid, user.username, user_email, user.name, user.bio) asktug_sso_record = DiscourseSingleSignOnRecord.objects.get(external_id=user.openid) asktug_user = DiscourseUser.objects.get(id=asktug_sso_record.user_id) asktug_auth_token_instance = DiscourseUserAuthToken.objects.create( user_id=asktug_user.id, auth_token=asktug_hashed_token, prev_auth_token=asktug_hashed_token, user_agent=get_client_agent(request), client_ip=get_client_ip(request), rotated_at=timezone.now(), created_at=timezone.now(), updated_at=timezone.now() ) SSOCookieDiscourseAuthToken.objects.create( session_id=session_id, auth_token_id=asktug_auth_token_instance.id ) return asktug_auth_token def sso_logout(request): sso_discourse_cookie_qs = SSOCookieDiscourseAuthToken.objects.filter( session_id=request.session.session_key ) asktug_auth_token_ids = sso_discourse_cookie_qs.values_list('auth_token_id', flat=True) for auth_token_id in asktug_auth_token_ids: try: with transaction.atomic(using='discourse'): auth_token_instance = DiscourseUserAuthToken.objects.get(id=auth_token_id) DiscourseUserAuthTokenLog.objects.create( action='sso destroy', user_auth_token_id=auth_token_id, user_id=auth_token_instance.user_id, client_ip=get_client_ip(request), user_agent=get_client_agent(request), auth_token=auth_token_instance.auth_token, path=request.path ) auth_token_instance.delete() except DiscourseUserAuthToken.DoesNotExist: pass sso_discourse_cookie_qs.update(deleted=True) def parse_signup_channel_from_url(source_url): if not source_url: return 'direct' source_url = urlparse(source_url) qs = dict(parse_qsl(source_url.query)) if source_url.path == reverse('oidc-auth'): return qs.get('client_id', 'unknown_oidc_client') elif qs.get("utm_campaign") == "mkt": return "mkt" else: return source_url.netloc or 'direct'