mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-16 16:00:17 +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 = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.db.backends.mysql',
|
'ENGINE': 'django.db.backends.mysql',
|
||||||
'NAME': 'aa_test',
|
'NAME': 'alliance_auth',
|
||||||
'USER': os.environ.get('AA_DB_DEFAULT_USER', 'allianceserver'),
|
'USER': os.environ.get('AA_DB_DEFAULT_USER', 'allianceserver'),
|
||||||
'PASSWORD': os.environ.get('AA_DB_DEFAULT_PASSWORD', 'password'),
|
'PASSWORD': os.environ.get('AA_DB_DEFAULT_PASSWORD', 'password'),
|
||||||
'HOST': os.environ.get('AA_DB_DEFAULT_HOST', '127.0.0.1'),
|
'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_URL - Web address of the forums (no trailing slash)
|
||||||
# DISCOURSE_API_USERNAME - API account username
|
# DISCOURSE_API_USERNAME - API account username
|
||||||
# DISCOURSE_API_KEY - API Key
|
# DISCOURSE_API_KEY - API Key
|
||||||
|
# DISCOURSE_SSO_SECRET - SSO secret key
|
||||||
######################################
|
######################################
|
||||||
DISCOURSE_URL = os.environ.get('AA_DISCOURSE_URL', '')
|
DISCOURSE_URL = os.environ.get('AA_DISCOURSE_URL', '')
|
||||||
DISCOURSE_API_USERNAME = os.environ.get('AA_DISCOURSE_API_USERNAME', '')
|
DISCOURSE_API_USERNAME = os.environ.get('AA_DISCOURSE_API_USERNAME', '')
|
||||||
DISCOURSE_API_KEY = os.environ.get('AA_DISCOURSE_API_KEY', '')
|
DISCOURSE_API_KEY = os.environ.get('AA_DISCOURSE_API_KEY', '')
|
||||||
|
DISCOURSE_SSO_SECRET = os.environ.get('AA_DISCOURSE_SSO_SECRET', '')
|
||||||
|
|
||||||
#####################################
|
#####################################
|
||||||
# IPS4 Configuration
|
# IPS4 Configuration
|
||||||
|
@ -94,8 +94,7 @@ urlpatterns = [
|
|||||||
url(r'^discord_add_bot/$', services.views.discord_add_bot, name='auth_discord_add_bot'),
|
url(r'^discord_add_bot/$', services.views.discord_add_bot, name='auth_discord_add_bot'),
|
||||||
|
|
||||||
# Discourse Service Control
|
# Discourse Service Control
|
||||||
url(r'^activate_discourse/$', services.views.activate_discourse, name='auth_activate_discourse'),
|
url(r'^discourse_sso$', services.views.discourse_sso, name='auth_discourse_sso'),
|
||||||
url(r'^deactivate_discourse/$', services.views.deactivate_discourse, name='auth_deactivate_discourse'),
|
|
||||||
|
|
||||||
# IPS4 Service Control
|
# IPS4 Service Control
|
||||||
url(r'^activate_ips4/$', services.views.activate_ips4,
|
url(r'^activate_ips4/$', services.views.activate_ips4,
|
||||||
|
@ -110,17 +110,6 @@ class AuthServicesInfoManager:
|
|||||||
else:
|
else:
|
||||||
logger.error("Failed to update user %s discord info: user does not exist." % user)
|
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
|
@staticmethod
|
||||||
def update_user_ips4_info(username, id, user):
|
def update_user_ips4_info(username, id, user):
|
||||||
if User.objects.filter(username=user.username).exists():
|
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_uid = models.CharField(max_length=254, blank=True, default="")
|
||||||
teamspeak3_perm_key = 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="")
|
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_username = models.CharField(max_length=254, blank=True, default="")
|
||||||
ips4_id = 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="")
|
smf_username = models.CharField(max_length=254, blank=True, default="")
|
||||||
|
@ -28,7 +28,10 @@ def login_user(request):
|
|||||||
if user.is_active:
|
if user.is_active:
|
||||||
logger.info("Successful login attempt from user %s" % user)
|
logger.info("Successful login attempt from user %s" % user)
|
||||||
login(request, 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:
|
else:
|
||||||
logger.info("Login attempt failed for user %s: user marked inactive." % user)
|
logger.info("Login attempt failed for user %s: user marked inactive." % user)
|
||||||
messages.warning(request, 'Your account has been disabled.')
|
messages.warning(request, 'Your account has been disabled.')
|
||||||
|
@ -5,7 +5,7 @@ dnspython
|
|||||||
passlib
|
passlib
|
||||||
requests>=2.9.1
|
requests>=2.9.1
|
||||||
bcrypt
|
bcrypt
|
||||||
#zeroc-ice
|
zeroc-ice
|
||||||
slugify
|
slugify
|
||||||
requests-oauthlib
|
requests-oauthlib
|
||||||
sleekxmpp
|
sleekxmpp
|
||||||
|
@ -11,6 +11,13 @@ from services.models import GroupCache
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
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
|
# not exhaustive, only the ones we need
|
||||||
ENDPOINTS = {
|
ENDPOINTS = {
|
||||||
'groups': {
|
'groups': {
|
||||||
@ -112,6 +119,22 @@ ENDPOINTS = {
|
|||||||
'optional': [],
|
'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_key': settings.DISCOURSE_API_KEY,
|
||||||
'api_username': settings.DISCOURSE_API_USERNAME,
|
'api_username': settings.DISCOURSE_API_USERNAME,
|
||||||
}
|
}
|
||||||
|
silent = kwargs.pop('silent', False)
|
||||||
if args:
|
if args:
|
||||||
path = endpoint['path'] % args
|
endpoint['path'] = endpoint['path'] % args
|
||||||
else:
|
|
||||||
path = endpoint['path']
|
|
||||||
data = {}
|
data = {}
|
||||||
for arg in endpoint['args']['required']:
|
for arg in endpoint['args']['required']:
|
||||||
data[arg] = kwargs[arg]
|
data[arg] = kwargs[arg]
|
||||||
@ -142,21 +164,24 @@ class DiscourseManager:
|
|||||||
if arg in kwargs:
|
if arg in kwargs:
|
||||||
data[arg] = kwargs[arg]
|
data[arg] = kwargs[arg]
|
||||||
for arg in kwargs:
|
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))
|
logger.warn("Received unrecognized kwarg %s for endpoint %s" % (arg, endpoint))
|
||||||
r = endpoint['method'](settings.DISCOURSE_URL + path, params=params, json=data)
|
r = endpoint['method'](settings.DISCOURSE_URL + endpoint['path'], params=params, json=data)
|
||||||
out = r.text
|
|
||||||
try:
|
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']))
|
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 'success' in r.json():
|
||||||
if not r.json()['success']:
|
if not r.json()['success'] and not silent:
|
||||||
raise Exception("Execution failed")
|
raise DiscourseError(endpoint, None)
|
||||||
out = r.json()
|
out = r.json()
|
||||||
except ValueError:
|
except ValueError:
|
||||||
logger.warn("No json data received for endpoint %s" % endpoint)
|
out = r.text
|
||||||
r.raise_for_status()
|
finally:
|
||||||
|
try:
|
||||||
|
r.raise_for_status()
|
||||||
|
except requests.exceptions.HTTPError as e:
|
||||||
|
raise DiscourseError(endpoint, e.response.status_code)
|
||||||
return out
|
return out
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -235,8 +260,8 @@ class DiscourseManager:
|
|||||||
return [g['id'] for g in data['user']['groups'] if not g['automatic']]
|
return [g['id'] for g in data['user']['groups'] if not g['automatic']]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __user_name_to_id(name):
|
def __user_name_to_id(name, silent=False):
|
||||||
data = DiscourseManager.__get_user(name)
|
data = DiscourseManager.__get_user(name, silent=silent)
|
||||||
return data['user']['id']
|
return data['user']['id']
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -244,9 +269,9 @@ class DiscourseManager:
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __get_user(username):
|
def __get_user(username, silent=False):
|
||||||
endpoint = ENDPOINTS['users']['get']
|
endpoint = ENDPOINTS['users']['get']
|
||||||
return DiscourseManager.__exc(endpoint, username)
|
return DiscourseManager.__exc(endpoint, username, silent=silent)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __activate_user(username):
|
def __activate_user(username):
|
||||||
@ -268,7 +293,7 @@ class DiscourseManager:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def __check_if_user_exists(username):
|
def __check_if_user_exists(username):
|
||||||
try:
|
try:
|
||||||
DiscourseManager.__user_name_to_id(username)
|
DiscourseManager.__user_name_to_id(username, silent=True)
|
||||||
return True
|
return True
|
||||||
except:
|
except:
|
||||||
return False
|
return False
|
||||||
@ -292,11 +317,26 @@ class DiscourseManager:
|
|||||||
return DiscourseManager.__exc(endpoint, username, email=email)
|
return DiscourseManager.__exc(endpoint, username, email=email)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _sanatize_username(username):
|
def __logout(id):
|
||||||
sanatized = username.replace(" ", "_")
|
endpoint = ENDPOINTS['users']['logout']
|
||||||
sanatized = sanatized.strip(' _')
|
return DiscourseManager.__exc(endpoint, id)
|
||||||
sanatized = sanatized.replace("'", "")
|
|
||||||
return sanatized
|
@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
|
@staticmethod
|
||||||
def _sanitize_groupname(name):
|
def _sanitize_groupname(name):
|
||||||
@ -304,42 +344,14 @@ class DiscourseManager:
|
|||||||
return re.sub('[^\w]', '', name)
|
return re.sub('[^\w]', '', name)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def add_user(username, email):
|
def update_groups(user):
|
||||||
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):
|
|
||||||
groups = []
|
groups = []
|
||||||
for g in raw_groups:
|
for g in user.groups.all():
|
||||||
groups.append(DiscourseManager._sanitize_groupname(g[:20]))
|
groups.append(DiscourseManager._sanitize_groupname(str(g)[:20]))
|
||||||
logger.debug("Updating discourse user %s groups to %s" % (username, groups))
|
logger.debug("Updating discourse user %s groups to %s" % (user, groups))
|
||||||
group_dict = DiscourseManager.__generate_group_dict(groups)
|
group_dict = DiscourseManager.__generate_group_dict(groups)
|
||||||
inv_group_dict = {v: k for k, v in group_dict.items()}
|
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)
|
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]
|
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]
|
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)
|
DiscourseManager.__add_user_to_group(g, username)
|
||||||
for g in rem_groups:
|
for g in rem_groups:
|
||||||
DiscourseManager.__remove_user_from_group(g, username)
|
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})
|
self.send_command('use', keys={'sid': id})
|
||||||
|
|
||||||
|
|
||||||
class TeamspeakError:
|
class TeamspeakError(Exception):
|
||||||
def __init__(self, code, msg=None):
|
def __init__(self, code, msg=None):
|
||||||
self.code = str(code)
|
self.code = str(code)
|
||||||
if not msg:
|
if not msg:
|
||||||
|
@ -43,7 +43,7 @@ def m2m_changed_user_groups(sender, instance, action, *args, **kwargs):
|
|||||||
update_discord_groups.delay(instance.pk)
|
update_discord_groups.delay(instance.pk)
|
||||||
if auth.mumble_username:
|
if auth.mumble_username:
|
||||||
update_mumble_groups.delay(instance.pk)
|
update_mumble_groups.delay(instance.pk)
|
||||||
if auth.discourse_username:
|
if auth.discourse_enabled:
|
||||||
update_discourse_groups.delay(instance.pk)
|
update_discourse_groups.delay(instance.pk)
|
||||||
if auth.smf_username:
|
if auth.smf_username:
|
||||||
update_smf_groups.delay(instance.pk)
|
update_smf_groups.delay(instance.pk)
|
||||||
|
@ -181,10 +181,11 @@ def deactivate_services(user):
|
|||||||
marketManager.disable_user(authinfo.market_username)
|
marketManager.disable_user(authinfo.market_username)
|
||||||
AuthServicesInfoManager.update_user_market_info("", user)
|
AuthServicesInfoManager.update_user_market_info("", user)
|
||||||
change = True
|
change = True
|
||||||
if authinfo.discourse_username and authinfo.discourse_username != "":
|
if authinfo.discourse_enabled:
|
||||||
logger.debug("User %s has a Discourse account %s. Deleting." % (user, authinfo.discourse_username))
|
logger.debug("User %s has a Discourse account. Disabling login." % user)
|
||||||
DiscourseManager.delete_user(authinfo.discourse_username)
|
DiscourseManager.disable_user(user)
|
||||||
AuthServicesInfoManager.update_user_discourse_info("", user)
|
authinfo.discourse_enabled = False
|
||||||
|
authinfo.save()
|
||||||
change = True
|
change = True
|
||||||
if authinfo.smf_username and authinfo.smf_username != "":
|
if authinfo.smf_username and authinfo.smf_username != "":
|
||||||
logger.debug("User %s has a SMF account %s. Deleting." % (user, 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")
|
notify(user, "Services Disabled", message="Your services accounts have been disabled.", level="danger")
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task(bind=True)
|
||||||
def validate_services(user, state):
|
def validate_services(self, user, state):
|
||||||
if state == MEMBER_STATE:
|
if state == MEMBER_STATE:
|
||||||
setting_string = 'AUTH'
|
setting_string = 'AUTH'
|
||||||
elif state == BLUE_STATE:
|
elif state == BLUE_STATE:
|
||||||
@ -238,9 +239,10 @@ def validate_services(user, state):
|
|||||||
marketManager.disable_user(auth.market_username)
|
marketManager.disable_user(auth.market_username)
|
||||||
AuthServicesInfoManager.update_user_market_info("", user)
|
AuthServicesInfoManager.update_user_market_info("", user)
|
||||||
notify(user, 'Alliance Market Account Disabled', level='danger')
|
notify(user, 'Alliance Market Account Disabled', level='danger')
|
||||||
if auth.discourse_username and not getattr(settings, 'ENABLE_%s_DISCOURSE' % setting_string, False):
|
if auth.discourse_enabled and not getattr(settings, 'ENABLE_%s_DISCOURSE' % setting_string, False):
|
||||||
DiscourseManager.delete_user(auth.discourse_username)
|
DiscourseManager.disable_user(user)
|
||||||
AuthServicesInfoManager.update_user_discourse_info("", user)
|
authinfo.discourse_enabled = False
|
||||||
|
autninfo.save()
|
||||||
notify(user, 'Discourse Account Disabled', level='danger')
|
notify(user, 'Discourse Account Disabled', level='danger')
|
||||||
if auth.smf_username and not getattr(settings, 'ENABLE_%s_SMF' % setting_string, False):
|
if auth.smf_username and not getattr(settings, 'ENABLE_%s_SMF' % setting_string, False):
|
||||||
smfManager.disable_user(auth.smf_username)
|
smfManager.disable_user(auth.smf_username)
|
||||||
@ -248,8 +250,8 @@ def validate_services(user, state):
|
|||||||
notify(user, "SMF Account Disabled", level='danger')
|
notify(user, "SMF Account Disabled", level='danger')
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task(bind=True)
|
||||||
def update_jabber_groups(pk):
|
def update_jabber_groups(self, pk):
|
||||||
user = User.objects.get(pk=pk)
|
user = User.objects.get(pk=pk)
|
||||||
logger.debug("Updating jabber groups for user %s" % user)
|
logger.debug("Updating jabber groups for user %s" % user)
|
||||||
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
||||||
@ -274,8 +276,8 @@ def update_all_jabber_groups():
|
|||||||
update_jabber_groups.delay(user.user_id)
|
update_jabber_groups.delay(user.user_id)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task(bind=True)
|
||||||
def update_mumble_groups(pk):
|
def update_mumble_groups(self, pk):
|
||||||
user = User.objects.get(pk=pk)
|
user = User.objects.get(pk=pk)
|
||||||
logger.debug("Updating mumble groups for user %s" % user)
|
logger.debug("Updating mumble groups for user %s" % user)
|
||||||
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
||||||
@ -300,8 +302,8 @@ def update_all_mumble_groups():
|
|||||||
update_mumble_groups.delay(user.user_id)
|
update_mumble_groups.delay(user.user_id)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task(bind=True)
|
||||||
def update_forum_groups(pk):
|
def update_forum_groups(self, pk):
|
||||||
user = User.objects.get(pk=pk)
|
user = User.objects.get(pk=pk)
|
||||||
logger.debug("Updating forum groups for user %s" % user)
|
logger.debug("Updating forum groups for user %s" % user)
|
||||||
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
||||||
@ -326,8 +328,8 @@ def update_all_forum_groups():
|
|||||||
update_forum_groups.delay(user.user_id)
|
update_forum_groups.delay(user.user_id)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task(bind=True)
|
||||||
def update_smf_groups(pk):
|
def update_smf_groups(self, pk):
|
||||||
user = User.objects.get(pk=pk)
|
user = User.objects.get(pk=pk)
|
||||||
logger.debug("Updating smf groups for user %s" % user)
|
logger.debug("Updating smf groups for user %s" % user)
|
||||||
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
||||||
@ -352,8 +354,8 @@ def update_all_smf_groups():
|
|||||||
update_smf_groups.delay(user.user_id)
|
update_smf_groups.delay(user.user_id)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task(bind=True)
|
||||||
def update_ipboard_groups(pk):
|
def update_ipboard_groups(self, pk):
|
||||||
user = User.objects.get(pk=pk)
|
user = User.objects.get(pk=pk)
|
||||||
logger.debug("Updating user %s ipboard groups." % user)
|
logger.debug("Updating user %s ipboard groups." % user)
|
||||||
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
||||||
@ -378,8 +380,8 @@ def update_all_ipboard_groups():
|
|||||||
update_ipboard_groups.delay(user.user_id)
|
update_ipboard_groups.delay(user.user_id)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task(bind=True)
|
||||||
def update_teamspeak3_groups(pk):
|
def update_teamspeak3_groups(self, pk):
|
||||||
user = User.objects.get(pk=pk)
|
user = User.objects.get(pk=pk)
|
||||||
logger.debug("Updating user %s teamspeak3 groups" % user)
|
logger.debug("Updating user %s teamspeak3 groups" % user)
|
||||||
usergroups = user.groups.all()
|
usergroups = user.groups.all()
|
||||||
@ -407,8 +409,8 @@ def update_all_teamspeak3_groups():
|
|||||||
update_teamspeak3_groups.delay(user.user_id)
|
update_teamspeak3_groups.delay(user.user_id)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task(bind=True)
|
||||||
def update_discord_groups(pk):
|
def update_discord_groups(self, pk):
|
||||||
user = User.objects.get(pk=pk)
|
user = User.objects.get(pk=pk)
|
||||||
logger.debug("Updating discord groups for user %s" % user)
|
logger.debug("Updating discord groups for user %s" % user)
|
||||||
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
||||||
@ -434,8 +436,8 @@ def update_all_discord_groups():
|
|||||||
update_discord_groups.delay(user.user_id)
|
update_discord_groups.delay(user.user_id)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task(bind=True)
|
||||||
def update_discord_nickname(pk):
|
def update_discord_nickname(self, pk):
|
||||||
user = User.objects.get(pk=pk)
|
user = User.objects.get(pk=pk)
|
||||||
logger.debug("Updating discord nickname for user %s" % user)
|
logger.debug("Updating discord nickname for user %s" % user)
|
||||||
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
authserviceinfo = AuthServicesInfo.objects.get(user=user)
|
||||||
@ -456,22 +458,14 @@ def update_all_discord_nicknames():
|
|||||||
update_discord_nickname(user.user_id)
|
update_discord_nickname(user.user_id)
|
||||||
|
|
||||||
|
|
||||||
@task
|
@task(bind=True)
|
||||||
def update_discourse_groups(pk):
|
def update_discourse_groups(self, pk):
|
||||||
user = User.objects.get(pk=pk)
|
user = User.objects.get(pk=pk)
|
||||||
logger.debug("Updating discourse groups for user %s" % user)
|
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:
|
try:
|
||||||
DiscourseManager.update_groups(authserviceinfo.discourse_username, groups)
|
DiscourseManager.update_groups(user)
|
||||||
except:
|
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)
|
raise self.retry(countdown=60 * 10)
|
||||||
logger.debug("Updated user %s discourse groups." % user)
|
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.decorators import members_and_blues
|
||||||
from authentication.states import MEMBER_STATE, BLUE_STATE
|
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 datetime
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
@ -131,6 +143,12 @@ def jabber_broadcast_view(request):
|
|||||||
def services_view(request):
|
def services_view(request):
|
||||||
logger.debug("services_view called by user %s" % request.user)
|
logger.debug("services_view called by user %s" % request.user)
|
||||||
auth = AuthServicesInfo.objects.get_or_create(user=request.user)[0]
|
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 = [
|
services = [
|
||||||
'FORUM',
|
'FORUM',
|
||||||
@ -146,7 +164,10 @@ def services_view(request):
|
|||||||
'XENFORO',
|
'XENFORO',
|
||||||
]
|
]
|
||||||
|
|
||||||
context = {'authinfo': auth}
|
context = {
|
||||||
|
'authinfo': auth,
|
||||||
|
'char': char,
|
||||||
|
}
|
||||||
|
|
||||||
for s in services:
|
for s in services:
|
||||||
context['SHOW_' + s] = (getattr(settings, 'ENABLE_AUTH_' + s) and (
|
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)
|
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
|
@login_required
|
||||||
@members_and_blues()
|
@members_and_blues()
|
||||||
def activate_ips4(request):
|
def activate_ips4(request):
|
||||||
@ -1145,3 +1123,87 @@ def set_market_password(request):
|
|||||||
logger.debug("Rendering form for user %s" % request.user)
|
logger.debug("Rendering form for user %s" % request.user)
|
||||||
context = {'form': form, 'service': 'Market'}
|
context = {'form': form, 'service': 'Market'}
|
||||||
return render(request, 'registered/service_password.html', context=context)
|
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 %}
|
{% csrf_token %}
|
||||||
<h2 class="form-signin-heading text-center">{% trans "Please sign in" %}</h2>
|
<h2 class="form-signin-heading text-center">{% trans "Please sign in" %}</h2>
|
||||||
{{ form|bootstrap }}
|
{{ form|bootstrap }}
|
||||||
|
<input type="hidden" name="next" value="{{ request.GET.next }}" />
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-12">
|
<div class="col-md-12">
|
||||||
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Sign in" %}</button>
|
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Sign in" %}</button>
|
||||||
|
@ -238,18 +238,10 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% if SHOW_DISCOURSE %}
|
{% if SHOW_DISCOURSE %}
|
||||||
<td class="text-center">Discourse</td>
|
<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"><a href="{{ DISCOURSE_URL }}">{{ DISCOURSE_URL }}</a></td>
|
||||||
<td class="text-center">
|
<td class="text-center">
|
||||||
{% ifequal authinfo.discourse_username "" %}
|
<a class="btn btn-success" href="{{ DISCOURSE_URL }}"><span class="glyphicon glyphicon-arrow-right"></span></a>
|
||||||
<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 %}
|
|
||||||
</td>
|
</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if SHOW_TEAMSPEAK3 %}
|
{% if SHOW_TEAMSPEAK3 %}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user