Merge branch 'master' of https://github.com/Adarnof/allianceauth into sso_registration

# Conflicts:
#	alliance_auth/__init__.py
#	corputils/models.py
#	corputils/views.py
#	eveonline/tasks.py
#	fleetactivitytracking/views.py
#	hrapplications/admin.py
#	requirements.txt
This commit is contained in:
Adarnof 2017-09-17 01:36:05 -04:00
commit 02f2968ee5
36 changed files with 386 additions and 299 deletions

View File

@ -1,3 +1,4 @@
# -*- coding: UTF-8 -*-
""" """
Django settings for alliance_auth project. Django settings for alliance_auth project.
@ -195,10 +196,10 @@ MESSAGE_TAGS = {
CACHES = { CACHES = {
"default": { "default": {
"BACKEND": "django_redis.cache.RedisCache", "BACKEND": "redis_cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1", "LOCATION": "localhost:6379",
"OPTIONS": { "OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient", "DB": 1,
} }
} }
} }

View File

@ -9,6 +9,10 @@ from authentication.models import CharacterOwnership, UserProfile
from bravado.exception import HTTPForbidden from bravado.exception import HTTPForbidden
from corputils.managers import CorpStatsManager from corputils.managers import CorpStatsManager
import logging import logging
import os
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -35,7 +39,7 @@ class CorpStats(models.Model):
def update(self): def update(self):
try: try:
c = self.token.get_esi_client(Character='v4', Corporation='v2') c = self.token.get_esi_client(spec_file=SWAGGER_SPEC_PATH)
assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()[ assert c.Character.get_characters_character_id(character_id=self.token.character_id).result()[
'corporation_id'] == int(self.corp.corporation_id) 'corporation_id'] == int(self.corp.corporation_id)
members = c.Corporation.get_corporations_corporation_id_members( members = c.Corporation.get_corporations_corporation_id_members(
@ -46,7 +50,6 @@ class CorpStats(models.Model):
# the swagger spec doesn't have a maxItems count # the swagger spec doesn't have a maxItems count
# manual testing says we can do over 350, but let's not risk it # manual testing says we can do over 350, but let's not risk it
member_id_chunks = [member_ids[i:i + 255] for i in range(0, len(member_ids), 255)] member_id_chunks = [member_ids[i:i + 255] for i in range(0, len(member_ids), 255)]
c = self.token.get_esi_client(Character='v1') # ccplease bump versions of whole resources
member_name_chunks = [c.Character.get_characters_names(character_ids=id_chunk).result() for id_chunk in member_name_chunks = [c.Character.get_characters_names(character_ids=id_chunk).result() for id_chunk in
member_id_chunks] member_id_chunks]
member_list = {} member_list = {}

1
corputils/swagger.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -10,6 +10,9 @@ from eveonline.managers import EveManager
from corputils.models import CorpStats from corputils.models import CorpStats
from esi.decorators import token_required from esi.decorators import token_required
from bravado.exception import HTTPError from bravado.exception import HTTPError
import os
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
def access_corpstats_test(user): def access_corpstats_test(user):
@ -27,7 +30,7 @@ def corpstats_add(request, token):
corp_id = EveCharacter.objects.get(character_id=token.character_id).corporation_id corp_id = EveCharacter.objects.get(character_id=token.character_id).corporation_id
else: else:
corp_id = \ corp_id = \
token.get_esi_client(Character='v4').Character.get_characters_character_id( token.get_esi_client(spec_file=SWAGGER_SPEC_PATH).Character.get_characters_character_id(
character_id=token.character_id).result()['corporation_id'] character_id=token.character_id).result()['corporation_id']
try: try:
corp = EveCorporationInfo.objects.get(corporation_id=corp_id) corp = EveCorporationInfo.objects.get(corporation_id=corp_id)

View File

@ -22,7 +22,7 @@ CentOS:
Auth provides example config files for the celery workers, the periodic task scheduler (celery beat), and the mumble authenticator. All of these are available in `thirdparty/Supervisor`. Auth provides example config files for the celery workers, the periodic task scheduler (celery beat), and the mumble authenticator. All of these are available in `thirdparty/Supervisor`.
For most users, all you have to do is copy the config files to `/etc/supervisor/conf.d` then restart the service. Copy `auth-celerybeat.conf` and `auth-celeryd.conf` for the celery workers, and `auth-mumble.conf` for the mumble authenticator. For all three just use a wildcard: For most users, all you have to do is copy the config files to `/etc/supervisor/conf.d` then restart the service. Copy `auth.conf` for the celery workers, and `auth-mumble.conf` for the mumble authenticator. For all three just use a wildcard:
sudo cp thirdparty/Supervisor/* /etc/supervisor/conf.d sudo cp thirdparty/Supervisor/* /etc/supervisor/conf.d
@ -44,6 +44,24 @@ Processes will be `STARTING`, `RUNNING`, or `ERROR`. If an error has occurred, c
- celery workers: `log/worker.log` - celery workers: `log/worker.log`
- celery beat: `log/beat.log` - celery beat: `log/beat.log`
- authenticator: `log/authenticator.log` - authenticator: `log/authenticator.log`
## Restarting Processes
To restart the celery group:
sudo supervisorctl restart auth:*
To restart just celerybeat:
sudo supervisorctl restart auth:celerybeat
To restart just celeryd:
sudo supervisorctl restart auth:celeryd
To restart just mumble authenticator:
sudo supervisorctl restart auth-mumble
## Customizing Config Files ## Customizing Config Files

View File

@ -6,6 +6,9 @@ import json
from bravado.exception import HTTPNotFound, HTTPUnprocessableEntity from bravado.exception import HTTPNotFound, HTTPUnprocessableEntity
import evelink import evelink
import logging import logging
import os
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -228,7 +231,7 @@ class EveProvider(object):
@python_2_unicode_compatible @python_2_unicode_compatible
class EveSwaggerProvider(EveProvider): class EveSwaggerProvider(EveProvider):
def __init__(self, token=None, adapter=None): def __init__(self, token=None, adapter=None):
self.client = esi_client_factory(token=token, Alliance='v1', Character='v4', Corporation='v2', Universe='v2') self.client = esi_client_factory(token=token, spec_file=SWAGGER_SPEC_PATH)
self.adapter = adapter or self self.adapter = adapter or self
def __str__(self): def __str__(self):
@ -244,7 +247,7 @@ class EveSwaggerProvider(EveProvider):
data['alliance_name'], data['alliance_name'],
data['ticker'], data['ticker'],
corps, corps,
data['executor_corporation_id'], data['executor_corp'],
) )
return model return model
except HTTPNotFound: except HTTPNotFound:

1
eveonline/swagger.json Normal file

File diff suppressed because one or more lines are too long

View File

@ -1 +0,0 @@
# Create your tests here.

File diff suppressed because one or more lines are too long

View File

@ -2,6 +2,8 @@
{% load i18n %} {% load i18n %}
{% block title %}Fleet Participation{% endblock %} {% block title %}Fleet Participation{% endblock %}
{% block page_title %}{% trans "Fleet Participation" %}{% endblock %} {% block page_title %}{% trans "Fleet Participation" %}{% endblock %}
{% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% trans "Character not found!" %}</h1> <h1 class="page-header text-center">{% trans "Character not found!" %}</h1>
<div class="col-lg-12 container" id="example"> <div class="col-lg-12 container" id="example">

View File

@ -37,7 +37,7 @@
<td class="text-center">{{ fat.user }}</td> <td class="text-center">{{ fat.user }}</td>
<td class="text-center">{{ fat.character.character_name }}</td> <td class="text-center">{{ fat.character.character_name }}</td>
{% if fat.station != "No Station" %} {% if fat.station != "No Station" %}
<td class="text-center">{% blocktrans %}Docked in {{ fat.system }}{% endblocktrans %}</td> <td class="text-center">{% blocktrans %}Docked in {% endblocktrans %}{{ fat.system }}</td>
{% else %} {% else %}
<td class="text-center">{{ fat.system }}</td> <td class="text-center">{{ fat.system }}</td>
{% endif %} {% endif %}

View File

@ -17,14 +17,14 @@ from fleetactivitytracking.models import Fatlink, Fat
from authentication.models import CharacterOwnership from authentication.models import CharacterOwnership
from django.contrib.auth.models import User from django.contrib.auth.models import User
from esi.decorators import token_required from esi.decorators import token_required
from slugify import slugify from slugify import slugify
import string import string
import random import random
import datetime import datetime
import logging import logging
import os
SWAGGER_SPEC_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'swagger.json')
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -251,7 +251,7 @@ def click_fatlink_view(request, token, hash, fatname):
if character: if character:
# get data # get data
c = token.get_esi_client(Location='v1', Universe='v2') c = token.get_esi_client(spec_file=SWAGGER_SPEC_PATH)
location = c.Location.get_characters_character_id_location(character_id=token.character_id).result() location = c.Location.get_characters_character_id_location(character_id=token.character_id).result()
ship = c.Location.get_characters_character_id_ship(character_id=token.character_id).result() ship = c.Location.get_characters_character_id_ship(character_id=token.character_id).result()
location['solar_system_name'] = \ location['solar_system_name'] = \
@ -261,7 +261,6 @@ def click_fatlink_view(request, token, hash, fatname):
location['station_name'] = \ location['station_name'] = \
c.Universe.get_universe_stations_station_id(station_id=location['station_id']).result()['name'] c.Universe.get_universe_stations_station_id(station_id=location['station_id']).result()['name']
elif location['structure_id']: elif location['structure_id']:
c = token.get_esi_client(Universe='v1')
location['station_name'] = \ location['station_name'] = \
c.Universe.get_universe_structures_structure_id(structure_id=location['structure_id']).result()[ c.Universe.get_universe_structures_structure_id(structure_id=location['structure_id']).result()[
'name'] 'name']

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from django.conf import settings from django.conf import settings
from django.core.cache import cache from django.core.cache import cache
from django.utils import timezone
from datetime import datetime from datetime import datetime
import logging import logging
@ -17,6 +18,8 @@ class FleetUpManager:
GROUP_ID = settings.FLEETUP_GROUP_ID GROUP_ID = settings.FLEETUP_GROUP_ID
BASE_URL = "http://api.fleet-up.com/Api.svc/{}/{}/{}".format(APP_KEY, USER_ID, API_ID) BASE_URL = "http://api.fleet-up.com/Api.svc/{}/{}/{}".format(APP_KEY, USER_ID, API_ID)
TZ = timezone.utc
def __init__(self): def __init__(self):
pass pass
@ -60,7 +63,7 @@ class FleetUpManager:
cache.set(cache_key, json, cls._cache_until_seconds(json['CachedUntilUTC'])) cache.set(cache_key, json, cls._cache_until_seconds(json['CachedUntilUTC']))
return json return json
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
logger.warn("Can't connect to Fleet-Up API, is it offline?!") logger.warning("Can't connect to Fleet-Up API, is it offline?!")
except requests.HTTPError: except requests.HTTPError:
logger.exception("Error accessing Fleetup API") logger.exception("Error accessing Fleetup API")
return None return None
@ -87,8 +90,10 @@ class FleetUpManager:
if foperations is None: if foperations is None:
return None return None
return {row["StartString"]: {"subject": row["Subject"], return {row["StartString"]: {"subject": row["Subject"],
"start": datetime.strptime(row["StartString"], "%Y-%m-%d %H:%M:%S"), "start": timezone.make_aware(
"end": datetime.strptime(row["EndString"], "%Y-%m-%d %H:%M:%S"), datetime.strptime(row["StartString"], "%Y-%m-%d %H:%M:%S"), cls.TZ),
"end": timezone.make_aware(
datetime.strptime(row["EndString"], "%Y-%m-%d %H:%M:%S"), cls.TZ),
"operation_id": row["OperationId"], "operation_id": row["OperationId"],
"location": row["Location"], "location": row["Location"],
"location_info": row["LocationInfo"], "location_info": row["LocationInfo"],
@ -109,9 +114,9 @@ class FleetUpManager:
"owner": row["Owner"], "owner": row["Owner"],
"type": row["Type"], "type": row["Type"],
"timer_type": row["TimerType"], "timer_type": row["TimerType"],
"expires": (datetime.strptime(row["ExpiresString"], "%Y-%m-%d %H:%M:%S")), "expires": timezone.make_aware(
datetime.strptime(row["ExpiresString"], "%Y-%m-%d %H:%M:%S"), cls.TZ),
"notes": row["Notes"]} for row in ftimers["Data"]} "notes": row["Notes"]} for row in ftimers["Data"]}
return {}
@classmethod @classmethod
def get_fleetup_doctrines(cls): def get_fleetup_doctrines(cls):
@ -143,9 +148,10 @@ class FleetUpManager:
"estimated": row["EstPrice"], "estimated": row["EstPrice"],
"faction": row["Faction"], "faction": row["Faction"],
"categories": row["Categories"], "categories": row["Categories"],
"last_update": ( "last_update":
datetime.strptime(row["LastUpdatedString"], "%Y-%m-%d %H:%M:%S"))} for row in timezone.make_aware(
ffittings["Data"]} datetime.strptime(row["LastUpdatedString"], "%Y-%m-%d %H:%M:%S"), cls.TZ)}
for row in ffittings["Data"]}
@classmethod @classmethod
def get_fleetup_fitting(cls, fittingnumber): def get_fleetup_fitting(cls, fittingnumber):
@ -156,7 +162,7 @@ class FleetUpManager:
return None return None
return {"fitting_data": ffitting["Data"]} return {"fitting_data": ffitting["Data"]}
except KeyError: except KeyError:
logger.warn("Failed to retrieve fleetup fitting number %s" % fittingnumber) logger.warning("Failed to retrieve fleetup fitting number %s" % fittingnumber)
return {"fitting_data": {}} return {"fitting_data": {}}
@classmethod @classmethod
@ -180,5 +186,5 @@ class FleetUpManager:
return None return None
return {"fitting_eft": ffittingeft["Data"]["FittingData"]} return {"fitting_eft": ffittingeft["Data"]["FittingData"]}
except KeyError: except KeyError:
logger.warn("Fleetup fitting eft not found for fitting number %s" % fittingnumber) logger.warning("Fleetup fitting eft not found for fitting number %s" % fittingnumber)
return {"fitting_eft": {}} return {"fitting_eft": {}}

View File

@ -12,6 +12,7 @@ import json
import datetime import datetime
from django.test import TestCase from django.test import TestCase
from django.utils.timezone import make_aware, utc
from fleetup.managers import FleetUpManager from fleetup.managers import FleetUpManager
@ -148,8 +149,8 @@ class FleetupManagerTestCase(TestCase):
expected_result = { expected_result = {
'2017-05-06 11:11:11': { '2017-05-06 11:11:11': {
'subject': 'test_operation', 'subject': 'test_operation',
'start': datetime.datetime(2017, 5, 6, 11, 11, 11), 'start': make_aware(datetime.datetime(2017, 5, 6, 11, 11, 11), utc),
'end': datetime.datetime(2017, 5, 6, 12, 12, 12), 'end': make_aware(datetime.datetime(2017, 5, 6, 12, 12, 12), utc),
'operation_id': 1234, 'operation_id': 1234,
'location': 'Jita', 'location': 'Jita',
'location_info': '4-4', 'location_info': '4-4',
@ -208,7 +209,7 @@ class FleetupManagerTestCase(TestCase):
FleetUpManager.GROUP_ID) FleetUpManager.GROUP_ID)
expected_result = { expected_result = {
'2017-05-06 11:11:11': { '2017-05-06 11:11:11': {
'expires': datetime.datetime(2017, 5, 6, 11, 11, 11), 'expires': make_aware(datetime.datetime(2017, 5, 6, 11, 11, 11), utc),
'solarsystem': 'Jita', 'solarsystem': 'Jita',
'planet': '4', 'planet': '4',
'moon': '4', 'moon': '4',
@ -361,7 +362,7 @@ class FleetupManagerTestCase(TestCase):
'estimated': 500000000, 'estimated': 500000000,
'faction': 'Amarr', 'faction': 'Amarr',
'categories': ["Armor", "Laser"], 'categories': ["Armor", "Laser"],
'last_update': datetime.datetime(2017, 5, 6, 11, 11, 11) 'last_update': make_aware(datetime.datetime(2017, 5, 6, 11, 11, 11), utc)
} }
} }
self.assertDictEqual(expected_result, result) self.assertDictEqual(expected_result, result)

View File

@ -6,9 +6,22 @@ from hrapplications.models import ApplicationQuestion
from hrapplications.models import ApplicationForm from hrapplications.models import ApplicationForm
from hrapplications.models import ApplicationResponse from hrapplications.models import ApplicationResponse
from hrapplications.models import ApplicationComment from hrapplications.models import ApplicationComment
from hrapplications.models import ApplicationChoice
class ChoiceInline(admin.TabularInline):
model = ApplicationChoice
extra = 0
verbose_name_plural = 'Choices (optional)'
verbose_name= 'Choice'
class QuestionAdmin(admin.ModelAdmin):
fieldsets = [
(None, {'fields': ['title', 'help_text']}),
]
inlines = [ChoiceInline]
admin.site.register(Application) admin.site.register(Application)
admin.site.register(ApplicationComment) admin.site.register(ApplicationComment)
admin.site.register(ApplicationQuestion) admin.site.register(ApplicationQuestion, QuestionAdmin)
admin.site.register(ApplicationForm)
admin.site.register(ApplicationResponse) admin.site.register(ApplicationResponse)
admin.site.register(ApplicationForm)

View File

@ -0,0 +1,33 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.4 on 2017-08-23 19:46
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('hrapplications', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='ApplicationChoice',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('choice_text', models.CharField(max_length=200, verbose_name='Choice')),
],
),
migrations.AlterField(
model_name='applicationquestion',
name='title',
field=models.CharField(max_length=254, verbose_name='Question'),
),
migrations.AddField(
model_name='applicationchoice',
name='question',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='choices', to='hrapplications.ApplicationQuestion'),
),
]

View File

@ -9,13 +9,21 @@ from eveonline.models import EveCorporationInfo
@python_2_unicode_compatible @python_2_unicode_compatible
class ApplicationQuestion(models.Model): class ApplicationQuestion(models.Model):
title = models.CharField(max_length=254) title = models.CharField(max_length=254, verbose_name='Question')
help_text = models.CharField(max_length=254, blank=True, null=True) help_text = models.CharField(max_length=254, blank=True, null=True)
def __str__(self): def __str__(self):
return "Question: " + self.title return "Question: " + self.title
@python_2_unicode_compatible
class ApplicationChoice(models.Model):
question = models.ForeignKey(ApplicationQuestion,on_delete=models.CASCADE,related_name="choices")
choice_text = models.CharField(max_length=200, verbose_name='Choice')
def __str__(self):
return self.choice_text
@python_2_unicode_compatible @python_2_unicode_compatible
class ApplicationForm(models.Model): class ApplicationForm(models.Model):
questions = SortedManyToManyField(ApplicationQuestion) questions = SortedManyToManyField(ApplicationQuestion)

View File

@ -5,29 +5,33 @@
{% block title %}Apply To {{ corp.corporation_name }}{% endblock title %} {% block title %}Apply To {{ corp.corporation_name }}{% endblock title %}
{% block page_title %}{% trans "Apply To" %} {{ corp.corporation_name }}{% endblock page_title %} {% block page_title %}{% trans "Apply To" %} {{ corp.corporation_name }}{% endblock page_title %}
{% block content %} {% block content %}
<div class="col-lg-12"> <div class="col-lg-12">
<h1 class="page-header text-center">{% trans "Apply To" %} {{ corp.corporation_name }}</h1> <h1 class="page-header text-center">{% trans "Apply To" %} {{ corp.corporation_name }}</h1>
<div class="container-fluid"> <div class="container-fluid">
<div class="col-md-4 col-md-offset-4"> <div class="col-md-4 col-md-offset-4">
<div class="row"> <div class="row">
<form class="form-signin"> <form class="form-signin">
{% csrf_token %} {% csrf_token %}
{% for question in questions %} {% for question in questions %}
<div class="form-group"> <div class="form-group">
<label class="control-label" for="id_{{ question.pk }}">{{ question.title }}</label> <label class="control-label" for="id_{{ question.pk }}">{{ question.title }}</label>
<div class=" "> <div class=" ">
{% if question.help_text %} {% if question.help_text %}
<div cass="text-center">{{ question.help_text }}</div> <div cass="text-center">{{ question.help_text }}</div>
{% endif %} {% endif %}
<textarea class="form-control" cols="40" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="10"></textarea> {% for choice in question.choices.all %}
</div> <input type="radio" name="{{ question.pk }}" id="id_{{ question.pk }}" value="{{ choice.choice_text }}" />
</div> <label for="choice{{ forloop.counter }}">{{ choice.choice_text }}</label><br />
{% endfor %} {% empty %}
<button class="btn btn-lg btn-primary btn-block" type="submit" formmethod="post">{% trans "Submit" %}</button> <textarea class="form-control" cols="30" id="id_{{ question.pk }}" name="{{ question.pk }}" rows="4"></textarea>
</form> {% endfor %}
</div> </div>
</div>
{% endfor %}
<button class="btn btn-lg btn-primary btn-block" type="submit" formmethod="post">{% trans "Submit" %}</button>
</form>
</div> </div>
</div> </div>
</div> </div>
</div>
{% endblock %} {% endblock %}

View File

@ -15,15 +15,11 @@ celery>=4.0.2
django>=1.10,<2.0 django>=1.10,<2.0
django-bootstrap-form django-bootstrap-form
django-bootstrap-pagination django-bootstrap-pagination
django-redis>=4.4
django-registration django-registration
django-sortedm2m django-sortedm2m
django-redis-cache>=1.7.1
django-recaptcha
django-celery-beat django-celery-beat
git+https://github.com/adarnof/django-navhelper
# awating release for fix to celery/django-celery#447
# django-celery
git+https://github.com/celery/django-celery
# awating pyghassen/openfire-restapi #1 to fix installation issues # awating pyghassen/openfire-restapi #1 to fix installation issues
git+https://github.com/adarnof/openfire-restapi git+https://github.com/adarnof/openfire-restapi

View File

@ -1,5 +0,0 @@
from __future__ import unicode_literals
from django.contrib import admin
from services.models import GroupCache
admin.site.register(GroupCache)

View File

@ -0,0 +1,18 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-09-02 06:07
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('services', '0002_auto_20161016_0135'),
]
operations = [
migrations.DeleteModel(
name='GroupCache',
),
]

View File

@ -1,18 +0,0 @@
from __future__ import unicode_literals
from django.utils.encoding import python_2_unicode_compatible
from django.db import models
@python_2_unicode_compatible
class GroupCache(models.Model):
SERVICE_CHOICES = (
("discourse", "discourse"),
("discord", "discord"),
)
created = models.DateTimeField(auto_now_add=True)
groups = models.TextField(default={})
service = models.CharField(max_length=254, choices=SERVICE_CHOICES, unique=True)
def __str__(self):
return self.service

View File

@ -3,14 +3,13 @@ import requests
import json import json
import re import re
from django.conf import settings from django.conf import settings
from services.models import GroupCache
from requests_oauthlib import OAuth2Session from requests_oauthlib import OAuth2Session
from functools import wraps from functools import wraps
import logging import logging
import datetime import datetime
import time import time
from django.utils import timezone
from django.core.cache import cache from django.core.cache import cache
from hashlib import md5
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -20,9 +19,13 @@ EVE_IMAGE_SERVER = "https://image.eveonline.com"
AUTH_URL = "https://discordapp.com/api/oauth2/authorize" AUTH_URL = "https://discordapp.com/api/oauth2/authorize"
TOKEN_URL = "https://discordapp.com/api/oauth2/token" TOKEN_URL = "https://discordapp.com/api/oauth2/token"
# needs administrator, since Discord can't get their permissions system to work """
# was kick members, manage roles, manage nicknames Previously all we asked for was permission to kick members, manage roles, and manage nicknames.
#BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000 Users have reported weird unauthorized errors we don't understand. So now we ask for full server admin.
It's almost fixed the problem.
"""
# kick members, manage roles, manage nicknames
# BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000
BOT_PERMISSIONS = 0x00000008 BOT_PERMISSIONS = 0x00000008
# get user ID, accept invite # get user ID, accept invite
@ -31,7 +34,7 @@ SCOPES = [
'guilds.join', 'guilds.join',
] ]
GROUP_CACHE_MAX_AGE = datetime.timedelta(minutes=30) GROUP_CACHE_MAX_AGE = int(getattr(settings, 'DISCORD_GROUP_CACHE_MAX_AGE', 2 * 60 * 60)) # 2 hours default
class DiscordApiException(Exception): class DiscordApiException(Exception):
@ -110,16 +113,16 @@ def api_backoff(func):
break break
except requests.HTTPError as e: except requests.HTTPError as e:
if e.response.status_code == 429: if e.response.status_code == 429:
if 'Retry-After' in e.response.headers: try:
retry_after = e.response.headers['Retry-After'] retry_after = int(e.response.headers['Retry-After'])
else: except (TypeError, KeyError):
# Pick some random time # Pick some random time
retry_after = 5 retry_after = 5
logger.info("Received backoff from API of %s seconds, handling" % retry_after) logger.info("Received backoff from API of %s seconds, handling" % retry_after)
# Store value in redis # Store value in redis
backoff_until = (datetime.datetime.utcnow() + backoff_until = (datetime.datetime.utcnow() +
datetime.timedelta(seconds=int(retry_after))) datetime.timedelta(seconds=retry_after))
global_backoff = bool(e.response.headers.get('X-RateLimit-Global', False)) global_backoff = bool(e.response.headers.get('X-RateLimit-Global', False))
if global_backoff: if global_backoff:
logger.info("Global backoff!!") logger.info("Global backoff!!")
@ -150,10 +153,14 @@ class DiscordOAuthManager:
def __init__(self): def __init__(self):
pass pass
@staticmethod
def _sanitize_name(name):
return re.sub('[^\w.-]', '', name)[:32]
@staticmethod @staticmethod
def _sanitize_groupname(name): def _sanitize_groupname(name):
name = name.strip(' _') name = name.strip(' _')
return re.sub('[^\w.-]', '', name) return DiscordOAuthManager._sanitize_name(name)
@staticmethod @staticmethod
def generate_bot_add_url(): def generate_bot_add_url():
@ -198,8 +205,9 @@ class DiscordOAuthManager:
@staticmethod @staticmethod
def update_nickname(user_id, nickname): def update_nickname(user_id, nickname):
try: try:
nickname = DiscordOAuthManager._sanitize_name(nickname)
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
data = {'nick': nickname, } data = {'nick': nickname}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id) path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
r = requests.patch(path, headers=custom_headers, json=data) r = requests.patch(path, headers=custom_headers, json=data)
logger.debug("Got status code %s after setting nickname for Discord user ID %s (%s)" % ( logger.debug("Got status code %s after setting nickname for Discord user ID %s (%s)" % (
@ -230,7 +238,7 @@ class DiscordOAuthManager:
return False return False
@staticmethod @staticmethod
def __get_groups(): def _get_groups():
custom_headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} custom_headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles" path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles"
r = requests.get(path, headers=custom_headers) r = requests.get(path, headers=custom_headers)
@ -239,41 +247,20 @@ class DiscordOAuthManager:
return r.json() return r.json()
@staticmethod @staticmethod
def __update_group_cache(): def _generate_cache_role_key(name):
GroupCache.objects.filter(service="discord").delete() return 'DISCORD_ROLE_NAME__%s' % md5(str(name).encode('utf-8')).hexdigest()
cache = GroupCache.objects.create(service="discord")
cache.groups = json.dumps(DiscordOAuthManager.__get_groups())
cache.save()
return cache
@staticmethod @staticmethod
def __get_group_cache(): def _group_name_to_id(name):
if not GroupCache.objects.filter(service="discord").exists(): name = DiscordOAuthManager._sanitize_groupname(name)
DiscordOAuthManager.__update_group_cache()
cache = GroupCache.objects.get(service="discord")
age = timezone.now() - cache.created
if age > GROUP_CACHE_MAX_AGE:
logger.debug("Group cache has expired. Triggering update.")
cache = DiscordOAuthManager.__update_group_cache()
return json.loads(cache.groups)
@staticmethod def get_or_make_role():
def __group_name_to_id(name): groups = DiscordOAuthManager._get_groups()
cache = DiscordOAuthManager.__get_group_cache() for g in groups:
for g in cache: if g['name'] == name:
if g['name'] == name: return g['id']
return g['id'] return DiscordOAuthManager._create_group(name)['id']
logger.debug("Group %s not found on Discord. Creating" % name) return cache.get_or_set(DiscordOAuthManager._generate_cache_role_key(name), get_or_make_role, GROUP_CACHE_MAX_AGE)
DiscordOAuthManager.__create_group(name)
return DiscordOAuthManager.__group_name_to_id(name)
@staticmethod
def __group_id_to_name(id):
cache = DiscordOAuthManager.__get_group_cache()
for g in cache:
if g['id'] == id:
return g['name']
raise KeyError("Group ID %s not found on Discord" % id)
@staticmethod @staticmethod
def __generate_role(): def __generate_role():
@ -300,16 +287,15 @@ class DiscordOAuthManager:
return r.json() return r.json()
@staticmethod @staticmethod
def __create_group(name): def _create_group(name):
role = DiscordOAuthManager.__generate_role() role = DiscordOAuthManager.__generate_role()
DiscordOAuthManager.__edit_role(role['id'], name) return DiscordOAuthManager.__edit_role(role['id'], name)
DiscordOAuthManager.__update_group_cache()
@staticmethod @staticmethod
@api_backoff @api_backoff
def update_groups(user_id, groups): def update_groups(user_id, groups):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN} custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
group_ids = [DiscordOAuthManager.__group_name_to_id(DiscordOAuthManager._sanitize_groupname(g)) for g in groups] group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_groupname(g)) for g in groups]
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id) path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
data = {'roles': group_ids} data = {'roles': group_ids}
r = requests.patch(path, headers=custom_headers, json=data) r = requests.patch(path, headers=custom_headers, json=data)

View File

@ -9,7 +9,6 @@ from django.core.exceptions import ObjectDoesNotExist
from notifications import notify from notifications import notify
from services.modules.discord.manager import DiscordOAuthManager, DiscordApiBackoff from services.modules.discord.manager import DiscordOAuthManager, DiscordApiBackoff
from services.tasks import only_one
from .models import DiscordUser from .models import DiscordUser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)

View File

@ -346,7 +346,7 @@ class DiscordManagerTestCase(TestCase):
# Assert # Assert
self.assertTrue(result) self.assertTrue(result)
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._DiscordOAuthManager__get_group_cache') @mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups')
@requests_mock.Mocker() @requests_mock.Mocker()
def test_update_groups(self, group_cache, m): def test_update_groups(self, group_cache, m):
from . import manager from . import manager
@ -380,7 +380,7 @@ class DiscordManagerTestCase(TestCase):
self.assertNotIn(444, history['roles'], 'The group id 444 must NOT be added to the request') self.assertNotIn(444, history['roles'], 'The group id 444 must NOT be added to the request')
@mock.patch(MODULE_PATH + '.manager.cache') @mock.patch(MODULE_PATH + '.manager.cache')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._DiscordOAuthManager__get_group_cache') @mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups')
@requests_mock.Mocker() @requests_mock.Mocker()
def test_update_groups_backoff(self, group_cache, djcache, m): def test_update_groups_backoff(self, group_cache, djcache, m):
from . import manager from . import manager
@ -415,7 +415,7 @@ class DiscordManagerTestCase(TestCase):
self.assertTrue(datetime.datetime.strptime(args[1], manager.cache_time_format) > datetime.datetime.now()) self.assertTrue(datetime.datetime.strptime(args[1], manager.cache_time_format) > datetime.datetime.now())
@mock.patch(MODULE_PATH + '.manager.cache') @mock.patch(MODULE_PATH + '.manager.cache')
@mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._DiscordOAuthManager__get_group_cache') @mock.patch(MODULE_PATH + '.manager.DiscordOAuthManager._get_groups')
@requests_mock.Mocker() @requests_mock.Mocker()
def test_update_groups_global_backoff(self, group_cache, djcache, m): def test_update_groups_global_backoff(self, group_cache, djcache, m):
from . import manager from . import manager

View File

@ -1,17 +1,15 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import logging import logging
import requests import requests
import random
import string
import datetime
import json
import re import re
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.core.cache import cache
from services.models import GroupCache from hashlib import md5
logger = logging.getLogger(__name__) 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): class DiscourseError(Exception):
def __init__(self, endpoint, errors): def __init__(self, endpoint, errors):
@ -21,12 +19,13 @@ class DiscourseError(Exception):
def __str__(self): def __str__(self):
return "API execution failed.\nErrors: %s\nEndpoint: %s" % (self.errors, self.endpoint) 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': {
'list': { 'list': {
'path': "/admin/groups.json", 'path': "/admin/groups.json",
'method': requests.get, 'method': 'get',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@ -34,7 +33,7 @@ ENDPOINTS = {
}, },
'create': { 'create': {
'path': "/admin/groups", 'path': "/admin/groups",
'method': requests.post, 'method': 'post',
'args': { 'args': {
'required': ['name'], 'required': ['name'],
'optional': ['visible'], 'optional': ['visible'],
@ -42,7 +41,7 @@ ENDPOINTS = {
}, },
'add_user': { 'add_user': {
'path': "/admin/groups/%s/members.json", 'path': "/admin/groups/%s/members.json",
'method': requests.put, 'method': 'put',
'args': { 'args': {
'required': ['usernames'], 'required': ['usernames'],
'optional': [], 'optional': [],
@ -50,7 +49,7 @@ ENDPOINTS = {
}, },
'remove_user': { 'remove_user': {
'path': "/admin/groups/%s/members.json", 'path': "/admin/groups/%s/members.json",
'method': requests.delete, 'method': 'delete',
'args': { 'args': {
'required': ['username'], 'required': ['username'],
'optional': [], 'optional': [],
@ -58,7 +57,7 @@ ENDPOINTS = {
}, },
'delete': { 'delete': {
'path': "/admin/groups/%s.json", 'path': "/admin/groups/%s.json",
'method': requests.delete, 'method': 'delete',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@ -68,7 +67,7 @@ ENDPOINTS = {
'users': { 'users': {
'create': { 'create': {
'path': "/users", 'path': "/users",
'method': requests.post, 'method': 'post',
'args': { 'args': {
'required': ['name', 'email', 'password', 'username'], 'required': ['name', 'email', 'password', 'username'],
'optional': ['active'], 'optional': ['active'],
@ -76,7 +75,7 @@ ENDPOINTS = {
}, },
'update': { 'update': {
'path': "/users/%s.json", 'path': "/users/%s.json",
'method': requests.put, 'method': 'put',
'args': { 'args': {
'required': ['params'], 'required': ['params'],
'optional': [], 'optional': [],
@ -84,7 +83,7 @@ ENDPOINTS = {
}, },
'get': { 'get': {
'path': "/users/%s.json", 'path': "/users/%s.json",
'method': requests.get, 'method': 'get',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@ -92,7 +91,7 @@ ENDPOINTS = {
}, },
'activate': { 'activate': {
'path': "/admin/users/%s/activate", 'path': "/admin/users/%s/activate",
'method': requests.put, 'method': 'put',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@ -100,7 +99,7 @@ ENDPOINTS = {
}, },
'set_email': { 'set_email': {
'path': "/users/%s/preferences/email", 'path': "/users/%s/preferences/email",
'method': requests.put, 'method': 'put',
'args': { 'args': {
'required': ['email'], 'required': ['email'],
'optional': [], 'optional': [],
@ -108,7 +107,7 @@ ENDPOINTS = {
}, },
'suspend': { 'suspend': {
'path': "/admin/users/%s/suspend", 'path': "/admin/users/%s/suspend",
'method': requests.put, 'method': 'put',
'args': { 'args': {
'required': ['duration', 'reason'], 'required': ['duration', 'reason'],
'optional': [], 'optional': [],
@ -116,7 +115,7 @@ ENDPOINTS = {
}, },
'unsuspend': { 'unsuspend': {
'path': "/admin/users/%s/unsuspend", 'path': "/admin/users/%s/unsuspend",
'method': requests.put, 'method': 'put',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@ -124,7 +123,7 @@ ENDPOINTS = {
}, },
'logout': { 'logout': {
'path': "/admin/users/%s/log_out", 'path': "/admin/users/%s/log_out",
'method': requests.post, 'method': 'post',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@ -132,7 +131,7 @@ ENDPOINTS = {
}, },
'external': { 'external': {
'path': "/users/by-external/%s.json", 'path': "/users/by-external/%s.json",
'method': requests.get, 'method': 'get',
'args': { 'args': {
'required': [], 'required': [],
'optional': [], 'optional': [],
@ -146,8 +145,7 @@ class DiscourseManager:
def __init__(self): def __init__(self):
pass pass
GROUP_CACHE_MAX_AGE = datetime.timedelta(minutes=30) REVOKED_EMAIL = 'revoked@localhost'
REVOKED_EMAIL = 'revoked@' + settings.DOMAIN
SUSPEND_DAYS = 99999 SUSPEND_DAYS = 99999
SUSPEND_REASON = "Disabled by auth." SUSPEND_REASON = "Disabled by auth."
@ -171,7 +169,8 @@ class DiscourseManager:
for arg in kwargs: for arg in kwargs:
if arg not in endpoint['args']['required'] and arg not in endpoint['args']['optional'] and not silent: 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 + endpoint['parsed_url'], params=params, json=data) r = getattr(requests, endpoint['method'])(settings.DISCOURSE_URL + endpoint['parsed_url'], params=params,
json=data)
try: try:
if 'errors' in r.json() and not silent: 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']))
@ -190,67 +189,59 @@ class DiscourseManager:
return out return out
@staticmethod @staticmethod
def __generate_random_pass(): def _get_groups():
return ''.join([random.choice(string.ascii_letters + string.digits) for n in range(16)])
@staticmethod
def __get_groups():
endpoint = ENDPOINTS['groups']['list'] endpoint = ENDPOINTS['groups']['list']
data = DiscourseManager.__exc(endpoint) data = DiscourseManager.__exc(endpoint)
return [g for g in data if not g['automatic']] return [g for g in data if not g['automatic']]
@staticmethod @staticmethod
def __update_group_cache(): def _create_group(name):
GroupCache.objects.filter(service="discourse").delete()
cache = GroupCache.objects.create(service="discourse")
cache.groups = json.dumps(DiscourseManager.__get_groups())
cache.save()
return cache
@staticmethod
def __get_group_cache():
if not GroupCache.objects.filter(service="discourse").exists():
DiscourseManager.__update_group_cache()
cache = GroupCache.objects.get(service="discourse")
age = timezone.now() - cache.created
if age > DiscourseManager.GROUP_CACHE_MAX_AGE:
logger.debug("Group cache has expired. Triggering update.")
cache = DiscourseManager.__update_group_cache()
return json.loads(cache.groups)
@staticmethod
def __create_group(name):
endpoint = ENDPOINTS['groups']['create'] endpoint = ENDPOINTS['groups']['create']
DiscourseManager.__exc(endpoint, name=name[:20], visible=True) return DiscourseManager.__exc(endpoint, name=name[:20], visible=True)['basic_group']
DiscourseManager.__update_group_cache()
@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 @staticmethod
def __group_name_to_id(name): def __group_name_to_id(name):
cache = DiscourseManager.__get_group_cache() name = DiscourseManager._sanitize_groupname(name)
for g in cache:
if g['name'] == name[0:20]: def get_or_create_group():
return g['id'] groups = DiscourseManager._get_groups()
logger.debug("Group %s not found on Discourse. Creating" % name) for g in groups:
DiscourseManager.__create_group(name) if g['name'] == name:
return DiscourseManager.__group_name_to_id(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 @staticmethod
def __group_id_to_name(id): def __group_id_to_name(g_id):
cache = DiscourseManager.__get_group_cache() def get_group_name():
for g in cache: groups = DiscourseManager._get_groups()
if g['id'] == id: for g in groups:
return g['name'] if g['id'] == g_id:
raise KeyError("Group ID %s not found on Discourse" % 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 @staticmethod
def __add_user_to_group(id, username): def __add_user_to_group(g_id, username):
endpoint = ENDPOINTS['groups']['add_user'] endpoint = ENDPOINTS['groups']['add_user']
DiscourseManager.__exc(endpoint, id, usernames=[username]) DiscourseManager.__exc(endpoint, g_id, usernames=[username])
@staticmethod @staticmethod
def __remove_user_from_group(id, username): def __remove_user_from_group(g_id, username):
endpoint = ENDPOINTS['groups']['remove_user'] endpoint = ENDPOINTS['groups']['remove_user']
DiscourseManager.__exc(endpoint, id, username=username) DiscourseManager.__exc(endpoint, g_id, username=username)
@staticmethod @staticmethod
def __generate_group_dict(names): def __generate_group_dict(names):
@ -269,10 +260,6 @@ class DiscourseManager:
data = DiscourseManager.__get_user(name, silent=silent) data = DiscourseManager.__get_user(name, silent=silent)
return data['user']['id'] return data['user']['id']
@staticmethod
def __user_id_to_name(id):
raise NotImplementedError
@staticmethod @staticmethod
def __get_user(username, silent=False): def __get_user(username, silent=False):
endpoint = ENDPOINTS['users']['get'] endpoint = ENDPOINTS['users']['get']
@ -281,14 +268,14 @@ class DiscourseManager:
@staticmethod @staticmethod
def __activate_user(username): def __activate_user(username):
endpoint = ENDPOINTS['users']['activate'] endpoint = ENDPOINTS['users']['activate']
id = DiscourseManager.__user_name_to_id(username) u_id = DiscourseManager.__user_name_to_id(username)
DiscourseManager.__exc(endpoint, id) DiscourseManager.__exc(endpoint, u_id)
@staticmethod @staticmethod
def __update_user(username, **kwargs): def __update_user(username, **kwargs):
endpoint = ENDPOINTS['users']['update'] endpoint = ENDPOINTS['users']['update']
id = DiscourseManager.__user_name_to_id(username) u_id = DiscourseManager.__user_name_to_id(username)
DiscourseManager.__exc(endpoint, id, params=kwargs) DiscourseManager.__exc(endpoint, u_id, params=kwargs)
@staticmethod @staticmethod
def __create_user(username, email, password): def __create_user(username, email, password):
@ -300,21 +287,21 @@ class DiscourseManager:
try: try:
DiscourseManager.__user_name_to_id(username, silent=True) DiscourseManager.__user_name_to_id(username, silent=True)
return True return True
except: except DiscourseError:
return False return False
@staticmethod @staticmethod
def __suspend_user(username): def __suspend_user(username):
id = DiscourseManager.__user_name_to_id(username) u_id = DiscourseManager.__user_name_to_id(username)
endpoint = ENDPOINTS['users']['suspend'] endpoint = ENDPOINTS['users']['suspend']
return DiscourseManager.__exc(endpoint, id, duration=DiscourseManager.SUSPEND_DAYS, return DiscourseManager.__exc(endpoint, u_id, duration=DiscourseManager.SUSPEND_DAYS,
reason=DiscourseManager.SUSPEND_REASON) reason=DiscourseManager.SUSPEND_REASON)
@staticmethod @staticmethod
def __unsuspend(username): def __unsuspend(username):
id = DiscourseManager.__user_name_to_id(username) u_id = DiscourseManager.__user_name_to_id(username)
endpoint = ENDPOINTS['users']['unsuspend'] endpoint = ENDPOINTS['users']['unsuspend']
return DiscourseManager.__exc(endpoint, id) return DiscourseManager.__exc(endpoint, u_id)
@staticmethod @staticmethod
def __set_email(username, email): def __set_email(username, email):
@ -322,47 +309,53 @@ class DiscourseManager:
return DiscourseManager.__exc(endpoint, username, email=email) return DiscourseManager.__exc(endpoint, username, email=email)
@staticmethod @staticmethod
def __logout(id): def __logout(u_id):
endpoint = ENDPOINTS['users']['logout'] endpoint = ENDPOINTS['users']['logout']
return DiscourseManager.__exc(endpoint, id) return DiscourseManager.__exc(endpoint, u_id)
@staticmethod @staticmethod
def __get_user_by_external(id): def __get_user_by_external(u_id):
endpoint = ENDPOINTS['users']['external'] endpoint = ENDPOINTS['users']['external']
return DiscourseManager.__exc(endpoint, id) return DiscourseManager.__exc(endpoint, u_id)
@staticmethod @staticmethod
def __user_id_by_external_id(id): def __user_id_by_external_id(u_id):
data = DiscourseManager.__get_user_by_external(id) data = DiscourseManager.__get_user_by_external(u_id)
return data['user']['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 @staticmethod
def _sanitize_username(username): def _sanitize_username(username):
sanitized = username.replace(" ", "_") return DiscourseManager._sanitize_name(username)
sanitized = sanitized.strip(' _')
sanitized = sanitized.replace("'", "")
return sanitized
@staticmethod @staticmethod
def _sanitize_groupname(name): def _sanitize_groupname(name):
name = name.strip(' _')
name = re.sub('[^\w]', '', name) name = re.sub('[^\w]', '', name)
name = DiscourseManager._sanitize_name(name)
if len(name) < 3: if len(name) < 3:
name = name + "".join('_' for i in range(3-len(name))) name = "Group " + name
return name[:20] return name
@staticmethod @staticmethod
def update_groups(user): def update_groups(user):
groups = [] groups = []
for g in user.groups.all(): for g in user.groups.all():
groups.append(DiscourseManager._sanitize_groupname(str(g)[:20])) groups.append(DiscourseManager._sanitize_groupname(str(g)))
logger.debug("Updating discourse user %s groups to %s" % (user, 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'] 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 x not in inv_group_dict]
if add_groups or rem_groups: if add_groups or rem_groups:
logger.info( logger.info(
"Updating discourse user %s groups: adding %s, removing %s" % (username, add_groups, rem_groups)) "Updating discourse user %s groups: adding %s, removing %s" % (username, add_groups, rem_groups))

View File

@ -5,8 +5,6 @@ from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from notifications import notify from notifications import notify
from services.tasks import only_one
from .manager import DiscourseManager from .manager import DiscourseManager
from .models import DiscourseUser from .models import DiscourseUser

View File

@ -61,7 +61,7 @@ class OpenfireManager:
@staticmethod @staticmethod
def _sanitize_groupname(name): def _sanitize_groupname(name):
name = name.strip(' _') name = name.strip(' _').lower()
return re.sub('[^\w.-]', '', name) return re.sub('[^\w.-]', '', name)
@staticmethod @staticmethod
@ -120,9 +120,10 @@ class OpenfireManager:
logger.error("Unable to update openfire user %s password - user not found on server." % username) logger.error("Unable to update openfire user %s password - user not found on server." % username)
return "" return ""
@staticmethod @classmethod
def update_user_groups(username, groups): def update_user_groups(cls, username, groups):
logger.debug("Updating openfire user %s groups %s" % (username, groups)) logger.debug("Updating openfire user %s groups %s" % (username, groups))
s_groups = list(map(cls._sanitize_groupname, groups)) # Sanitized group names
api = ofUsers(settings.OPENFIRE_ADDRESS, settings.OPENFIRE_SECRET_KEY) api = ofUsers(settings.OPENFIRE_ADDRESS, settings.OPENFIRE_SECRET_KEY)
response = api.get_user_groups(username) response = api.get_user_groups(username)
remote_groups = [] remote_groups = []
@ -130,16 +131,15 @@ class OpenfireManager:
remote_groups = response['groupname'] remote_groups = response['groupname']
if isinstance(remote_groups, six.string_types): if isinstance(remote_groups, six.string_types):
remote_groups = [remote_groups] remote_groups = [remote_groups]
remote_groups = list(map(cls._sanitize_groupname, remote_groups))
logger.debug("Openfire user %s has groups %s" % (username, remote_groups)) logger.debug("Openfire user %s has groups %s" % (username, remote_groups))
add_groups = [] add_groups = []
del_groups = [] del_groups = []
for g in groups: for g in s_groups:
g = OpenfireManager._sanitize_groupname(g)
if g not in remote_groups: if g not in remote_groups:
add_groups.append(g) add_groups.append(g)
for g in remote_groups: for g in remote_groups:
g = OpenfireManager._sanitize_groupname(g) if g not in s_groups:
if g not in groups:
del_groups.append(g) del_groups.append(g)
logger.info( logger.info(
"Updating openfire groups for user %s - adding %s, removing %s" % (username, add_groups, del_groups)) "Updating openfire groups for user %s - adding %s, removing %s" % (username, add_groups, del_groups))
@ -155,10 +155,11 @@ class OpenfireManager:
api.delete_user_groups(username, groups) api.delete_user_groups(username, groups)
logger.info("Deleted groups %s from openfire user %s" % (groups, username)) logger.info("Deleted groups %s from openfire user %s" % (groups, username))
@staticmethod @classmethod
def send_broadcast_message(group_name, broadcast_message): def send_broadcast_message(cls, group_name, broadcast_message):
logger.debug("Sending jabber ping to group %s with message %s" % (group_name, broadcast_message)) s_group_name = cls._sanitize_groupname(group_name)
to_address = group_name + '@' + settings.BROADCAST_SERVICE_NAME + '.' + settings.JABBER_URL logger.debug("Sending jabber ping to group %s with message %s" % (s_group_name, broadcast_message))
to_address = s_group_name + '@' + settings.BROADCAST_SERVICE_NAME + '.' + settings.JABBER_URL
xmpp = PingBot(settings.BROADCAST_USER, settings.BROADCAST_USER_PASSWORD, to_address, broadcast_message) xmpp = PingBot(settings.BROADCAST_USER, settings.BROADCAST_USER_PASSWORD, to_address, broadcast_message)
xmpp.register_plugin('xep_0030') # Service Discovery xmpp.register_plugin('xep_0030') # Service Discovery
xmpp.register_plugin('xep_0199') # XMPP Ping xmpp.register_plugin('xep_0199') # XMPP Ping

View File

@ -211,3 +211,28 @@ class OpenfireManagerTestCase(TestCase):
result_username = self.manager._OpenfireManager__sanitize_username(test_username) result_username = self.manager._OpenfireManager__sanitize_username(test_username)
self.assertEqual(result_username, 'My_Test\\20User\\22\\27\\26\\2f\\3a\\3c\\3e\\40name\\5c20name') self.assertEqual(result_username, 'My_Test\\20User\\22\\27\\26\\2f\\3a\\3c\\3e\\40name\\5c20name')
def test__sanitize_groupname(self):
test_groupname = " My_Test Groupname"
result_groupname = self.manager._sanitize_groupname(test_groupname)
self.assertEqual(result_groupname, "my_testgroupname")
@mock.patch(MODULE_PATH + '.manager.ofUsers')
def test_update_user_groups(self, api):
groups = ["AddGroup", "othergroup", "Guest Group"]
server_groups = ["othergroup", "Guest Group", "REMOVE group"]
username = "testuser"
api_instance = api.return_value
api_instance.get_user_groups.return_value = {'groupname': server_groups}
self.manager.update_user_groups(username, groups)
self.assertTrue(api_instance.add_user_groups.called)
args, kwargs = api_instance.add_user_groups.call_args
self.assertEqual(args[1], ["addgroup"])
self.assertTrue(api_instance.delete_user_groups.called)
args, kwargs = api_instance.delete_user_groups.call_args
self.assertEqual(args[1], ["removegroup"])

View File

@ -63,7 +63,7 @@ class SeatManager:
logger.info("Added SeAT user with username %s" % sanitized) logger.info("Added SeAT user with username %s" % sanitized)
return sanitized, password return sanitized, password
logger.info("Failed to add SeAT user with username %s" % sanitized) logger.info("Failed to add SeAT user with username %s" % sanitized)
return None return None, None
@classmethod @classmethod
def delete_user(cls, username): def delete_user(cls, username):
@ -75,25 +75,6 @@ class SeatManager:
return username return username
return None return None
@classmethod
def disable_user(cls, username):
""" Disable user """
ret = cls.exec_request('user/{}'.format(username), 'put', active=0)
logger.debug(ret)
ret = cls.exec_request('user/{}'.format(username), 'put', email="")
logger.debug(ret)
if cls._response_ok(ret):
try:
cls.update_roles(username, [])
logger.info("Disabled SeAT user with username %s" % username)
return username
except KeyError:
# if something goes wrong, delete user from seat instead of disabling
if cls.delete_user(username):
return username
logger.info("Failed to disabled SeAT user with username %s" % username)
return None
@classmethod @classmethod
def enable_user(cls, username): def enable_user(cls, username):
""" Enable user """ """ Enable user """
@ -105,14 +86,22 @@ class SeatManager:
logger.info("Failed to enabled SeAT user with username %s" % username) logger.info("Failed to enabled SeAT user with username %s" % username)
return None return None
@classmethod
def _check_email_changed(cls, username, email):
"""Compares email to one set on SeAT"""
ret = cls.exec_request('user/{}'.format(username), 'get', raise_for_status=True)
return ret['email'] != email
@classmethod @classmethod
def update_user(cls, username, email, password): def update_user(cls, username, email, password):
""" Edit user info """ """ Edit user info """
logger.debug("Updating SeAT username %s with email %s and password" % (username, email)) if cls._check_email_changed(username, email):
ret = cls.exec_request('user/{}'.format(username), 'put', email=email) # if we try to set the email to whatever it is already on SeAT, we get a HTTP422 error
logger.debug(ret) logger.debug("Updating SeAT username %s with email %s and password" % (username, email))
if not cls._response_ok(ret): ret = cls.exec_request('user/{}'.format(username), 'put', email=email)
logger.warn("Failed to update email for username {}".format(username)) logger.debug(ret)
if not cls._response_ok(ret):
logger.warn("Failed to update email for username {}".format(username))
ret = cls.exec_request('user/{}'.format(username), 'put', password=password) ret = cls.exec_request('user/{}'.format(username), 'put', password=password)
logger.debug(ret) logger.debug(ret)
if not cls._response_ok(ret): if not cls._response_ok(ret):
@ -275,5 +264,5 @@ class SeatManager:
@staticmethod @staticmethod
def username_hash(username): def username_hash(username):
m = hashlib.sha1() m = hashlib.sha1()
m.update(username) m.update(username.encode('utf-8'))
return m.hexdigest() return m.hexdigest()

View File

@ -28,7 +28,7 @@ class SeatTasks:
@classmethod @classmethod
def delete_user(cls, user, notify_user=False): def delete_user(cls, user, notify_user=False):
if cls.has_account(user) and SeatManager.disable_user(user.seat.username): if cls.has_account(user) and SeatManager.delete_user(user.seat.username):
user.seat.delete() user.seat.delete()
logger.info("Successfully deactivated SeAT for user %s" % user) logger.info("Successfully deactivated SeAT for user %s" % user)
if notify_user: if notify_user:

View File

@ -92,10 +92,10 @@ class SeatHooksTestCase(TestCase):
# Test none user is deleted # Test none user is deleted
none_user = User.objects.get(username=self.none_user) none_user = User.objects.get(username=self.none_user)
manager.disable_user.return_value = 'abc123' manager.delete_user.return_value = 'abc123'
SeatUser.objects.create(user=none_user, username='abc123') SeatUser.objects.create(user=none_user, username='abc123')
service.validate_user(none_user) service.validate_user(none_user)
self.assertTrue(manager.disable_user.called) self.assertTrue(manager.delete_user.called)
with self.assertRaises(ObjectDoesNotExist): with self.assertRaises(ObjectDoesNotExist):
none_seat = User.objects.get(username=self.none_user).seat none_seat = User.objects.get(username=self.none_user).seat
@ -107,7 +107,7 @@ class SeatHooksTestCase(TestCase):
result = service.delete_user(member) result = service.delete_user(member)
self.assertTrue(result) self.assertTrue(result)
self.assertTrue(manager.disable_user.called) self.assertTrue(manager.delete_user.called)
with self.assertRaises(ObjectDoesNotExist): with self.assertRaises(ObjectDoesNotExist):
seat_user = User.objects.get(username=self.member).seat seat_user = User.objects.get(username=self.member).seat
@ -169,7 +169,7 @@ class SeatViewsTestCase(TestCase):
response = self.client.get(urls.reverse('auth_deactivate_seat')) response = self.client.get(urls.reverse('auth_deactivate_seat'))
self.assertTrue(manager.disable_user.called) self.assertTrue(manager.delete_user.called)
self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200) self.assertRedirects(response, expected_url=urls.reverse('auth_services'), target_status_code=200)
with self.assertRaises(ObjectDoesNotExist): with self.assertRaises(ObjectDoesNotExist):
seat_user = User.objects.get(pk=self.member.pk).seat seat_user = User.objects.get(pk=self.member.pk).seat

View File

@ -217,6 +217,10 @@ class Teamspeak3Manager:
logger.debug("Deleting user %s with id %s from TS3 server." % (user, uid)) logger.debug("Deleting user %s with id %s from TS3 server." % (user, uid))
if user: if user:
clients = self.server.send_command('clientlist') clients = self.server.send_command('clientlist')
if isinstance(clients, dict):
# Rewrap list
clients = [clients]
for client in clients: for client in clients:
try: try:
if client['keys']['client_database_id'] == user: if client['keys']['client_database_id'] == user:

View File

@ -1,10 +0,0 @@
[program:auth-celerybeat]
command=celery -A alliance_auth beat
directory=/home/allianceserver/allianceauth
user=allianceserver
stdout_logfile=/home/allianceserver/allianceauth/log/beat.log
stderr_logfile=/home/allianceserver/allianceauth/log/beat.log
autostart=true
autorestart=true
startsecs=10
priority=999

View File

@ -1,13 +0,0 @@
[program:auth-celeryd]
command=celery -A alliance_auth worker
directory=/home/allianceserver/allianceauth
user=allianceserver
numprocs=1
stdout_logfile=/home/allianceserver/allianceauth/log/worker.log
stderr_logfile=/home/allianceserver/allianceauth/log/worker.log
autostart=true
autorestart=true
startsecs=10
stopwaitsecs = 600
killasgroup=true
priority=1000

28
thirdparty/Supervisor/auth.conf vendored Normal file
View File

@ -0,0 +1,28 @@
[program:celerybeat]
command=celery -A alliance_auth beat
directory=/home/allianceserver/allianceauth
user=allianceserver
stdout_logfile=/home/allianceserver/allianceauth/log/beat.log
stderr_logfile=/home/allianceserver/allianceauth/log/beat.log
autostart=true
autorestart=true
startsecs=10
priority=998
[program:celeryd]
command=celery -A alliance_auth worker
directory=/home/allianceserver/allianceauth
user=allianceserver
numprocs=1
stdout_logfile=/home/allianceserver/allianceauth/log/worker.log
stderr_logfile=/home/allianceserver/allianceauth/log/worker.log
autostart=true
autorestart=true
startsecs=10
stopwaitsecs = 600
killasgroup=true
priority=998
[group:auth]
programs=celerybeat,celeryd
priority=999