mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-13 22:40:16 +02:00
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.
This commit is contained in:
parent
1daf77709d
commit
4ff21b25c3
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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():
|
||||
|
24
authentication/migrations/0009_auto_20161021_0228.py
Normal file
24
authentication/migrations/0009_auto_20161021_0228.py
Normal file
@ -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),
|
||||
),
|
||||
]
|
@ -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="")
|
||||
|
@ -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.')
|
||||
|
@ -5,7 +5,7 @@ dnspython
|
||||
passlib
|
||||
requests>=2.9.1
|
||||
bcrypt
|
||||
#zeroc-ice
|
||||
zeroc-ice
|
||||
slugify
|
||||
requests-oauthlib
|
||||
sleekxmpp
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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))
|
||||
|
@ -60,6 +60,7 @@
|
||||
{% csrf_token %}
|
||||
<h2 class="form-signin-heading text-center">{% trans "Please sign in" %}</h2>
|
||||
{{ form|bootstrap }}
|
||||
<input type="hidden" name="next" value="{{ request.GET.next }}" />
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Sign in" %}</button>
|
||||
|
@ -238,18 +238,10 @@
|
||||
{% endif %}
|
||||
{% if SHOW_DISCOURSE %}
|
||||
<td class="text-center">Discourse</td>
|
||||
<td class="text-center">{{ authinfo.discourse_username }}</td>
|
||||
<td class="text-center">{{ char.character_name }}</td>
|
||||
<td class="text-center"><a href="{{ DISCOURSE_URL }}">{{ DISCOURSE_URL }}</a></td>
|
||||
<td class="text-center">
|
||||
{% ifequal authinfo.discourse_username "" %}
|
||||
<a href="{% url 'auth_activate_discourse' %}" class="btn btn-warning">
|
||||
<span class="glyphicon glyphicon-ok"></span>
|
||||
</a>
|
||||
{% else %}
|
||||
<a href="{% url 'auth_deactivate_discourse' %}" class="btn btn-danger">
|
||||
<span class="glyphicon glyphicon-remove"></span>
|
||||
</a>
|
||||
{% endifequal %}
|
||||
<a class="btn btn-success" href="{{ DISCOURSE_URL }}"><span class="glyphicon glyphicon-arrow-right"></span></a>
|
||||
</td>
|
||||
{% endif %}
|
||||
{% if SHOW_TEAMSPEAK3 %}
|
||||
|
Loading…
x
Reference in New Issue
Block a user