mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-09 20:40:17 +02:00
Use django-redis-cache backend for locking get_or_set No longer require group-related tasks to be locked to one simultaneous execution. Remove legacy service group cache models. Truncate Discord nicknames to 32 characters Correct Discourse group name extension using only valid leading characters. Prevent name slicing from ending with illegal character Closes #801 Closes #847 Closes #835 Closes #852
374 lines
12 KiB
Python
374 lines
12 KiB
Python
from __future__ import unicode_literals
|
|
import logging
|
|
import requests
|
|
import re
|
|
from django.conf import settings
|
|
from django.core.cache import cache
|
|
from hashlib import md5
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
GROUP_CACHE_MAX_AGE = int(getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60)) # default 2 hours
|
|
|
|
|
|
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': {
|
|
'list': {
|
|
'path': "/admin/groups.json",
|
|
'method': 'get',
|
|
'args': {
|
|
'required': [],
|
|
'optional': [],
|
|
},
|
|
},
|
|
'create': {
|
|
'path': "/admin/groups",
|
|
'method': 'post',
|
|
'args': {
|
|
'required': ['name'],
|
|
'optional': ['visible'],
|
|
}
|
|
},
|
|
'add_user': {
|
|
'path': "/admin/groups/%s/members.json",
|
|
'method': 'put',
|
|
'args': {
|
|
'required': ['usernames'],
|
|
'optional': [],
|
|
},
|
|
},
|
|
'remove_user': {
|
|
'path': "/admin/groups/%s/members.json",
|
|
'method': 'delete',
|
|
'args': {
|
|
'required': ['username'],
|
|
'optional': [],
|
|
},
|
|
},
|
|
'delete': {
|
|
'path': "/admin/groups/%s.json",
|
|
'method': 'delete',
|
|
'args': {
|
|
'required': [],
|
|
'optional': [],
|
|
},
|
|
},
|
|
},
|
|
'users': {
|
|
'create': {
|
|
'path': "/users",
|
|
'method': 'post',
|
|
'args': {
|
|
'required': ['name', 'email', 'password', 'username'],
|
|
'optional': ['active'],
|
|
},
|
|
},
|
|
'update': {
|
|
'path': "/users/%s.json",
|
|
'method': 'put',
|
|
'args': {
|
|
'required': ['params'],
|
|
'optional': [],
|
|
}
|
|
},
|
|
'get': {
|
|
'path': "/users/%s.json",
|
|
'method': 'get',
|
|
'args': {
|
|
'required': [],
|
|
'optional': [],
|
|
},
|
|
},
|
|
'activate': {
|
|
'path': "/admin/users/%s/activate",
|
|
'method': 'put',
|
|
'args': {
|
|
'required': [],
|
|
'optional': [],
|
|
},
|
|
},
|
|
'set_email': {
|
|
'path': "/users/%s/preferences/email",
|
|
'method': 'put',
|
|
'args': {
|
|
'required': ['email'],
|
|
'optional': [],
|
|
},
|
|
},
|
|
'suspend': {
|
|
'path': "/admin/users/%s/suspend",
|
|
'method': 'put',
|
|
'args': {
|
|
'required': ['duration', 'reason'],
|
|
'optional': [],
|
|
},
|
|
},
|
|
'unsuspend': {
|
|
'path': "/admin/users/%s/unsuspend",
|
|
'method': 'put',
|
|
'args': {
|
|
'required': [],
|
|
'optional': [],
|
|
},
|
|
},
|
|
'logout': {
|
|
'path': "/admin/users/%s/log_out",
|
|
'method': 'post',
|
|
'args': {
|
|
'required': [],
|
|
'optional': [],
|
|
},
|
|
},
|
|
'external': {
|
|
'path': "/users/by-external/%s.json",
|
|
'method': 'get',
|
|
'args': {
|
|
'required': [],
|
|
'optional': [],
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
|
|
class DiscourseManager:
|
|
def __init__(self):
|
|
pass
|
|
|
|
REVOKED_EMAIL = 'revoked@localhost'
|
|
SUSPEND_DAYS = 99999
|
|
SUSPEND_REASON = "Disabled by auth."
|
|
|
|
@staticmethod
|
|
def __exc(endpoint, *args, **kwargs):
|
|
params = {
|
|
'api_key': settings.DISCOURSE_API_KEY,
|
|
'api_username': settings.DISCOURSE_API_USERNAME,
|
|
}
|
|
silent = kwargs.pop('silent', False)
|
|
if args:
|
|
endpoint['parsed_url'] = endpoint['path'] % args
|
|
else:
|
|
endpoint['parsed_url'] = endpoint['path']
|
|
data = {}
|
|
for arg in endpoint['args']['required']:
|
|
data[arg] = kwargs[arg]
|
|
for arg in endpoint['args']['optional']:
|
|
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'] and not silent:
|
|
logger.warn("Received unrecognized kwarg %s for endpoint %s" % (arg, endpoint))
|
|
r = getattr(requests, endpoint['method'])(settings.DISCOURSE_URL + endpoint['parsed_url'], params=params,
|
|
json=data)
|
|
try:
|
|
if 'errors' in r.json() and not silent:
|
|
logger.error("Discourse execution failed.\nEndpoint: %s\nErrors: %s" % (endpoint, r.json()['errors']))
|
|
raise DiscourseError(endpoint, r.json()['errors'])
|
|
if 'success' in r.json():
|
|
if not r.json()['success'] and not silent:
|
|
raise DiscourseError(endpoint, None)
|
|
out = r.json()
|
|
except ValueError:
|
|
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
|
|
def _get_groups():
|
|
endpoint = ENDPOINTS['groups']['list']
|
|
data = DiscourseManager.__exc(endpoint)
|
|
return [g for g in data if not g['automatic']]
|
|
|
|
@staticmethod
|
|
def _create_group(name):
|
|
endpoint = ENDPOINTS['groups']['create']
|
|
return DiscourseManager.__exc(endpoint, name=name[:20], visible=True)['basic_group']
|
|
|
|
@staticmethod
|
|
def _generate_cache_group_name_key(name):
|
|
return 'DISCOURSE_GROUP_NAME__%s' % md5(name.encode('utf-8')).hexdigest()
|
|
|
|
@staticmethod
|
|
def _generate_cache_group_id_key(g_id):
|
|
return 'DISCOURSE_GROUP_ID__%s' % g_id
|
|
|
|
@staticmethod
|
|
def __group_name_to_id(name):
|
|
name = DiscourseManager._sanitize_groupname(name)
|
|
|
|
def get_or_create_group():
|
|
groups = DiscourseManager._get_groups()
|
|
for g in groups:
|
|
if g['name'] == name:
|
|
return g['id']
|
|
return DiscourseManager._create_group(name)['id']
|
|
|
|
return cache.get_or_set(DiscourseManager._generate_cache_group_name_key(name), get_or_create_group,
|
|
GROUP_CACHE_MAX_AGE)
|
|
|
|
@staticmethod
|
|
def __group_id_to_name(g_id):
|
|
def get_group_name():
|
|
groups = DiscourseManager._get_groups()
|
|
for g in groups:
|
|
if g['id'] == g_id:
|
|
return g['name']
|
|
raise KeyError("Group ID %s not found on Discourse" % g_id)
|
|
|
|
return cache.get_or_set(DiscourseManager._generate_cache_group_id_key(g_id), get_group_name,
|
|
GROUP_CACHE_MAX_AGE)
|
|
|
|
@staticmethod
|
|
def __add_user_to_group(g_id, username):
|
|
endpoint = ENDPOINTS['groups']['add_user']
|
|
DiscourseManager.__exc(endpoint, g_id, usernames=[username])
|
|
|
|
@staticmethod
|
|
def __remove_user_from_group(g_id, username):
|
|
endpoint = ENDPOINTS['groups']['remove_user']
|
|
DiscourseManager.__exc(endpoint, g_id, username=username)
|
|
|
|
@staticmethod
|
|
def __generate_group_dict(names):
|
|
group_dict = {}
|
|
for name in names:
|
|
group_dict[name] = DiscourseManager.__group_name_to_id(name)
|
|
return group_dict
|
|
|
|
@staticmethod
|
|
def __get_user_groups(username):
|
|
data = DiscourseManager.__get_user(username)
|
|
return [g['id'] for g in data['user']['groups'] if not g['automatic']]
|
|
|
|
@staticmethod
|
|
def __user_name_to_id(name, silent=False):
|
|
data = DiscourseManager.__get_user(name, silent=silent)
|
|
return data['user']['id']
|
|
|
|
@staticmethod
|
|
def __get_user(username, silent=False):
|
|
endpoint = ENDPOINTS['users']['get']
|
|
return DiscourseManager.__exc(endpoint, username, silent=silent)
|
|
|
|
@staticmethod
|
|
def __activate_user(username):
|
|
endpoint = ENDPOINTS['users']['activate']
|
|
u_id = DiscourseManager.__user_name_to_id(username)
|
|
DiscourseManager.__exc(endpoint, u_id)
|
|
|
|
@staticmethod
|
|
def __update_user(username, **kwargs):
|
|
endpoint = ENDPOINTS['users']['update']
|
|
u_id = DiscourseManager.__user_name_to_id(username)
|
|
DiscourseManager.__exc(endpoint, u_id, params=kwargs)
|
|
|
|
@staticmethod
|
|
def __create_user(username, email, password):
|
|
endpoint = ENDPOINTS['users']['create']
|
|
DiscourseManager.__exc(endpoint, name=username, username=username, email=email, password=password, active=True)
|
|
|
|
@staticmethod
|
|
def __check_if_user_exists(username):
|
|
try:
|
|
DiscourseManager.__user_name_to_id(username, silent=True)
|
|
return True
|
|
except DiscourseError:
|
|
return False
|
|
|
|
@staticmethod
|
|
def __suspend_user(username):
|
|
u_id = DiscourseManager.__user_name_to_id(username)
|
|
endpoint = ENDPOINTS['users']['suspend']
|
|
return DiscourseManager.__exc(endpoint, u_id, duration=DiscourseManager.SUSPEND_DAYS,
|
|
reason=DiscourseManager.SUSPEND_REASON)
|
|
|
|
@staticmethod
|
|
def __unsuspend(username):
|
|
u_id = DiscourseManager.__user_name_to_id(username)
|
|
endpoint = ENDPOINTS['users']['unsuspend']
|
|
return DiscourseManager.__exc(endpoint, u_id)
|
|
|
|
@staticmethod
|
|
def __set_email(username, email):
|
|
endpoint = ENDPOINTS['users']['set_email']
|
|
return DiscourseManager.__exc(endpoint, username, email=email)
|
|
|
|
@staticmethod
|
|
def __logout(u_id):
|
|
endpoint = ENDPOINTS['users']['logout']
|
|
return DiscourseManager.__exc(endpoint, u_id)
|
|
|
|
@staticmethod
|
|
def __get_user_by_external(u_id):
|
|
endpoint = ENDPOINTS['users']['external']
|
|
return DiscourseManager.__exc(endpoint, u_id)
|
|
|
|
@staticmethod
|
|
def __user_id_by_external_id(u_id):
|
|
data = DiscourseManager.__get_user_by_external(u_id)
|
|
return data['user']['id']
|
|
|
|
@staticmethod
|
|
def _sanitize_name(name):
|
|
name = name.replace(' ', '_')
|
|
name = name.replace("'", '')
|
|
name = name.lstrip(' _')
|
|
name = name[:20]
|
|
name = name.rstrip(' _')
|
|
return name
|
|
|
|
@staticmethod
|
|
def _sanitize_username(username):
|
|
return DiscourseManager._sanitize_name(username)
|
|
|
|
@staticmethod
|
|
def _sanitize_groupname(name):
|
|
name = re.sub('[^\w]', '', name)
|
|
name = DiscourseManager._sanitize_name(name)
|
|
if len(name) < 3:
|
|
name = "Group " + name
|
|
return name
|
|
|
|
@staticmethod
|
|
def update_groups(user):
|
|
groups = []
|
|
for g in user.groups.all():
|
|
groups.append(DiscourseManager._sanitize_groupname(str(g)))
|
|
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 x not in inv_group_dict]
|
|
if add_groups or rem_groups:
|
|
logger.info(
|
|
"Updating discourse user %s groups: adding %s, removing %s" % (username, add_groups, rem_groups))
|
|
for g in add_groups:
|
|
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'])
|
|
logger.info("Disabled user %s Discourse access." % user)
|
|
return True
|