From 4ff21b25c3955ac2834e91dc9c123c675ffc6cef Mon Sep 17 00:00:00 2001 From: Adarnof Date: Tue, 25 Oct 2016 14:52:12 -0400 Subject: [PATCH] Discourse SSO (#560) * Alter Discourse support to act as SSO provider. Correct service group sync retry queueing. * Correct default database enviroment variable names. * Redirect to requested page after succesful login. * Correct default redirect handling. Correct attribute used to logout users on Discourse. Improve logging messages to use parsed path on Discourse. * Correct task retry syntax using bind=True. Inherit from base exception so can catch TeamspeakErrors. --- alliance_auth/settings.py.example | 5 +- alliance_auth/urls.py | 3 +- authentication/managers.py | 11 -- .../migrations/0009_auto_20161021_0228.py | 24 +++ authentication/models.py | 2 +- authentication/views.py | 5 +- requirements.txt | 2 +- services/managers/discourse_manager.py | 131 ++++++++------- services/managers/util/ts3.py | 2 +- services/signals.py | 2 +- services/tasks.py | 68 ++++---- services/views.py | 150 +++++++++++++----- stock/templates/public/login.html | 1 + stock/templates/registered/services.html | 12 +- 14 files changed, 252 insertions(+), 166 deletions(-) create mode 100644 authentication/migrations/0009_auto_20161021_0228.py diff --git a/alliance_auth/settings.py.example b/alliance_auth/settings.py.example index 3fc5f6d7..42b55319 100644 --- a/alliance_auth/settings.py.example +++ b/alliance_auth/settings.py.example @@ -125,7 +125,7 @@ WSGI_APPLICATION = 'alliance_auth.wsgi.application' DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', - 'NAME': 'aa_test', + 'NAME': 'alliance_auth', 'USER': os.environ.get('AA_DB_DEFAULT_USER', 'allianceserver'), 'PASSWORD': os.environ.get('AA_DB_DEFAULT_PASSWORD', 'password'), 'HOST': os.environ.get('AA_DB_DEFAULT_HOST', '127.0.0.1'), @@ -477,11 +477,12 @@ DISCORD_SYNC_NAMES = 'True' == os.environ.get('AA_DISCORD_SYNC_NAMES', 'False') # DISCOURSE_URL - Web address of the forums (no trailing slash) # DISCOURSE_API_USERNAME - API account username # DISCOURSE_API_KEY - API Key +# DISCOURSE_SSO_SECRET - SSO secret key ###################################### DISCOURSE_URL = os.environ.get('AA_DISCOURSE_URL', '') DISCOURSE_API_USERNAME = os.environ.get('AA_DISCOURSE_API_USERNAME', '') DISCOURSE_API_KEY = os.environ.get('AA_DISCOURSE_API_KEY', '') - +DISCOURSE_SSO_SECRET = os.environ.get('AA_DISCOURSE_SSO_SECRET', '') ##################################### # IPS4 Configuration diff --git a/alliance_auth/urls.py b/alliance_auth/urls.py index 57a0f3ce..26d204ea 100755 --- a/alliance_auth/urls.py +++ b/alliance_auth/urls.py @@ -94,8 +94,7 @@ urlpatterns = [ url(r'^discord_add_bot/$', services.views.discord_add_bot, name='auth_discord_add_bot'), # Discourse Service Control - url(r'^activate_discourse/$', services.views.activate_discourse, name='auth_activate_discourse'), - url(r'^deactivate_discourse/$', services.views.deactivate_discourse, name='auth_deactivate_discourse'), + url(r'^discourse_sso$', services.views.discourse_sso, name='auth_discourse_sso'), # IPS4 Service Control url(r'^activate_ips4/$', services.views.activate_ips4, diff --git a/authentication/managers.py b/authentication/managers.py index f9376a42..6471880b 100755 --- a/authentication/managers.py +++ b/authentication/managers.py @@ -110,17 +110,6 @@ class AuthServicesInfoManager: else: logger.error("Failed to update user %s discord info: user does not exist." % user) - @staticmethod - def update_user_discourse_info(username, user): - if User.objects.filter(username=user.username).exists(): - logger.debug("Updating user %s discourse info: username %s" % (user, username)) - authserviceinfo = AuthServicesInfo.objects.get_or_create(user=user)[0] - authserviceinfo.discourse_username = username - authserviceinfo.save(update_fields=['discourse_username']) - logger.info("Updated user %s discourse info in authservicesinfo model." % user) - else: - logger.error("Failed to update user %s discourse info: user does not exist." % user) - @staticmethod def update_user_ips4_info(username, id, user): if User.objects.filter(username=user.username).exists(): diff --git a/authentication/migrations/0009_auto_20161021_0228.py b/authentication/migrations/0009_auto_20161021_0228.py new file mode 100644 index 00000000..5123ac22 --- /dev/null +++ b/authentication/migrations/0009_auto_20161021_0228.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.2 on 2016-10-21 02:28 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('authentication', '0008_set_state'), + ] + + operations = [ + migrations.RemoveField( + model_name='authservicesinfo', + name='discourse_username', + ), + migrations.AddField( + model_name='authservicesinfo', + name='discourse_enabled', + field=models.BooleanField(default=False), + ), + ] diff --git a/authentication/models.py b/authentication/models.py index b9a69bde..6782e5e8 100755 --- a/authentication/models.py +++ b/authentication/models.py @@ -21,7 +21,7 @@ class AuthServicesInfo(models.Model): teamspeak3_uid = models.CharField(max_length=254, blank=True, default="") teamspeak3_perm_key = models.CharField(max_length=254, blank=True, default="") discord_uid = models.CharField(max_length=254, blank=True, default="") - discourse_username = models.CharField(max_length=254, blank=True, default="") + discourse_enabled = models.BooleanField(default=False, blank=True) ips4_username = models.CharField(max_length=254, blank=True, default="") ips4_id = models.CharField(max_length=254, blank=True, default="") smf_username = models.CharField(max_length=254, blank=True, default="") diff --git a/authentication/views.py b/authentication/views.py index 6093c119..745a74bd 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -28,7 +28,10 @@ def login_user(request): if user.is_active: logger.info("Successful login attempt from user %s" % user) login(request, user) - return redirect("auth_dashboard") + redirect_to = request.POST.get('next', request.GET.get('next', '')) + if not redirect_to: + redirect_to = 'auth_dashboard' + return redirect(redirect_to) else: logger.info("Login attempt failed for user %s: user marked inactive." % user) messages.warning(request, 'Your account has been disabled.') diff --git a/requirements.txt b/requirements.txt index e4223b4c..aa50fe15 100755 --- a/requirements.txt +++ b/requirements.txt @@ -5,7 +5,7 @@ dnspython passlib requests>=2.9.1 bcrypt -#zeroc-ice +zeroc-ice slugify requests-oauthlib sleekxmpp diff --git a/services/managers/discourse_manager.py b/services/managers/discourse_manager.py index bc87f099..060e345e 100644 --- a/services/managers/discourse_manager.py +++ b/services/managers/discourse_manager.py @@ -11,6 +11,13 @@ from services.models import GroupCache logger = logging.getLogger(__name__) +class DiscourseError(Exception): + def __init__(self, endpoint, errors): + self.endpoint = endpoint + self.errors = errors + def __str__(self): + return "API execution failed.\nErrors: %s\nEndpoint: %s" % (self.errors, self.endpoint) + # not exhaustive, only the ones we need ENDPOINTS = { 'groups': { @@ -112,6 +119,22 @@ ENDPOINTS = { 'optional': [], }, }, + 'logout': { + 'path': "/admin/users/%s/log_out", + 'method': requests.post, + 'args': { + 'required': [], + 'optional': [], + }, + }, + 'external': { + 'path': "/users/by-external/%s.json", + 'method': requests.get, + 'args': { + 'required': [], + 'optional': [], + }, + }, }, } @@ -131,10 +154,9 @@ class DiscourseManager: 'api_key': settings.DISCOURSE_API_KEY, 'api_username': settings.DISCOURSE_API_USERNAME, } + silent = kwargs.pop('silent', False) if args: - path = endpoint['path'] % args - else: - path = endpoint['path'] + endpoint['path'] = endpoint['path'] % args data = {} for arg in endpoint['args']['required']: data[arg] = kwargs[arg] @@ -142,21 +164,24 @@ class DiscourseManager: if arg in kwargs: data[arg] = kwargs[arg] for arg in kwargs: - if arg not in endpoint['args']['required'] and arg not in endpoint['args']['optional']: + if arg not in endpoint['args']['required'] and arg not in endpoint['args']['optional'] and not silent: logger.warn("Received unrecognized kwarg %s for endpoint %s" % (arg, endpoint)) - r = endpoint['method'](settings.DISCOURSE_URL + path, params=params, json=data) - out = r.text + r = endpoint['method'](settings.DISCOURSE_URL + endpoint['path'], params=params, json=data) try: - if 'errors' in r.json(): + if 'errors' in r.json() and not silent: logger.error("Discourse execution failed.\nEndpoint: %s\nErrors: %s" % (endpoint, r.json()['errors'])) - r.raise_for_status() + raise DiscourseError(endpoint, r.json()['errors']) if 'success' in r.json(): - if not r.json()['success']: - raise Exception("Execution failed") + if not r.json()['success'] and not silent: + raise DiscourseError(endpoint, None) out = r.json() except ValueError: - logger.warn("No json data received for endpoint %s" % endpoint) - r.raise_for_status() + out = r.text + finally: + try: + r.raise_for_status() + except requests.exceptions.HTTPError as e: + raise DiscourseError(endpoint, e.response.status_code) return out @staticmethod @@ -235,8 +260,8 @@ class DiscourseManager: return [g['id'] for g in data['user']['groups'] if not g['automatic']] @staticmethod - def __user_name_to_id(name): - data = DiscourseManager.__get_user(name) + def __user_name_to_id(name, silent=False): + data = DiscourseManager.__get_user(name, silent=silent) return data['user']['id'] @staticmethod @@ -244,9 +269,9 @@ class DiscourseManager: raise NotImplementedError @staticmethod - def __get_user(username): + def __get_user(username, silent=False): endpoint = ENDPOINTS['users']['get'] - return DiscourseManager.__exc(endpoint, username) + return DiscourseManager.__exc(endpoint, username, silent=silent) @staticmethod def __activate_user(username): @@ -268,7 +293,7 @@ class DiscourseManager: @staticmethod def __check_if_user_exists(username): try: - DiscourseManager.__user_name_to_id(username) + DiscourseManager.__user_name_to_id(username, silent=True) return True except: return False @@ -292,11 +317,26 @@ class DiscourseManager: return DiscourseManager.__exc(endpoint, username, email=email) @staticmethod - def _sanatize_username(username): - sanatized = username.replace(" ", "_") - sanatized = sanatized.strip(' _') - sanatized = sanatized.replace("'", "") - return sanatized + def __logout(id): + endpoint = ENDPOINTS['users']['logout'] + return DiscourseManager.__exc(endpoint, id) + + @staticmethod + def __get_user_by_external(id): + endpoint = ENDPOINTS['users']['external'] + return DiscourseManager.__exc(endpoint, id) + + @staticmethod + def __user_id_by_external_id(id): + data = DiscourseManager.__get_user_by_external(id) + return data['user']['id'] + + @staticmethod + def _sanitize_username(username): + sanitized = username.replace(" ", "_") + sanitized = sanitized.strip(' _') + sanitized = sanitized.replace("'", "") + return sanitized @staticmethod def _sanitize_groupname(name): @@ -304,42 +344,14 @@ class DiscourseManager: return re.sub('[^\w]', '', name) @staticmethod - def add_user(username, email): - logger.debug("Adding new discourse user %s" % username) - password = DiscourseManager.__generate_random_pass() - safe_username = DiscourseManager._sanatize_username(username) - try: - if DiscourseManager.__check_if_user_exists(safe_username): - logger.debug("Discourse user %s already exists. Reactivating" % safe_username) - DiscourseManager.__unsuspend(safe_username) - else: - logger.debug("Creating new user account for %s" % username) - DiscourseManager.__create_user(safe_username, email, password) - logger.info("Added new discourse user %s" % username) - return safe_username, password - except: - logger.exception("Failed to add new discourse user %s" % username) - return "", "" - - @staticmethod - def delete_user(username): - logger.debug("Deleting discourse user %s" % username) - try: - DiscourseManager.__suspend_user(username) - logger.info("Deleted discourse user %s" % username) - return True - except: - logger.exception("Failed to delete discourse user %s" % username) - return False - - @staticmethod - def update_groups(username, raw_groups): + def update_groups(user): groups = [] - for g in raw_groups: - groups.append(DiscourseManager._sanitize_groupname(g[:20])) - logger.debug("Updating discourse user %s groups to %s" % (username, groups)) + for g in user.groups.all(): + groups.append(DiscourseManager._sanitize_groupname(str(g)[:20])) + logger.debug("Updating discourse user %s groups to %s" % (user, groups)) group_dict = DiscourseManager.__generate_group_dict(groups) inv_group_dict = {v: k for k, v in group_dict.items()} + username = DiscourseManager.__get_user_by_external(user.pk)['user']['username'] user_groups = DiscourseManager.__get_user_groups(username) add_groups = [group_dict[x] for x in group_dict if not group_dict[x] in user_groups] rem_groups = [x for x in user_groups if not x in inv_group_dict] @@ -350,3 +362,12 @@ class DiscourseManager: DiscourseManager.__add_user_to_group(g, username) for g in rem_groups: DiscourseManager.__remove_user_from_group(g, username) + + @staticmethod + def disable_user(user): + logger.debug("Disabling user %s Discourse access." % user) + d_user = DiscourseManager.__get_user_by_external(user.pk) + DiscourseManager.__logout(d_user['user']['id']) + DiscourseManager.__suspend_user(d_user['user']['username']) + logger.info("Disabled user %s Discourse access." % user) + return True diff --git a/services/managers/util/ts3.py b/services/managers/util/ts3.py index c61b830e..d36d2c00 100755 --- a/services/managers/util/ts3.py +++ b/services/managers/util/ts3.py @@ -249,7 +249,7 @@ class TS3Server(TS3Proto): self.send_command('use', keys={'sid': id}) -class TeamspeakError: +class TeamspeakError(Exception): def __init__(self, code, msg=None): self.code = str(code) if not msg: diff --git a/services/signals.py b/services/signals.py index acc1102d..41ad0ddc 100644 --- a/services/signals.py +++ b/services/signals.py @@ -43,7 +43,7 @@ def m2m_changed_user_groups(sender, instance, action, *args, **kwargs): update_discord_groups.delay(instance.pk) if auth.mumble_username: update_mumble_groups.delay(instance.pk) - if auth.discourse_username: + if auth.discourse_enabled: update_discourse_groups.delay(instance.pk) if auth.smf_username: update_smf_groups.delay(instance.pk) diff --git a/services/tasks.py b/services/tasks.py index cbafb725..5ceb0571 100644 --- a/services/tasks.py +++ b/services/tasks.py @@ -181,10 +181,11 @@ def deactivate_services(user): marketManager.disable_user(authinfo.market_username) AuthServicesInfoManager.update_user_market_info("", user) change = True - if authinfo.discourse_username and authinfo.discourse_username != "": - logger.debug("User %s has a Discourse account %s. Deleting." % (user, authinfo.discourse_username)) - DiscourseManager.delete_user(authinfo.discourse_username) - AuthServicesInfoManager.update_user_discourse_info("", user) + if authinfo.discourse_enabled: + logger.debug("User %s has a Discourse account. Disabling login." % user) + DiscourseManager.disable_user(user) + authinfo.discourse_enabled = False + authinfo.save() change = True if authinfo.smf_username and authinfo.smf_username != "": logger.debug("User %s has a SMF account %s. Deleting." % (user, authinfo.smf_username)) @@ -195,8 +196,8 @@ def deactivate_services(user): notify(user, "Services Disabled", message="Your services accounts have been disabled.", level="danger") -@task -def validate_services(user, state): +@task(bind=True) +def validate_services(self, user, state): if state == MEMBER_STATE: setting_string = 'AUTH' elif state == BLUE_STATE: @@ -238,9 +239,10 @@ def validate_services(user, state): marketManager.disable_user(auth.market_username) AuthServicesInfoManager.update_user_market_info("", user) notify(user, 'Alliance Market Account Disabled', level='danger') - if auth.discourse_username and not getattr(settings, 'ENABLE_%s_DISCOURSE' % setting_string, False): - DiscourseManager.delete_user(auth.discourse_username) - AuthServicesInfoManager.update_user_discourse_info("", user) + if auth.discourse_enabled and not getattr(settings, 'ENABLE_%s_DISCOURSE' % setting_string, False): + DiscourseManager.disable_user(user) + authinfo.discourse_enabled = False + autninfo.save() notify(user, 'Discourse Account Disabled', level='danger') if auth.smf_username and not getattr(settings, 'ENABLE_%s_SMF' % setting_string, False): smfManager.disable_user(auth.smf_username) @@ -248,8 +250,8 @@ def validate_services(user, state): notify(user, "SMF Account Disabled", level='danger') -@task -def update_jabber_groups(pk): +@task(bind=True) +def update_jabber_groups(self, pk): user = User.objects.get(pk=pk) logger.debug("Updating jabber groups for user %s" % user) authserviceinfo = AuthServicesInfo.objects.get(user=user) @@ -274,8 +276,8 @@ def update_all_jabber_groups(): update_jabber_groups.delay(user.user_id) -@task -def update_mumble_groups(pk): +@task(bind=True) +def update_mumble_groups(self, pk): user = User.objects.get(pk=pk) logger.debug("Updating mumble groups for user %s" % user) authserviceinfo = AuthServicesInfo.objects.get(user=user) @@ -300,8 +302,8 @@ def update_all_mumble_groups(): update_mumble_groups.delay(user.user_id) -@task -def update_forum_groups(pk): +@task(bind=True) +def update_forum_groups(self, pk): user = User.objects.get(pk=pk) logger.debug("Updating forum groups for user %s" % user) authserviceinfo = AuthServicesInfo.objects.get(user=user) @@ -326,8 +328,8 @@ def update_all_forum_groups(): update_forum_groups.delay(user.user_id) -@task -def update_smf_groups(pk): +@task(bind=True) +def update_smf_groups(self, pk): user = User.objects.get(pk=pk) logger.debug("Updating smf groups for user %s" % user) authserviceinfo = AuthServicesInfo.objects.get(user=user) @@ -352,8 +354,8 @@ def update_all_smf_groups(): update_smf_groups.delay(user.user_id) -@task -def update_ipboard_groups(pk): +@task(bind=True) +def update_ipboard_groups(self, pk): user = User.objects.get(pk=pk) logger.debug("Updating user %s ipboard groups." % user) authserviceinfo = AuthServicesInfo.objects.get(user=user) @@ -378,8 +380,8 @@ def update_all_ipboard_groups(): update_ipboard_groups.delay(user.user_id) -@task -def update_teamspeak3_groups(pk): +@task(bind=True) +def update_teamspeak3_groups(self, pk): user = User.objects.get(pk=pk) logger.debug("Updating user %s teamspeak3 groups" % user) usergroups = user.groups.all() @@ -407,8 +409,8 @@ def update_all_teamspeak3_groups(): update_teamspeak3_groups.delay(user.user_id) -@task -def update_discord_groups(pk): +@task(bind=True) +def update_discord_groups(self, pk): user = User.objects.get(pk=pk) logger.debug("Updating discord groups for user %s" % user) authserviceinfo = AuthServicesInfo.objects.get(user=user) @@ -434,8 +436,8 @@ def update_all_discord_groups(): update_discord_groups.delay(user.user_id) -@task -def update_discord_nickname(pk): +@task(bind=True) +def update_discord_nickname(self, pk): user = User.objects.get(pk=pk) logger.debug("Updating discord nickname for user %s" % user) authserviceinfo = AuthServicesInfo.objects.get(user=user) @@ -456,22 +458,14 @@ def update_all_discord_nicknames(): update_discord_nickname(user.user_id) -@task -def update_discourse_groups(pk): +@task(bind=True) +def update_discourse_groups(self, pk): user = User.objects.get(pk=pk) logger.debug("Updating discourse groups for user %s" % user) - authserviceinfo = AuthServicesInfo.objects.get(user=user) - groups = [] - for group in user.groups.all(): - groups.append(str(group.name)) - if len(groups) == 0: - logger.debug("No syncgroups found for user. Adding empty group.") - groups.append('empty') - logger.debug("Updating user %s discourse groups to %s" % (user, groups)) try: - DiscourseManager.update_groups(authserviceinfo.discourse_username, groups) + DiscourseManager.update_groups(user) except: - logger.warn("Discourse group sync failed for %s, retrying in 10 mins" % user, exc_info=True) + logger.warn("Discourse group sync failed for %s, retrying in 10 mins" % user) raise self.retry(countdown=60 * 10) logger.debug("Updated user %s discourse groups." % user) diff --git a/services/views.py b/services/views.py index ecf352dd..1d5e08a5 100755 --- a/services/views.py +++ b/services/views.py @@ -38,6 +38,18 @@ from services.forms import TeamspeakJoinForm from authentication.decorators import members_and_blues from authentication.states import MEMBER_STATE, BLUE_STATE +import base64 +import hmac +import hashlib +try: + from urllib import unquote, urlencode +except ImportError: #py3 + from urllib.parse import unquote, urlencode +try: + from urlparse import parse_qs +except ImportError: #py3 + from urllib.parse import parse_qs + import datetime import logging @@ -131,6 +143,12 @@ def jabber_broadcast_view(request): def services_view(request): logger.debug("services_view called by user %s" % request.user) auth = AuthServicesInfo.objects.get_or_create(user=request.user)[0] + char = None + if auth.main_char_id: + try: + char = EveCharacter.objects.get(character_id=auth.main_char_id) + except EveCharacter.DoesNotExist: + messages.warning(request, "There's a problem with your main character. Please select a new one.") services = [ 'FORUM', @@ -146,7 +164,10 @@ def services_view(request): 'XENFORO', ] - context = {'authinfo': auth} + context = { + 'authinfo': auth, + 'char': char, + } for s in services: context['SHOW_' + s] = (getattr(settings, 'ENABLE_AUTH_' + s) and ( @@ -817,49 +838,6 @@ def set_ipboard_password(request): return render(request, 'registered/service_password.html', context=context) -@login_required -@members_and_blues() -def activate_discourse(request): - logger.debug("activate_discourse called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get_or_create(user=request.user)[0] - character = EveManager.get_character_by_id(authinfo.main_char_id) - logger.debug("Adding discourse user for user %s with main character %s" % (request.user, character)) - result = DiscourseManager.add_user(character.character_name, request.user.email) - if result[0] != "": - AuthServicesInfoManager.update_user_discourse_info(result[0], request.user) - logger.debug("Updated authserviceinfo for user %s with discourse credentials. Updating groups." % request.user) - update_discourse_groups.delay(request.user.pk) - logger.info("Successfully activated discourse for user %s" % request.user) - messages.success(request, 'Activated Discourse account.') - messages.warning(request, 'Do not lose your Discourse password. It cannot be reset through auth.') - credentials = { - 'username': result[0], - 'password': result[1], - } - return render(request, 'registered/service_credentials.html', - context={'credentials': credentials, 'service': 'Discourse'}) - else: - logger.error("Unsuccessful attempt to activate discourse for user %s" % request.user) - messages.error(request, 'An error occurred while processing your Discourse account.') - return redirect("auth_services") - - -@login_required -@members_and_blues() -def deactivate_discourse(request): - logger.debug("deactivate_discourse called by user %s" % request.user) - authinfo = AuthServicesInfo.objects.get_or_create(user=request.user)[0] - result = DiscourseManager.delete_user(authinfo.discourse_username) - if result: - AuthServicesInfoManager.update_user_discourse_info("", request.user) - logger.info("Successfully deactivated discourse for user %s" % request.user) - messages.success(request, 'Deactivated Discourse account.') - else: - logger.error("Unsuccessful attempt to activate discourse for user %s" % request.user) - messages.error(request, 'An error occurred while processing your Discourse account.') - return redirect("auth_services") - - @login_required @members_and_blues() def activate_ips4(request): @@ -1145,3 +1123,87 @@ def set_market_password(request): logger.debug("Rendering form for user %s" % request.user) context = {'form': form, 'service': 'Market'} return render(request, 'registered/service_password.html', context=context) + + +@login_required +def discourse_sso(request): + + ## Check if user has access + + auth, c = AuthServicesInfo.objects.get_or_create(user=request.user) + if not request.user.is_superuser: + if auth.state == MEMBER_STATE and not settings.ENABLE_AUTH_DISCOURSE: + messages.error(request, 'You are not authorized to access Discourse.') + return redirect('auth_dashboard') + elif auth.state == BLUE_STATE and not settings.ENABLE_BLUE_DISCOURSE: + messages.error(request, 'You are not authorized to access Discourse.') + return redirect('auth_dashboard') + else: + messages.error(request, 'You are not authorized to access Discourse.') + return redirect('auth_dashboard') + + if not auth.main_char_id: + messages.error(request, "You must have a main character set to access Discourse.") + return redirect('auth_characters') + try: + main_char = EveCharacter.objects.get(character_id=auth.main_char_id) + except EveCharacter.DoesNotExist: + messages.error(request, "Your main character is missing a database model. Please select a new one.") + return redirect('auth_characters') + + payload = request.GET.get('sso') + signature = request.GET.get('sig') + + if None in [payload, signature]: + messages.error(request, 'No SSO payload or signature. Please contact support if this problem persists.') + return redirect('auth_dashboard') + + ## Validate the payload + + try: + payload = unquote(payload).encode('utf-8') + decoded = base64.decodestring(payload).decode('utf-8') + assert 'nonce' in decoded + assert len(payload) > 0 + except AssertionError: + messages.error(request, 'Invalid payload. Please contact support if this problem persists.') + return redirect('auth_dashboard') + + key = str(settings.DISCOURSE_SSO_SECRET).encode('utf-8') + h = hmac.new(key, payload, digestmod=hashlib.sha256) + this_signature = h.hexdigest() + + if this_signature != signature: + messages.error(request, 'Invalid payload. Please contact support if this problem persists.') + return redirect('auth_dashboard') + + ## Build the return payload + + username = DiscourseManager._sanitize_username(main_char.character_name) + + qs = parse_qs(decoded) + params = { + 'nonce': qs['nonce'][0], + 'email': request.user.email, + 'external_id': request.user.pk, + 'username': username, + 'name': username, + } + + if auth.main_char_id: + params['avatar_url'] = 'https://image.eveonline.com/Character/%s_256.jpg' % auth.main_char_id + + return_payload = base64.encodestring(urlencode(params).encode('utf-8')) + h = hmac.new(key, return_payload, digestmod=hashlib.sha256) + query_string = urlencode({'sso': return_payload, 'sig': h.hexdigest()}) + + ## Record activation and queue group sync + + auth.discourse_enabled = True + auth.save() + update_discourse_groups.delay(request.user.pk) + + ## Redirect back to Discourse + + url = '%s/session/sso_login' % settings.DISCOURSE_URL + return redirect('%s?%s' % (url, query_string)) diff --git a/stock/templates/public/login.html b/stock/templates/public/login.html index d5469429..c8ecfa23 100644 --- a/stock/templates/public/login.html +++ b/stock/templates/public/login.html @@ -60,6 +60,7 @@ {% csrf_token %}

{% trans "Please sign in" %}

{{ form|bootstrap }} +
diff --git a/stock/templates/registered/services.html b/stock/templates/registered/services.html index f91a42f7..e3e3423c 100755 --- a/stock/templates/registered/services.html +++ b/stock/templates/registered/services.html @@ -238,18 +238,10 @@ {% endif %} {% if SHOW_DISCOURSE %} Discourse - {{ authinfo.discourse_username }} + {{ char.character_name }} {{ DISCOURSE_URL }} - {% ifequal authinfo.discourse_username "" %} - - - - {% else %} - - - - {% endifequal %} + {% endif %} {% if SHOW_TEAMSPEAK3 %}