Merge pull request #2 from allianceauth/master

sync
This commit is contained in:
colcrunch 2018-05-02 20:01:42 -04:00 committed by GitHub
commit c8ad1dcc7a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
85 changed files with 932 additions and 473 deletions

View File

@ -23,6 +23,7 @@ Beta Testers / Bug Fixers:
- [ghoti](https://github.com/ghoti/) - [ghoti](https://github.com/ghoti/)
- [mmolitor87](https://github.com/mmolitor87/) - [mmolitor87](https://github.com/mmolitor87/)
- [TargetZ3R0](https://github.com/TargetZ3R0)
- [kaezon](https://github.com/kaezon/) - [kaezon](https://github.com/kaezon/)
- [orbitroom](https://github.com/orbitroom/) - [orbitroom](https://github.com/orbitroom/)
- [tehfiend](https://github.com/tehfiend/) - [tehfiend](https://github.com/tehfiend/)

View File

@ -1,7 +1,7 @@
# This will make sure the app is always imported when # This will make sure the app is always imported when
# Django starts so that shared_task will use this app. # Django starts so that shared_task will use this app.
__version__ = '2.0b3' __version__ = '2.0.1'
NAME = 'Alliance Auth v%s' % __version__ NAME = 'Alliance Auth v%s' % __version__
default_app_config = 'allianceauth.apps.AllianceAuthConfig' default_app_config = 'allianceauth.apps.AllianceAuthConfig'

View File

@ -6,7 +6,7 @@ from django.db.models import Q
from allianceauth.services.hooks import ServicesHook from allianceauth.services.hooks import ServicesHook
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
from django.dispatch import receiver from django.dispatch import receiver
from allianceauth.authentication.models import State, get_guest_state, CharacterOwnership, UserProfile from allianceauth.authentication.models import State, get_guest_state, CharacterOwnership, UserProfile, OwnershipRecord
from allianceauth.hooks import get_hooks from allianceauth.hooks import get_hooks
from allianceauth.eveonline.models import EveCharacter from allianceauth.eveonline.models import EveCharacter
from django.forms import ModelForm from django.forms import ModelForm
@ -160,12 +160,23 @@ class StateAdmin(admin.ModelAdmin):
return obj.userprofile_set.all().count() return obj.userprofile_set.all().count()
@admin.register(CharacterOwnership) class BaseOwnershipAdmin(admin.ModelAdmin):
class CharacterOwnershipAdmin(admin.ModelAdmin):
list_display = ('user', 'character') list_display = ('user', 'character')
search_fields = ('user__username', 'character__character_name', 'character__corporation_name', 'character__alliance_name') search_fields = ('user__username', 'character__character_name', 'character__corporation_name', 'character__alliance_name')
readonly_fields = ('owner_hash', 'character')
def get_readonly_fields(self, request, obj=None):
if obj and obj.pk:
return 'owner_hash', 'character'
return tuple()
@admin.register(OwnershipRecord)
class OwnershipRecordAdmin(BaseOwnershipAdmin):
list_display = BaseOwnershipAdmin.list_display + ('created',)
@admin.register(CharacterOwnership)
class CharacterOwnershipAdmin(BaseOwnershipAdmin):
def has_add_permission(self, request): def has_add_permission(self, request):
return False return False

View File

@ -1,8 +1,11 @@
from django.contrib.auth.backends import ModelBackend from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import Permission from django.contrib.auth.models import Permission
from django.contrib.auth.models import User from django.contrib.auth.models import User
import logging
from .models import UserProfile, CharacterOwnership, OwnershipRecord
from .models import UserProfile, CharacterOwnership
logger = logging.getLogger(__name__)
class StateBackend(ModelBackend): class StateBackend(ModelBackend):
@ -30,32 +33,48 @@ class StateBackend(ModelBackend):
try: try:
ownership = CharacterOwnership.objects.get(character__character_id=token.character_id) ownership = CharacterOwnership.objects.get(character__character_id=token.character_id)
if ownership.owner_hash == token.character_owner_hash: if ownership.owner_hash == token.character_owner_hash:
logger.debug('Authenticating {0} by ownership of character {1}'.format(ownership.user, token.character_name))
return ownership.user return ownership.user
else: else:
logger.debug('{0} has changed ownership. Creating new user account.'.format(token.character_name))
ownership.delete() ownership.delete()
return self.create_user(token) return self.create_user(token)
except CharacterOwnership.DoesNotExist: except CharacterOwnership.DoesNotExist:
try: try:
# insecure legacy main check for pre-sso registration auth installs # insecure legacy main check for pre-sso registration auth installs
profile = UserProfile.objects.get(main_character__character_id=token.character_id) profile = UserProfile.objects.get(main_character__character_id=token.character_id)
logger.debug('Authenticating {0} by their main character {1} without active ownership.'.format(profile.user, profile.main_character))
# attach an ownership # attach an ownership
token.user = profile.user token.user = profile.user
CharacterOwnership.objects.create_by_token(token) CharacterOwnership.objects.create_by_token(token)
return profile.user return profile.user
except UserProfile.DoesNotExist: except UserProfile.DoesNotExist:
pass # now we check historical records to see if this is a returning user
records = OwnershipRecord.objects.filter(owner_hash=token.character_owner_hash).filter(character__character_id=token.character_id)
if records.exists():
# we've seen this character owner before. Re-attach to their old user account
user = records[0].user
token.user = user
co = CharacterOwnership.objects.create_by_token(token)
logger.debug('Authenticating {0} by matching owner hash record of character {1}'.format(user, co.character))
if not user.profile.main_character:
# set this as their main by default if they have none
user.profile.main_character = co.character
user.profile.save()
return user
logger.debug('Unable to authenticate character {0}. Creating new user.'.format(token.character_name))
return self.create_user(token) return self.create_user(token)
def create_user(self, token): def create_user(self, token):
username = self.iterate_username(token.character_name) # build unique username off character name username = self.iterate_username(token.character_name) # build unique username off character name
user = User.objects.create_user(username) user = User.objects.create_user(username, is_active=False) # prevent login until email set
user.set_unusable_password() # prevent login via password user.set_unusable_password() # prevent login via password
user.is_active = False # prevent login until email set
user.save() user.save()
token.user = user token.user = user
co = CharacterOwnership.objects.create_by_token(token) # assign ownership to this user co = CharacterOwnership.objects.create_by_token(token) # assign ownership to this user
user.profile.main_character = co.character # assign main character as token character user.profile.main_character = co.character # assign main character as token character
user.profile.save() user.profile.save()
logger.debug('Created new user {0}'.format(user))
return user return user
@staticmethod @staticmethod

View File

@ -0,0 +1,20 @@
from django.core.management.base import BaseCommand
from allianceauth.authentication.models import UserProfile
class Command(BaseCommand):
help = 'Ensures all main characters have an active ownership'
def handle(self, *args, **options):
profiles = UserProfile.objects.filter(main_character__isnull=False).filter(
main_character__character_ownership__isnull=True)
if profiles.exists():
for profile in profiles:
self.stdout.write(self.style.ERROR(
'{0} does not have an ownership. Resetting user {1} main character.'.format(profile.main_character,
profile.user)))
profile.main_character = None
profile.save()
self.stdout.write(self.style.WARNING('Reset {0} main characters.'.format(profiles.count())))
else:
self.stdout.write(self.style.SUCCESS('All main characters have active ownership.'))

View File

@ -43,7 +43,7 @@ def create_member_group(apps, schema_editor):
member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member') member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member')
try: try:
g = Group.objects.get(name=member_state_name) g, _ = Group.objects.get_or_create(name=member_state_name)
# move permissions back # move permissions back
state = State.objects.get(name=member_state_name) state = State.objects.get(name=member_state_name)
[g.permissions.add(p.pk) for p in state.permissions.all()] [g.permissions.add(p.pk) for p in state.permissions.all()]
@ -51,7 +51,7 @@ def create_member_group(apps, schema_editor):
# move users back # move users back
for profile in state.userprofile_set.all().select_related('user'): for profile in state.userprofile_set.all().select_related('user'):
profile.user.groups.add(g.pk) profile.user.groups.add(g.pk)
except (Group.DoesNotExist, State.DoesNotExist): except State.DoesNotExist:
pass pass
@ -67,7 +67,7 @@ def create_blue_state(apps, schema_editor):
# move group permissions to state # move group permissions to state
g = Group.objects.get(name=blue_state_name) g = Group.objects.get(name=blue_state_name)
[s.permissions.add(p.pk) for p in g.permissions.all()] [s.permissions.add(p.pk) for p in g.permissions.all()]
g.permissions.clear() g.delete()
except Group.DoesNotExist: except Group.DoesNotExist:
pass pass
@ -84,7 +84,7 @@ def create_blue_group(apps, schema_editor):
blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue') blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue')
try: try:
g = Group.objects.get(name=blue_state_name) g, _ = Group.objects.get_or_create(name=blue_state_name)
# move permissions back # move permissions back
state = State.objects.get(name=blue_state_name) state = State.objects.get(name=blue_state_name)
[g.permissions.add(p.pk) for p in state.permissions.all()] [g.permissions.add(p.pk) for p in state.permissions.all()]
@ -92,10 +92,15 @@ def create_blue_group(apps, schema_editor):
# move users back # move users back
for profile in state.userprofile_set.all().select_related('user'): for profile in state.userprofile_set.all().select_related('user'):
profile.user.groups.add(g.pk) profile.user.groups.add(g.pk)
except (Group.DoesNotExist, State.DoesNotExist): except State.DoesNotExist:
pass pass
def purge_tokens(apps, schema_editor):
Token = apps.get_model('esi', 'Token')
Token.objects.filter(refresh_token__isnull=True).delete()
def populate_ownerships(apps, schema_editor): def populate_ownerships(apps, schema_editor):
Token = apps.get_model('esi', 'Token') Token = apps.get_model('esi', 'Token')
CharacterOwnership = apps.get_model('authentication', 'CharacterOwnership') CharacterOwnership = apps.get_model('authentication', 'CharacterOwnership')
@ -128,15 +133,24 @@ def create_profiles(apps, schema_editor):
auth['n'] == 1 and EveCharacter.objects.filter(character_id=auth['main_char_id']).exists()] auth['n'] == 1 and EveCharacter.objects.filter(character_id=auth['main_char_id']).exists()]
auths = AuthServicesInfo.objects.filter(main_char_id__in=unique_mains).select_related('user') auths = AuthServicesInfo.objects.filter(main_char_id__in=unique_mains).select_related('user')
blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue')
member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member')
states = {
'Member': State.objects.get(name=member_state_name),
'Blue': State.objects.get(name=blue_state_name),
}
guest_state = State.objects.get(name='Guest')
for auth in auths: for auth in auths:
# carry states and mains forward # carry states and mains forward
state = State.objects.get(name=auth.state if auth.state else 'Guest') state = states.get(auth.state, guest_state)
char = EveCharacter.objects.get(character_id=auth.main_char_id) char = EveCharacter.objects.get(character_id=auth.main_char_id)
UserProfile.objects.create(user=auth.user, state=state, main_character=char) UserProfile.objects.create(user=auth.user, state=state, main_character=char)
for auth in AuthServicesInfo.objects.exclude(main_char_id__in=unique_mains).select_related('user'): for auth in AuthServicesInfo.objects.exclude(main_char_id__in=unique_mains).select_related('user'):
# prepare empty profiles # prepare empty profiles
state = State.objects.get(name='Guest') UserProfile.objects.create(user=auth.user, state=guest_state)
UserProfile.objects.create(user=auth.user, state=state)
def recreate_authservicesinfo(apps, schema_editor): def recreate_authservicesinfo(apps, schema_editor):
@ -144,6 +158,14 @@ def recreate_authservicesinfo(apps, schema_editor):
UserProfile = apps.get_model('authentication', 'UserProfile') UserProfile = apps.get_model('authentication', 'UserProfile')
User = apps.get_model('auth', 'User') User = apps.get_model('auth', 'User')
blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue')
member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member')
states = {
member_state_name: 'Member',
blue_state_name: 'Blue',
}
# recreate all missing AuthServicesInfo models # recreate all missing AuthServicesInfo models
AuthServicesInfo.objects.bulk_create([AuthServicesInfo(user_id=u.pk) for u in User.objects.all()]) AuthServicesInfo.objects.bulk_create([AuthServicesInfo(user_id=u.pk) for u in User.objects.all()])
@ -154,8 +176,8 @@ def recreate_authservicesinfo(apps, schema_editor):
# repopulate states we understand # repopulate states we understand
for profile in UserProfile.objects.exclude(state__name='Guest').filter( for profile in UserProfile.objects.exclude(state__name='Guest').filter(
state__name__in=['Member', 'Blue']).select_related('user', 'state'): state__name__in=[member_state_name, blue_state_name]).select_related('user', 'state'):
AuthServicesInfo.objects.update_or_create(user=profile.user, defaults={'state': profile.state.name}) AuthServicesInfo.objects.update_or_create(user=profile.user, defaults={'state': states[profile.state.name]})
def disable_passwords(apps, schema_editor): def disable_passwords(apps, schema_editor):
@ -221,6 +243,7 @@ class Migration(migrations.Migration):
migrations.RunPython(create_guest_state, migrations.RunPython.noop), migrations.RunPython(create_guest_state, migrations.RunPython.noop),
migrations.RunPython(create_member_state, create_member_group), migrations.RunPython(create_member_state, create_member_group),
migrations.RunPython(create_blue_state, create_blue_group), migrations.RunPython(create_blue_state, create_blue_group),
migrations.RunPython(purge_tokens, migrations.RunPython.noop),
migrations.RunPython(populate_ownerships, migrations.RunPython.noop), migrations.RunPython(populate_ownerships, migrations.RunPython.noop),
migrations.RunPython(create_profiles, recreate_authservicesinfo), migrations.RunPython(create_profiles, recreate_authservicesinfo),
migrations.RemoveField( migrations.RemoveField(

View File

@ -0,0 +1,40 @@
# Generated by Django 2.0.4 on 2018-04-14 18:28
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
def create_initial_records(apps, schema_editor):
OwnershipRecord = apps.get_model('authentication', 'OwnershipRecord')
CharacterOwnership = apps.get_model('authentication', 'CharacterOwnership')
OwnershipRecord.objects.bulk_create([
OwnershipRecord(user=o.user, character=o.character, owner_hash=o.owner_hash) for o in CharacterOwnership.objects.all()
])
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('eveonline', '0009_on_delete'),
('authentication', '0015_user_profiles'),
]
operations = [
migrations.CreateModel(
name='OwnershipRecord',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('owner_hash', models.CharField(db_index=True, max_length=28)),
('created', models.DateTimeField(auto_now=True)),
('character', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ownership_records', to='eveonline.EveCharacter')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ownership_records', to=settings.AUTH_USER_MODEL)),
],
options={
'ordering': ['-created'],
},
),
migrations.RunPython(create_initial_records, migrations.RunPython.noop)
]

View File

@ -96,3 +96,16 @@ class CharacterOwnership(models.Model):
def __str__(self): def __str__(self):
return "%s: %s" % (self.user, self.character) return "%s: %s" % (self.user, self.character)
class OwnershipRecord(models.Model):
character = models.ForeignKey(EveCharacter, on_delete=models.CASCADE, related_name='ownership_records')
owner_hash = models.CharField(max_length=28, db_index=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='ownership_records')
created = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created']
def __str__(self):
return "%s: %s on %s" % (self.user, self.character, self.created)

View File

@ -1,6 +1,6 @@
import logging import logging
from .models import CharacterOwnership, UserProfile, get_guest_state, State from .models import CharacterOwnership, UserProfile, get_guest_state, State, OwnershipRecord
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.db.models import Q from django.db.models import Q
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
@ -103,29 +103,23 @@ def record_character_ownership(sender, instance, created, *args, **kwargs):
@receiver(pre_delete, sender=CharacterOwnership) @receiver(pre_delete, sender=CharacterOwnership)
def validate_main_character(sender, instance, *args, **kwargs): def validate_main_character(sender, instance, *args, **kwargs):
if instance.user.profile.main_character == instance.character: try:
logger.info("Ownership of a main character {0} has been revoked. Resetting {1} main character.".format( if instance.user.profile.main_character == instance.character:
instance.character, instance.user)) logger.info("Ownership of a main character {0} has been revoked. Resetting {1} main character.".format(
# clear main character as user no longer owns them instance.character, instance.user))
instance.user.profile.main_character = None # clear main character as user no longer owns them
instance.user.profile.save() instance.user.profile.main_character = None
instance.user.profile.save()
except UserProfile.DoesNotExist:
# a user is being deleted
pass
@receiver(post_delete, sender=Token) @receiver(post_delete, sender=Token)
def validate_main_character_token(sender, instance, *args, **kwargs): def validate_ownership(sender, instance, *args, **kwargs):
if UserProfile.objects.filter(main_character__character_id=instance.character_id).exists(): if not Token.objects.filter(character_owner_hash=instance.character_owner_hash).filter(refresh_token__isnull=False).exists():
logger.debug( logger.info("No remaining tokens to validate ownership of character {0}. Revoking ownership.".format(instance.character_name))
"Token for a main character {0} is being deleted. Ensuring there are valid tokens to refresh.".format( CharacterOwnership.objects.filter(owner_hash=instance.character_owner_hash).delete()
instance.character_name))
profile = UserProfile.objects.get(main_character__character_id=instance.character_id)
if not Token.objects.filter(character_id=instance.character_id).filter(
user=profile.user).filter(refresh_token__isnull=False).exists():
logger.info(
"No remaining tokens to validate {0} ownership of main character {1}. Resetting main character.".format(
profile.user, profile.main_character))
# clear main character as we can no longer verify ownership
profile.main_character = None
profile.save()
@receiver(pre_save, sender=User) @receiver(pre_save, sender=User)
@ -153,3 +147,15 @@ def check_state_on_character_update(sender, instance, *args, **kwargs):
except UserProfile.DoesNotExist: except UserProfile.DoesNotExist:
logger.debug("Character {0} is not a main character. No state assessment required.".format(instance)) logger.debug("Character {0} is not a main character. No state assessment required.".format(instance))
pass pass
@receiver(post_save, sender=CharacterOwnership)
def ownership_record_creation(sender, instance, created, *args, **kwargs):
if created:
records = OwnershipRecord.objects.filter(owner_hash=instance.owner_hash).filter(character=instance.character)
if records.exists():
if records[0].user == instance.user: # most recent record is sorted first
logger.debug("Already have ownership record of {0} by user {1}".format(instance.character, instance.user))
return
logger.info("Character {0} has a new owner {1}. Creating ownership record.".format(instance.character, instance.user))
OwnershipRecord.objects.create(user=instance.user, character=instance.character, owner_hash=instance.owner_hash)

View File

@ -1,6 +1,6 @@
import logging import logging
from esi.errors import TokenExpiredError, TokenInvalidError from esi.errors import TokenExpiredError, TokenInvalidError, IncompleteResponseError
from esi.models import Token from esi.models import Token
from celery import shared_task from celery import shared_task
@ -20,13 +20,19 @@ def check_character_ownership(owner_hash):
except (TokenExpiredError, TokenInvalidError): except (TokenExpiredError, TokenInvalidError):
t.delete() t.delete()
continue continue
except (KeyError, IncompleteResponseError):
if t.character_owner_hash == old_hash: # We can't validate the hash hasn't changed but also can't assume it has. Abort for now.
logger.warning("Failed to validate owner hash of {0} due to problems contacting SSO servers.".format(
tokens[0].character_name))
break break
else:
logger.info('Character %s has changed ownership. Revoking %s tokens.' % (t.character_name, tokens.count())) if not t.character_owner_hash == old_hash:
logger.info(
'Character %s has changed ownership. Revoking %s tokens.' % (t.character_name, tokens.count()))
tokens.delete() tokens.delete()
else: break
if not Token.objects.filter(character_owner_hash=owner_hash).exists():
logger.info('No tokens found with owner hash %s. Revoking ownership.' % owner_hash) logger.info('No tokens found with owner hash %s. Revoking ownership.' % owner_hash)
CharacterOwnership.objects.filter(owner_hash=owner_hash).delete() CharacterOwnership.objects.filter(owner_hash=owner_hash).delete()

View File

@ -1,19 +1,21 @@
from unittest import mock from unittest import mock
from io import StringIO
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.models import User from django.contrib.auth.models import User
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from .models import CharacterOwnership, UserProfile, State, get_guest_state from .models import CharacterOwnership, UserProfile, State, get_guest_state, OwnershipRecord
from .backends import StateBackend from .backends import StateBackend
from .tasks import check_character_ownership from .tasks import check_character_ownership
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from esi.models import Token from esi.models import Token
from esi.errors import IncompleteResponseError
from allianceauth.authentication.decorators import main_character_required from allianceauth.authentication.decorators import main_character_required
from django.test.client import RequestFactory from django.test.client import RequestFactory
from django.http.response import HttpResponse from django.http.response import HttpResponse
from django.contrib.auth.models import AnonymousUser from django.contrib.auth.models import AnonymousUser
from django.conf import settings from django.conf import settings
from django.shortcuts import reverse from django.shortcuts import reverse
from django.core.management import call_command
from urllib import parse from urllib import parse
MODULE_PATH = 'allianceauth.authentication' MODULE_PATH = 'allianceauth.authentication'
@ -90,6 +92,7 @@ class BackendTestCase(TestCase):
corporation_ticker='CORP', corporation_ticker='CORP',
) )
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True) cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
cls.old_user = AuthUtils.create_user('old_user', disconnect_signals=True)
AuthUtils.disconnect_signals() AuthUtils.disconnect_signals()
CharacterOwnership.objects.create(user=cls.user, character=cls.main_character, owner_hash='1') CharacterOwnership.objects.create(user=cls.user, character=cls.main_character, owner_hash='1')
CharacterOwnership.objects.create(user=cls.user, character=cls.alt_character, owner_hash='2') CharacterOwnership.objects.create(user=cls.user, character=cls.alt_character, owner_hash='2')
@ -113,6 +116,14 @@ class BackendTestCase(TestCase):
self.assertEqual(user.username, 'Unclaimed_Character') self.assertEqual(user.username, 'Unclaimed_Character')
self.assertEqual(user.profile.main_character, self.unclaimed_character) self.assertEqual(user.profile.main_character, self.unclaimed_character)
def test_authenticate_character_record(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4')
record = OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4')
user = StateBackend().authenticate(t)
self.assertEqual(user, self.old_user)
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists())
self.assertTrue(user.profile.main_character)
def test_iterate_username(self): def test_iterate_username(self):
t = Token(character_id=self.unclaimed_character.character_id, t = Token(character_id=self.unclaimed_character.character_id,
character_name=self.unclaimed_character.character_name, character_owner_hash='3') character_name=self.unclaimed_character.character_name, character_owner_hash='3')
@ -185,28 +196,6 @@ class CharacterOwnershipTestCase(TestCase):
self.user = User.objects.get(pk=self.user.pk) self.user = User.objects.get(pk=self.user.pk)
self.assertIsNone(self.user.profile.main_character) self.assertIsNone(self.user.profile.main_character)
@mock.patch('esi.models.Token.update_token_data')
def test_character_ownership_check(self, update_token_data):
t = Token.objects.create(
user=self.user,
character_id=self.character.character_id,
character_name=self.character.character_name,
character_owner_hash='1',
)
co = CharacterOwnership.objects.get(owner_hash='1')
check_character_ownership(co.owner_hash)
self.assertTrue(CharacterOwnership.objects.filter(owner_hash='1').exists())
t.character_owner_hash = '2'
t.save()
check_character_ownership(co.owner_hash)
self.assertFalse(CharacterOwnership.objects.filter(owner_hash='1').exists())
t.delete()
co = CharacterOwnership.objects.create(user=self.user, character=self.character, owner_hash='3')
check_character_ownership(co.owner_hash)
self.assertFalse(CharacterOwnership.objects.filter(owner_hash='3').exists())
class StateTestCase(TestCase): class StateTestCase(TestCase):
@classmethod @classmethod
@ -341,3 +330,73 @@ class StateTestCase(TestCase):
self.user.save() self.user.save()
self._refresh_user() self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state) self.assertEquals(self.user.profile.state, self.member_state)
class CharacterOwnershipCheckTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
AuthUtils.add_main_character(cls.user, 'Test Character', '1', corp_id='1', alliance_id='1',
corp_name='Test Corp', alliance_name='Test Alliance')
cls.character = EveCharacter.objects.get(character_id='1')
cls.token = Token.objects.create(
user=cls.user,
character_id='1',
character_name='Test',
character_owner_hash='1',
)
cls.ownership = CharacterOwnership.objects.get(character=cls.character)
@mock.patch(MODULE_PATH + '.tasks.Token.update_token_data')
def test_no_change_owner_hash(self, update_token_data):
# makes sure the ownership isn't delete if owner hash hasn't changed
check_character_ownership(self.ownership)
self.assertTrue(CharacterOwnership.objects.filter(user=self.user).filter(character=self.character).exists())
@mock.patch(MODULE_PATH + '.tasks.Token.update_token_data')
def test_unable_to_update_token_data(self, update_token_data):
# makes sure ownerships and tokens aren't hellpurged when there's problems with the SSO servers
update_token_data.side_effect = IncompleteResponseError()
check_character_ownership(self.ownership)
self.assertTrue(CharacterOwnership.objects.filter(user=self.user).filter(character=self.character).exists())
update_token_data.side_effect = KeyError()
check_character_ownership(self.ownership)
self.assertTrue(CharacterOwnership.objects.filter(user=self.user).filter(character=self.character).exists())
@mock.patch(MODULE_PATH + '.tasks.Token.update_token_data')
@mock.patch(MODULE_PATH + '.tasks.Token.delete')
@mock.patch(MODULE_PATH + '.tasks.Token.objects.exists')
@mock.patch(MODULE_PATH + '.tasks.CharacterOwnership.objects.filter')
def test_owner_hash_changed(self, filter, exists, delete, update_token_data):
# makes sure the ownership is revoked when owner hash changes
filter.return_value.exists.return_value = False
check_character_ownership(self.ownership)
self.assertTrue(filter.return_value.delete.called)
class ManagementCommandTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('test user', disconnect_signals=True)
AuthUtils.add_main_character(cls.user, 'test character', '1', '2', 'test corp', 'test')
character = UserProfile.objects.get(user=cls.user).main_character
CharacterOwnership.objects.create(user=cls.user, character=character, owner_hash='test')
def setUp(self):
self.stdout = StringIO()
def test_ownership(self):
call_command('checkmains', stdout=self.stdout)
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
self.assertNotIn(self.user.username, self.stdout.getvalue())
self.assertIn('All main characters', self.stdout.getvalue())
def test_no_ownership(self):
user = AuthUtils.create_user('v1 user', disconnect_signals=True)
AuthUtils.add_main_character(user, 'v1 character', '10', '20', 'test corp', 'test')
self.assertFalse(UserProfile.objects.filter(main_character__isnull=True).count())
call_command('checkmains', stdout=self.stdout)
self.assertEqual(UserProfile.objects.filter(main_character__isnull=True).count(), 1)
self.assertIn(user.username, self.stdout.getvalue())

View File

@ -95,20 +95,33 @@ class RegistrationView(BaseRegistrationView):
form_class = RegistrationForm form_class = RegistrationForm
success_url = 'authentication:dashboard' success_url = 'authentication:dashboard'
def dispatch(self, *args, **kwargs): def get_success_url(self, user):
if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
return 'authentication:dashboard', (), {}
return super().get_success_url(user)
def dispatch(self, request, *args, **kwargs):
# We're storing a key in the session to pass user information from OAuth response. Make sure it's there. # We're storing a key in the session to pass user information from OAuth response. Make sure it's there.
if not self.request.session.get('registration_uid', None) or not User.objects.filter( if not self.request.session.get('registration_uid', None) or not User.objects.filter(
pk=self.request.session.get('registration_uid')).exists(): pk=self.request.session.get('registration_uid')).exists():
messages.error(self.request, _('Registration token has expired.')) messages.error(self.request, _('Registration token has expired.'))
return redirect(settings.LOGIN_URL) return redirect(settings.LOGIN_URL)
return super(RegistrationView, self).dispatch(*args, **kwargs) if not getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
# Keep the request so the user can be automagically logged in.
setattr(self, 'request', request)
return super(RegistrationView, self).dispatch(request, *args, **kwargs)
def register(self, form): def register(self, form):
user = User.objects.get(pk=self.request.session.get('registration_uid')) user = User.objects.get(pk=self.request.session.get('registration_uid'))
user.email = form.cleaned_data['email'] user.email = form.cleaned_data['email']
user_registered.send(self.__class__, user=user, request=self.request) user_registered.send(self.__class__, user=user, request=self.request)
# Go to Step 3 if getattr(settings, 'REGISTRATION_VERIFY_EMAIL', True):
self.send_activation_email(user) # Go to Step 3
self.send_activation_email(user)
else:
user.is_active = True
user.save()
login(self.request, user, 'allianceauth.authentication.backends.StateBackend')
return user return user
def get_activation_key(self, user): def get_activation_key(self, user):

View File

@ -40,9 +40,8 @@ class CorpStats(models.Model):
c = self.token.get_esi_client(spec_file=SWAGGER_SPEC_PATH) 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( member_ids = c.Corporation.get_corporations_corporation_id_members(
corporation_id=self.corp.corporation_id).result() corporation_id=self.corp.corporation_id).result()
member_ids = [m['character_id'] for m in members]
# requesting too many ids per call results in a HTTP400 # requesting too many ids per call results in a HTTP400
# the swagger spec doesn't have a maxItems count # the swagger spec doesn't have a maxItems count

File diff suppressed because one or more lines are too long

View File

@ -85,7 +85,7 @@ class CorpStatsUpdateTestCase(TestCase):
@mock.patch('esi.clients.SwaggerClient') @mock.patch('esi.clients.SwaggerClient')
def test_update_add_member(self, SwaggerClient): def test_update_add_member(self, SwaggerClient):
SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 2} SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 2}
SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [{'character_id': 1}] SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [1]
SwaggerClient.from_spec.return_value.Character.get_characters_names.return_value.result.return_value = [{'character_id': 1, 'character_name': 'test character'}] SwaggerClient.from_spec.return_value.Character.get_characters_names.return_value.result.return_value = [{'character_id': 1, 'character_name': 'test character'}]
self.corpstats.update() self.corpstats.update()
self.assertTrue(CorpMember.objects.filter(character_id='1', character_name='test character', corpstats=self.corpstats).exists()) self.assertTrue(CorpMember.objects.filter(character_id='1', character_name='test character', corpstats=self.corpstats).exists())
@ -94,7 +94,7 @@ class CorpStatsUpdateTestCase(TestCase):
def test_update_remove_member(self, SwaggerClient): def test_update_remove_member(self, SwaggerClient):
CorpMember.objects.create(character_id='2', character_name='old test character', corpstats=self.corpstats) CorpMember.objects.create(character_id='2', character_name='old test character', corpstats=self.corpstats)
SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 2} SwaggerClient.from_spec.return_value.Character.get_characters_character_id.return_value.result.return_value = {'corporation_id': 2}
SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [{'character_id': 1}] SwaggerClient.from_spec.return_value.Corporation.get_corporations_corporation_id_members.return_value.result.return_value = [1]
SwaggerClient.from_spec.return_value.Character.get_characters_names.return_value.result.return_value = [{'character_id': 1, 'character_name': 'test character'}] SwaggerClient.from_spec.return_value.Character.get_characters_names.return_value.result.return_value = [{'character_id': 1, 'character_name': 'test character'}]
self.corpstats.update() self.corpstats.update()
self.assertFalse(CorpMember.objects.filter(character_id='2', corpstats=self.corpstats).exists()) self.assertFalse(CorpMember.objects.filter(character_id='2', corpstats=self.corpstats).exists())

View File

@ -11,7 +11,7 @@ logger = logging.getLogger(__name__)
def get_users_for_state(state: State): def get_users_for_state(state: State):
return User.objects.select_related('profile').prefetch_related('profile__main_character')\ return User.objects.select_related('profile').prefetch_related('profile__main_character')\
.filter(profile__state__pk=state.pk) .filter(profile__state_id=state.pk)
class AutogroupsConfigManager(models.Manager): class AutogroupsConfigManager(models.Manager):
@ -39,7 +39,12 @@ class AutogroupsConfigManager(models.Manager):
if state is None: if state is None:
state = user.profile.state state = user.profile.state
for config in self.filter(states=state): for config in self.filter(states=state):
config.update_group_membership_for_user(user) # grant user new groups for their state
config.update_group_membership_for_user(user)
for config in self.exclude(states=state):
# ensure user does not have groups from previous state
config.remove_user_from_alliance_groups(user)
config.remove_user_from_corp_groups(user)
class AutogroupsConfig(models.Model): class AutogroupsConfig(models.Model):
@ -119,8 +124,9 @@ class AutogroupsConfig(models.Model):
return return
group = self.get_alliance_group(alliance) group = self.get_alliance_group(alliance)
except EveAllianceInfo.DoesNotExist: except EveAllianceInfo.DoesNotExist:
logger.warning('User {} main characters alliance does not exist in the database.' logger.debug('User {} main characters alliance does not exist in the database. Creating.'.format(user))
' Group membership not updated'.format(user)) alliance = EveAllianceInfo.objects.create_alliance(user.profile.main_character.alliance_id)
group = self.get_alliance_group(alliance)
except AttributeError: except AttributeError:
logger.warning('User {} does not have a main character. Group membership not updated'.format(user)) logger.warning('User {} does not have a main character. Group membership not updated'.format(user))
finally: finally:
@ -139,8 +145,9 @@ class AutogroupsConfig(models.Model):
corp = user.profile.main_character.corporation corp = user.profile.main_character.corporation
group = self.get_corp_group(corp) group = self.get_corp_group(corp)
except EveCorporationInfo.DoesNotExist: except EveCorporationInfo.DoesNotExist:
logger.warning('User {} main characters corporation does not exist in the database.' logger.debug('User {} main characters corporation does not exist in the database. Creating.'.format(user))
' Group membership not updated'.format(user)) corp = EveCorporationInfo.objects.create_corporation(user.profile.main_character.corporation_id)
group = self.get_corp_group(corp)
except AttributeError: except AttributeError:
logger.warning('User {} does not have a main character. Group membership not updated'.format(user)) logger.warning('User {} does not have a main character. Group membership not updated'.format(user))
finally: finally:

View File

@ -2,6 +2,7 @@ import logging
from django.dispatch import receiver from django.dispatch import receiver
from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed
from allianceauth.authentication.models import UserProfile, State from allianceauth.authentication.models import UserProfile, State
from allianceauth.eveonline.models import EveCharacter
from .models import AutogroupsConfig from .models import AutogroupsConfig
@ -45,9 +46,7 @@ def check_groups_on_profile_update(sender, instance, created, *args, **kwargs):
""" """
Trigger check when main character or state changes. Trigger check when main character or state changes.
""" """
update_fields = kwargs.pop('update_fields', []) or [] AutogroupsConfig.objects.update_groups_for_user(instance.user)
if 'main_character' in update_fields or 'state' in update_fields:
AutogroupsConfig.objects.update_groups_for_user(instance.user)
@receiver(m2m_changed, sender=AutogroupsConfig.states.through) @receiver(m2m_changed, sender=AutogroupsConfig.states.through)
@ -64,3 +63,13 @@ def autogroups_states_changed(sender, instance, action, reverse, model, pk_set,
except State.DoesNotExist: except State.DoesNotExist:
# Deleted States handled by the profile state change # Deleted States handled by the profile state change
pass pass
@receiver(post_save, sender=EveCharacter)
def check_groups_on_character_update(sender, instance, created, *args, **kwargs):
if not created:
try:
profile = UserProfile.objects.prefetch_related('user').get(main_character_id=instance.pk)
AutogroupsConfig.objects.update_groups_for_user(profile.user)
except UserProfile.DoesNotExist:
pass

View File

@ -1,8 +1,7 @@
from unittest import mock from unittest import mock
from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed from django.db.models.signals import pre_save, post_save, pre_delete, m2m_changed
from allianceauth.authentication.models import UserProfile from allianceauth.authentication.models import UserProfile
from allianceauth.authentication.signals import state_changed from allianceauth.authentication.signals import reassess_on_profile_save
from allianceauth.eveonline.models import EveCharacter
from .. import signals from .. import signals
from ..models import AutogroupsConfig from ..models import AutogroupsConfig
@ -14,6 +13,7 @@ def patch(target, *args, **kwargs):
def connect_signals(): def connect_signals():
post_save.connect(receiver=reassess_on_profile_save, sender=UserProfile)
pre_save.connect(receiver=signals.pre_save_config, sender=AutogroupsConfig) pre_save.connect(receiver=signals.pre_save_config, sender=AutogroupsConfig)
pre_delete.connect(receiver=signals.pre_delete_config, sender=AutogroupsConfig) pre_delete.connect(receiver=signals.pre_delete_config, sender=AutogroupsConfig)
post_save.connect(receiver=signals.check_groups_on_profile_update, sender=UserProfile) post_save.connect(receiver=signals.check_groups_on_profile_update, sender=UserProfile)
@ -21,6 +21,7 @@ def connect_signals():
def disconnect_signals(): def disconnect_signals():
post_save.disconnect(receiver=reassess_on_profile_save, sender=UserProfile)
pre_save.disconnect(receiver=signals.pre_save_config, sender=AutogroupsConfig) pre_save.disconnect(receiver=signals.pre_save_config, sender=AutogroupsConfig)
pre_delete.disconnect(receiver=signals.pre_delete_config, sender=AutogroupsConfig) pre_delete.disconnect(receiver=signals.pre_delete_config, sender=AutogroupsConfig)
post_save.disconnect(receiver=signals.check_groups_on_profile_update, sender=UserProfile) post_save.disconnect(receiver=signals.check_groups_on_profile_update, sender=UserProfile)

View File

@ -7,7 +7,7 @@ from . import patch
class AutogroupsConfigManagerTestCase(TestCase): class AutogroupsConfigManagerTestCase(TestCase):
def test_update_groups_for_state(self, ): def test_update_groups_for_state(self):
member = AuthUtils.create_member('test member') member = AuthUtils.create_member('test member')
obj = AutogroupsConfig.objects.create() obj = AutogroupsConfig.objects.create()
obj.states.add(member.profile.state) obj.states.add(member.profile.state)
@ -32,3 +32,23 @@ class AutogroupsConfigManagerTestCase(TestCase):
self.assertEqual(update_group_membership_for_user.call_count, 1) self.assertEqual(update_group_membership_for_user.call_count, 1)
args, kwargs = update_group_membership_for_user.call_args args, kwargs = update_group_membership_for_user.call_args
self.assertEqual(args[0], member) self.assertEqual(args[0], member)
@patch('.models.AutogroupsConfig.update_group_membership_for_user')
@patch('.models.AutogroupsConfig.remove_user_from_alliance_groups')
@patch('.models.AutogroupsConfig.remove_user_from_corp_groups')
def test_update_groups_no_config(self, remove_corp, remove_alliance, update_groups):
member = AuthUtils.create_member('test member')
obj = AutogroupsConfig.objects.create()
# Corp and alliance groups should be removed from users if their state has no config
AutogroupsConfig.objects.update_groups_for_user(member)
self.assertFalse(update_groups.called)
self.assertTrue(remove_alliance.called)
self.assertTrue(remove_corp.called)
# The normal group assignment should occur if there state has a config
obj.states.add(member.profile.state)
AutogroupsConfig.objects.update_groups_for_user(member)
self.assertTrue(update_groups.called)

View File

@ -16,8 +16,6 @@ class AutogroupsConfigTestCase(TestCase):
# Disconnect signals # Disconnect signals
disconnect_signals() disconnect_signals()
self.member = AuthUtils.create_member('test user')
state = AuthUtils.get_member_state() state = AuthUtils.get_member_state()
self.alliance = EveAllianceInfo.objects.create( self.alliance = EveAllianceInfo.objects.create(
@ -38,6 +36,8 @@ class AutogroupsConfigTestCase(TestCase):
state.member_alliances.add(self.alliance) state.member_alliances.add(self.alliance)
state.member_corporations.add(self.corp) state.member_corporations.add(self.corp)
self.member = AuthUtils.create_member('test user')
def tearDown(self): def tearDown(self):
# Reconnect signals # Reconnect signals
connect_signals() connect_signals()

View File

@ -1,11 +1,11 @@
from django.test import TestCase from django.test import TestCase
from django.contrib.auth.models import Group, User from django.contrib.auth.models import User
from allianceauth.tests.auth_utils import AuthUtils from allianceauth.tests.auth_utils import AuthUtils
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from ..models import AutogroupsConfig, ManagedAllianceGroup from ..models import AutogroupsConfig
from . import patch, disconnect_signals, connect_signals from . import patch, disconnect_signals, connect_signals
@ -13,8 +13,6 @@ from . import patch, disconnect_signals, connect_signals
class SignalsTestCase(TestCase): class SignalsTestCase(TestCase):
def setUp(self): def setUp(self):
disconnect_signals() disconnect_signals()
self.member = AuthUtils.create_member('test user')
state = AuthUtils.get_member_state() state = AuthUtils.get_member_state()
self.char = EveCharacter.objects.create( self.char = EveCharacter.objects.create(
@ -27,9 +25,6 @@ class SignalsTestCase(TestCase):
alliance_name='alliance name', alliance_name='alliance name',
) )
self.member.profile.main_character = self.char
self.member.profile.save()
self.alliance = EveAllianceInfo.objects.create( self.alliance = EveAllianceInfo.objects.create(
alliance_id='3456', alliance_id='3456',
alliance_name='alliance name', alliance_name='alliance name',
@ -47,13 +42,17 @@ class SignalsTestCase(TestCase):
state.member_alliances.add(self.alliance) state.member_alliances.add(self.alliance)
state.member_corporations.add(self.corp) state.member_corporations.add(self.corp)
self.member = AuthUtils.create_member('test user')
self.member.profile.main_character = self.char
self.member.profile.save()
connect_signals() connect_signals()
@patch('.models.AutogroupsConfigManager.update_groups_for_user') @patch('.models.AutogroupsConfigManager.update_groups_for_user')
def test_check_groups_on_profile_update_state(self, update_groups_for_user): def test_check_groups_on_profile_update_state(self, update_groups_for_user):
# Trigger signal # Trigger signal
self.member.profile.state = AuthUtils.get_guest_state() self.member.profile.assign_state(state=AuthUtils.get_guest_state())
self.member.profile.save()
self.assertTrue(update_groups_for_user.called) self.assertTrue(update_groups_for_user.called)
self.assertEqual(update_groups_for_user.call_count, 1) self.assertEqual(update_groups_for_user.call_count, 1)
@ -71,10 +70,10 @@ class SignalsTestCase(TestCase):
alliance_id='3456', alliance_id='3456',
alliance_name='alliance name', alliance_name='alliance name',
) )
# Trigger signal # Trigger signal
self.member.profile.main_character = char self.member.profile.main_character = char
self.member.profile.save() self.member.profile.save()
self.assertTrue(update_groups_for_user.called) self.assertTrue(update_groups_for_user.called)
self.assertEqual(update_groups_for_user.call_count, 1) self.assertEqual(update_groups_for_user.call_count, 1)
args, kwargs = update_groups_for_user.call_args args, kwargs = update_groups_for_user.call_args

View File

@ -26,6 +26,7 @@ class EveCharacterManager(models.Manager):
corporation_ticker=character.corp.ticker, corporation_ticker=character.corp.ticker,
alliance_id=character.alliance.id, alliance_id=character.alliance.id,
alliance_name=character.alliance.name, alliance_name=character.alliance.name,
alliance_ticker=getattr(character.alliance, 'ticker', None),
) )
def update_character(self, character_id): def update_character(self, character_id):

View File

@ -0,0 +1,23 @@
# Generated by Django 2.0.4 on 2018-04-17 20:07
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('eveonline', '0009_on_delete'),
]
operations = [
migrations.AddField(
model_name='evecharacter',
name='alliance_ticker',
field=models.CharField(blank=True, default='', max_length=5, null=True),
),
migrations.AlterField(
model_name='evecharacter',
name='corporation_ticker',
field=models.CharField(max_length=5),
),
]

View File

@ -84,9 +84,10 @@ class EveCharacter(models.Model):
character_name = models.CharField(max_length=254, unique=True) character_name = models.CharField(max_length=254, unique=True)
corporation_id = models.CharField(max_length=254) corporation_id = models.CharField(max_length=254)
corporation_name = models.CharField(max_length=254) corporation_name = models.CharField(max_length=254)
corporation_ticker = models.CharField(max_length=254) corporation_ticker = models.CharField(max_length=5)
alliance_id = models.CharField(max_length=254, blank=True, null=True, default='') alliance_id = models.CharField(max_length=254, blank=True, null=True, default='')
alliance_name = models.CharField(max_length=254, blank=True, null=True, default='') alliance_name = models.CharField(max_length=254, blank=True, null=True, default='')
alliance_ticker = models.CharField(max_length=5, blank=True, null=True, default='')
objects = EveCharacterManager() objects = EveCharacterManager()
provider = EveCharacterProviderManager() provider = EveCharacterProviderManager()
@ -120,6 +121,7 @@ class EveCharacter(models.Model):
self.corporation_ticker = character.corp.ticker self.corporation_ticker = character.corp.ticker
self.alliance_id = character.alliance.id self.alliance_id = character.alliance.id
self.alliance_name = character.alliance.name self.alliance_name = character.alliance.name
self.alliance_ticker = getattr(character.alliance, 'ticker', None)
self.save() self.save()
return self return self

View File

@ -81,7 +81,9 @@ class Alliance(Entity):
@property @property
def executor_corp(self): def executor_corp(self):
return self.corp(self.executor_corp_id) if self.executor_corp_id:
return self.corp(self.executor_corp_id)
return Entity(None, None)
class Character(Entity): class Character(Entity):
@ -150,10 +152,10 @@ class EveSwaggerProvider(EveProvider):
corps = self.client.Alliance.get_alliances_alliance_id_corporations(alliance_id=alliance_id).result() corps = self.client.Alliance.get_alliances_alliance_id_corporations(alliance_id=alliance_id).result()
model = Alliance( model = Alliance(
id=alliance_id, id=alliance_id,
name=data['alliance_name'], name=data['name'],
ticker=data['ticker'], ticker=data['ticker'],
corp_ids=corps, corp_ids=corps,
executor_corp_id=data['executor_corp'], executor_corp_id=data['executor_corporation_id'] if 'executor_corporation_id' in data else None,
) )
return model return model
except HTTPNotFound: except HTTPNotFound:
@ -164,7 +166,7 @@ class EveSwaggerProvider(EveProvider):
data = self.client.Corporation.get_corporations_corporation_id(corporation_id=corp_id).result() data = self.client.Corporation.get_corporations_corporation_id(corporation_id=corp_id).result()
model = Corporation( model = Corporation(
id=corp_id, id=corp_id,
name=data['corporation_name'], name=data['name'],
ticker=data['ticker'], ticker=data['ticker'],
ceo_id=data['ceo_id'], ceo_id=data['ceo_id'],
members=data['member_count'], members=data['member_count'],
@ -177,12 +179,11 @@ class EveSwaggerProvider(EveProvider):
def get_character(self, character_id): def get_character(self, character_id):
try: try:
data = self.client.Character.get_characters_character_id(character_id=character_id).result() data = self.client.Character.get_characters_character_id(character_id=character_id).result()
alliance_id = self.adapter.get_corp(data['corporation_id']).alliance_id
model = Character( model = Character(
id=character_id, id=character_id,
name=data['name'], name=data['name'],
corp_id=data['corporation_id'], corp_id=data['corporation_id'],
alliance_id=alliance_id, alliance_id=data['alliance_id'] if 'alliance_id' in data else None,
) )
return model return model
except (HTTPNotFound, HTTPUnprocessableEntity): except (HTTPNotFound, HTTPUnprocessableEntity):

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -155,7 +155,7 @@
<span class="glyphicon glyphicon-eye-open"></span> <span class="glyphicon glyphicon-eye-open"></span>
</a> </a>
{% if perms.hrapplications.delete_application %} {% if perms.hrapplications.delete_application %}
<a href="(% url 'hrapplications:remove' app.id %}" <a href="{% url 'hrapplications:remove' app.id %}"
class="btn btn-danger"> class="btn btn-danger">
<span class="glyphicon glyphicon-remove"></span> <span class="glyphicon glyphicon-remove"></span>
</a> </a>

View File

@ -13,19 +13,19 @@ urlpatterns = [
name="create_view"), name="create_view"),
url(r'^remove/(\w+)', views.hr_application_remove, url(r'^remove/(\w+)', views.hr_application_remove,
name="remove"), name="remove"),
url(r'view/(\w+)', views.hr_application_view, url(r'^view/(\w+)', views.hr_application_view,
name="view"), name="view"),
url(r'personal/view/(\w+)', views.hr_application_personal_view, url(r'^personal/view/(\w+)', views.hr_application_personal_view,
name="personal_view"), name="personal_view"),
url(r'personal/removal/(\w+)', url(r'^personal/removal/(\w+)',
views.hr_application_personal_removal, views.hr_application_personal_removal,
name="personal_removal"), name="personal_removal"),
url(r'approve/(\w+)', views.hr_application_approve, url(r'^approve/(\w+)', views.hr_application_approve,
name="approve"), name="approve"),
url(r'reject/(\w+)', views.hr_application_reject, url(r'^reject/(\w+)', views.hr_application_reject,
name="reject"), name="reject"),
url(r'search/', views.hr_application_search, url(r'^search/', views.hr_application_search,
name="search"), name="search"),
url(r'mark_in_progress/(\w+)', views.hr_application_mark_in_progress, url(r'^mark_in_progress/(\w+)', views.hr_application_mark_in_progress,
name="mark_in_progress"), name="mark_in_progress"),
] ]

View File

@ -11,6 +11,10 @@ app = Celery('{{ project_name }}')
# Using a string here means the worker don't have to serialize # Using a string here means the worker don't have to serialize
# the configuration object to child processes. # the configuration object to child processes.
app.config_from_object('django.conf:settings') app.config_from_object('django.conf:settings')
app.conf.ONCE = {
'backend': 'allianceauth.services.tasks.DjangoBackend',
'settings': {}
}
# Load task modules from all registered Django app configs. # Load task modules from all registered Django app configs.
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

View File

@ -41,11 +41,11 @@ CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler"
CELERYBEAT_SCHEDULE = { CELERYBEAT_SCHEDULE = {
'esi_cleanup_callbackredirect': { 'esi_cleanup_callbackredirect': {
'task': 'esi.tasks.cleanup_callbackredirect', 'task': 'esi.tasks.cleanup_callbackredirect',
'schedule': crontab(hour='*/4'), 'schedule': crontab(minute=0, hour='*/4'),
}, },
'esi_cleanup_token': { 'esi_cleanup_token': {
'task': 'esi.tasks.cleanup_token', 'task': 'esi.tasks.cleanup_token',
'schedule': crontab(day_of_month='*/1'), 'schedule': crontab(minute=0, hour=0),
}, },
'run_model_update': { 'run_model_update': {
'task': 'allianceauth.eveonline.tasks.run_model_update', 'task': 'allianceauth.eveonline.tasks.run_model_update',
@ -53,7 +53,7 @@ CELERYBEAT_SCHEDULE = {
}, },
'check_all_character_ownership': { 'check_all_character_ownership': {
'task': 'allianceauth.authentication.tasks.check_all_character_ownership', 'task': 'allianceauth.authentication.tasks.check_all_character_ownership',
'schedule': crontab(hour='*/4'), 'schedule': crontab(minute=0, hour='*/4'),
} }
} }
@ -166,7 +166,6 @@ CACHES = {
} }
} }
SECRET_KEY = 'this is a very bad secret key you should change'
DEBUG = True DEBUG = True
ALLOWED_HOSTS = ['*'] ALLOWED_HOSTS = ['*']
DATABASES = { DATABASES = {
@ -235,5 +234,9 @@ LOGGING = {
'handlers': ['log_file', 'console'], 'handlers': ['log_file', 'console'],
'level': 'ERROR', 'level': 'ERROR',
}, },
'esi': {
'handlers': ['log_file', 'console'],
'level': 'DEBUG',
},
} }
} }

View File

@ -41,10 +41,13 @@ ESI_SSO_CLIENT_ID = ''
ESI_SSO_CLIENT_SECRET = '' ESI_SSO_CLIENT_SECRET = ''
ESI_SSO_CALLBACK_URL = '' ESI_SSO_CALLBACK_URL = ''
# Emails are validated before new users can log in. # By default emails are validated before new users can log in.
# It's recommended to use a free service like SparkPost or Mailgun to send email. # It's recommended to use a free service like SparkPost or Elastic Email to send email.
# https://www.sparkpost.com/docs/integrations/django/ # https://www.sparkpost.com/docs/integrations/django/
# https://elasticemail.com/resources/settings/smtp-api/
# Set the default from email to something like 'noreply@example.com' # Set the default from email to something like 'noreply@example.com'
# Email validation can be turned off by uncommenting the line below. This can break some services.
# REGISTRATION_VERIFY_EMAIL = False
EMAIL_HOST = '' EMAIL_HOST = ''
EMAIL_PORT = 587 EMAIL_PORT = 587
EMAIL_HOST_USER = '' EMAIL_HOST_USER = ''

View File

@ -1,7 +1,6 @@
from django.conf.urls import include, url from django.conf.urls import include, url
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.functional import cached_property from django.utils.functional import cached_property
from django.core.exceptions import ObjectDoesNotExist
from django.conf import settings from django.conf import settings
from string import Formatter from string import Formatter
@ -160,15 +159,12 @@ class NameFormatter:
'corp_id': getattr(main_char, 'corporation_id', None), 'corp_id': getattr(main_char, 'corporation_id', None),
'alliance_name': getattr(main_char, 'alliance_name', None), 'alliance_name': getattr(main_char, 'alliance_name', None),
'alliance_id': getattr(main_char, 'alliance_id', None), 'alliance_id': getattr(main_char, 'alliance_id', None),
'alliance_ticker': getattr(main_char, 'alliance_ticker', None),
'username': self.user.username, 'username': self.user.username,
} }
if main_char is not None and 'alliance_ticker' in self.string_formatter: format_data['alliance_or_corp_name'] = format_data['alliance_name'] or format_data['corp_name']
# Reduces db lookups format_data['alliance_or_corp_ticker'] = format_data['alliance_ticker'] or format_data['corp_ticker']
try:
format_data['alliance_ticker'] = getattr(getattr(main_char, 'alliance', None), 'alliance_ticker', None)
except ObjectDoesNotExist:
format_data['alliance_ticker'] = None
return format_data return format_data
@cached_property @cached_property

View File

@ -204,7 +204,7 @@ class DiscordOAuthManager:
'access_token': token, 'access_token': token,
} }
if nickname: if nickname:
data['nick'] = nickname data['nick'] = DiscordOAuthManager._sanitize_name(nickname)
custom_headers['authorization'] = 'Bot ' + settings.DISCORD_BOT_TOKEN custom_headers['authorization'] = 'Bot ' + settings.DISCORD_BOT_TOKEN
r = requests.put(path, headers=custom_headers, json=data) r = requests.put(path, headers=custom_headers, json=data)
logger.debug("Got status code %s after joining Discord server" % r.status_code) logger.debug("Got status code %s after joining Discord server" % r.status_code)
@ -219,22 +219,18 @@ class DiscordOAuthManager:
@staticmethod @staticmethod
@api_backoff @api_backoff
def update_nickname(user_id, nickname): def update_nickname(user_id, nickname):
try: nickname = DiscordOAuthManager._sanitize_name(nickname)
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)" % ( r.status_code, user_id, nickname))
r.status_code, user_id, nickname)) if r.status_code == 404:
if r.status_code == 404: logger.warn("Discord user ID %s could not be found in server." % user_id)
logger.warn("Discord user ID %s could not be found in server." % user_id)
return True
r.raise_for_status()
return True return True
except: r.raise_for_status()
logger.exception("Failed to set nickname for Discord user ID %s (%s)" % (user_id, nickname)) return True
return False
@staticmethod @staticmethod
def delete_user(user_id): def delete_user(user_id):

View File

@ -9,6 +9,7 @@ from requests.exceptions import HTTPError
from allianceauth.services.hooks import NameFormatter from allianceauth.services.hooks import NameFormatter
from .manager import DiscordOAuthManager, DiscordApiBackoff from .manager import DiscordOAuthManager, DiscordApiBackoff
from .models import DiscordUser from .models import DiscordUser
from allianceauth.services.tasks import QueueOnce
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -58,8 +59,8 @@ class DiscordTasks:
return True return True
@staticmethod @staticmethod
@shared_task(bind=True, name='discord.update_groups') @shared_task(bind=True, name='discord.update_groups', base=QueueOnce)
def update_groups(task_self, pk): def update_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)
if DiscordTasks.has_account(user): if DiscordTasks.has_account(user):
@ -70,7 +71,7 @@ class DiscordTasks:
except DiscordApiBackoff as bo: except DiscordApiBackoff as bo:
logger.info("Discord group sync API back off for %s, " logger.info("Discord group sync API back off for %s, "
"retrying in %s seconds" % (user, bo.retry_after_seconds)) "retrying in %s seconds" % (user, bo.retry_after_seconds))
raise task_self.retry(countdown=bo.retry_after_seconds) raise self.retry(countdown=bo.retry_after_seconds)
except HTTPError as e: except HTTPError as e:
if e.response.status_code == 404: if e.response.status_code == 404:
try: try:
@ -81,9 +82,9 @@ class DiscordTasks:
finally: finally:
raise e raise e
except Exception as e: except Exception as e:
if task_self: if self:
logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user) logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user)
raise task_self.retry(countdown=60 * 10) raise self.retry(countdown=60 * 10)
else: else:
# Rethrow # Rethrow
raise e raise e
@ -99,8 +100,8 @@ class DiscordTasks:
DiscordTasks.update_groups.delay(discord_user.user.pk) DiscordTasks.update_groups.delay(discord_user.user.pk)
@staticmethod @staticmethod
@shared_task(bind=True, name='discord.update_nickname') @shared_task(bind=True, name='discord.update_nickname', base=QueueOnce)
def update_nickname(task_self, pk): def update_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)
if DiscordTasks.has_account(user): if DiscordTasks.has_account(user):
@ -112,11 +113,11 @@ class DiscordTasks:
except DiscordApiBackoff as bo: except DiscordApiBackoff as bo:
logger.info("Discord nickname update API back off for %s, " logger.info("Discord nickname update API back off for %s, "
"retrying in %s seconds" % (user, bo.retry_after_seconds)) "retrying in %s seconds" % (user, bo.retry_after_seconds))
raise task_self.retry(countdown=bo.retry_after_seconds) raise self.retry(countdown=bo.retry_after_seconds)
except Exception as e: except Exception as e:
if task_self: if self:
logger.exception("Discord nickname sync failed for %s, retrying in 10 mins" % user) logger.exception("Discord nickname sync failed for %s, retrying in 10 mins" % user)
raise task_self.retry(countdown=60 * 10) raise self.retry(countdown=60 * 10)
else: else:
# Rethrow # Rethrow
raise e raise e

View File

@ -7,7 +7,7 @@ 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 GROUP_CACHE_MAX_AGE = getattr(settings, 'DISCOURSE_GROUP_CACHE_MAX_AGE', 2 * 60 * 60) # default 2 hours
class DiscourseError(Exception): class DiscourseError(Exception):
@ -23,7 +23,7 @@ class DiscourseError(Exception):
ENDPOINTS = { ENDPOINTS = {
'groups': { 'groups': {
'list': { 'list': {
'path': "/admin/groups.json", 'path': "/groups/search.json",
'method': 'get', 'method': 'get',
'args': { 'args': {
'required': [], 'required': [],

View File

@ -6,6 +6,7 @@ from celery import shared_task
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter from allianceauth.services.hooks import NameFormatter
from allianceauth.services.tasks import QueueOnce
from .manager import DiscourseManager from .manager import DiscourseManager
from .models import DiscourseUser from .models import DiscourseUser
@ -40,7 +41,7 @@ class DiscourseTasks:
return False return False
@staticmethod @staticmethod
@shared_task(bind=True, name='discourse.update_groups') @shared_task(bind=True, name='discourse.update_groups', base=QueueOnce)
def update_groups(self, pk): def update_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)

View File

@ -3,7 +3,7 @@ import logging
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from celery import shared_task from celery import shared_task
from allianceauth.services.tasks import QueueOnce
from .models import MumbleUser from .models import MumbleUser
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -26,7 +26,7 @@ class MumbleTasks:
MumbleUser.objects.all().delete() MumbleUser.objects.all().delete()
@staticmethod @staticmethod
@shared_task(bind=True, name="mumble.update_groups") @shared_task(bind=True, name="mumble.update_groups", base=QueueOnce)
def update_groups(self, pk): def update_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)

View File

@ -4,7 +4,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from allianceauth.notifications import notify from allianceauth.notifications import notify
from celery import shared_task from celery import shared_task
from allianceauth.services.tasks import QueueOnce
from allianceauth.services.modules.openfire.manager import OpenfireManager from allianceauth.services.modules.openfire.manager import OpenfireManager
from allianceauth.services.hooks import NameFormatter from allianceauth.services.hooks import NameFormatter
from .models import OpenfireUser from .models import OpenfireUser
@ -40,7 +40,7 @@ class OpenfireTasks:
OpenfireUser.objects.all().delete() OpenfireUser.objects.all().delete()
@staticmethod @staticmethod
@shared_task(bind=True, name="openfire.update_groups") @shared_task(bind=True, name="openfire.update_groups", base=QueueOnce)
def update_groups(self, pk): def update_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)

View File

@ -3,7 +3,7 @@ import logging
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from celery import shared_task from celery import shared_task
from allianceauth.services.tasks import QueueOnce
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter from allianceauth.services.hooks import NameFormatter
from .manager import Phpbb3Manager from .manager import Phpbb3Manager
@ -35,7 +35,7 @@ class Phpbb3Tasks:
return False return False
@staticmethod @staticmethod
@shared_task(bind=True, name="phpbb3.update_groups") @shared_task(bind=True, name="phpbb3.update_groups", base=QueueOnce)
def update_groups(self, pk): def update_groups(self, pk):
user = User.objects.get(pk=pk) user = User.objects.get(pk=pk)
logger.debug("Updating phpbb3 groups for user %s" % user) logger.debug("Updating phpbb3 groups for user %s" % user)

View File

@ -3,7 +3,7 @@ import logging
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from celery import shared_task from celery import shared_task
from allianceauth.services.tasks import QueueOnce
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter from allianceauth.services.hooks import NameFormatter
from .manager import SeatManager from .manager import SeatManager
@ -34,7 +34,7 @@ class SeatTasks:
return False return False
@staticmethod @staticmethod
@shared_task(bind=True) @shared_task(bind=True, name='seat.update_roles', base=QueueOnce)
def update_roles(self, pk): def update_roles(self, pk):
user = User.objects.get(pk=pk) user = User.objects.get(pk=pk)
logger.debug("Updating SeAT roles for user %s" % user) logger.debug("Updating SeAT roles for user %s" % user)

View File

@ -3,7 +3,7 @@ import logging
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from celery import shared_task from celery import shared_task
from allianceauth.services.tasks import QueueOnce
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter from allianceauth.services.hooks import NameFormatter
from .manager import SmfManager from .manager import SmfManager
@ -39,7 +39,7 @@ class SmfTasks:
SmfUser.objects.all().delete() SmfUser.objects.all().delete()
@staticmethod @staticmethod
@shared_task(bind=True, name="smf.update_groups") @shared_task(bind=True, name="smf.update_groups", base=QueueOnce)
def update_groups(self, pk): def update_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)

View File

@ -3,7 +3,7 @@ import logging
from django.contrib.auth.models import User from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from celery import shared_task from celery import shared_task
from allianceauth.services.tasks import QueueOnce
from allianceauth.notifications import notify from allianceauth.notifications import notify
from allianceauth.services.hooks import NameFormatter from allianceauth.services.hooks import NameFormatter
from .manager import Teamspeak3Manager from .manager import Teamspeak3Manager
@ -56,7 +56,7 @@ class Teamspeak3Tasks:
logger.info("Teamspeak3 disabled") logger.info("Teamspeak3 disabled")
@staticmethod @staticmethod
@shared_task(bind=True, name="teamspeak3.update_groups") @shared_task(bind=True, name="teamspeak3.update_groups", base=QueueOnce)
def update_groups(self, pk): def update_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)

View File

@ -10,11 +10,11 @@ module_urls = [
name='activate'), name='activate'),
url(r'^deactivate/$', views.deactivate_teamspeak3, url(r'^deactivate/$', views.deactivate_teamspeak3,
name='deactivate'), name='deactivate'),
url(r'reset_perm/$', views.reset_teamspeak3_perm, url(r'^reset_perm/$', views.reset_teamspeak3_perm,
name='reset_perm'), name='reset_perm'),
# Teamspeak Urls # Teamspeak Urls
url(r'verify/$', views.verify_teamspeak3, name='verify'), url(r'^verify/$', views.verify_teamspeak3, name='verify'),
] ]
urlpatterns = [ urlpatterns = [

View File

@ -36,7 +36,7 @@ class TS3Proto:
def connect(self, ip, port): def connect(self, ip, port):
try: try:
self._conn = telnetlib.Telnet(host=ip, port=port) self._conn = telnetlib.Telnet(host=ip, port=port, timeout=5)
self._connected = True self._connected = True
except: except:
# raise ConnectionError(ip, port) # raise ConnectionError(ip, port)

View File

@ -141,7 +141,7 @@ def pre_delete_user(sender, instance, *args, **kwargs):
@receiver(pre_save, sender=User) @receiver(pre_save, sender=User)
def pre_save_user(sender, instance, *args, **kwargs): def disable_services_on_inactive(sender, instance, *args, **kwargs):
logger.debug("Received pre_save from %s" % instance) logger.debug("Received pre_save from %s" % instance)
# check if user is being marked active/inactive # check if user is being marked active/inactive
if not instance.pk: if not instance.pk:
@ -154,3 +154,17 @@ def pre_save_user(sender, instance, *args, **kwargs):
disable_user(instance) disable_user(instance)
except User.DoesNotExist: except User.DoesNotExist:
pass pass
@receiver(pre_save, sender=UserProfile)
def disable_services_on_no_main(sender, instance, *args, **kwargs):
if not instance.pk:
# new model being created
return
try:
old_instance = UserProfile.objects.get(pk=instance.pk)
if old_instance.main_character and not instance.main_character:
logger.info("Disabling services due to loss of main character for user {0}".format(instance.user))
disable_user(instance.user)
except UserProfile.DoesNotExist:
pass

View File

@ -1,40 +1,39 @@
import logging import logging
import redis
from celery import shared_task from celery import shared_task
from django.contrib.auth.models import User from django.contrib.auth.models import User
from .hooks import ServicesHook from .hooks import ServicesHook
from celery_once import QueueOnce as BaseTask, AlreadyQueued
from celery_once.helpers import now_unix
from django.core.cache import cache
REDIS_CLIENT = redis.Redis()
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# http://loose-bits.com/2010/10/distributed-task-locking-in-celery.html class QueueOnce(BaseTask):
def only_one(function=None, key="", timeout=None): once = BaseTask.once
"""Enforce only one celery task at a time.""" once['graceful'] = True
def _dec(run_func):
"""Decorator."""
def _caller(*args, **kwargs): class DjangoBackend:
"""Caller.""" def __init__(self, settings):
ret_value = None pass
have_lock = False
lock = REDIS_CLIENT.lock(key, timeout=timeout)
try:
have_lock = lock.acquire(blocking=False)
if have_lock:
ret_value = run_func(*args, **kwargs)
finally:
if have_lock:
lock.release()
return ret_value @staticmethod
def raise_or_lock(key, timeout):
now = now_unix()
result = cache.get(key)
if result:
remaining = int(result) - now
if remaining > 0:
raise AlreadyQueued(remaining)
else:
cache.set(key, now + timeout, timeout)
return _caller @staticmethod
def clear_lock(key):
return _dec(function) if function is not None else _dec return cache.delete(key)
@shared_task(bind=True) @shared_task(bind=True)

View File

@ -33,6 +33,7 @@ class NameFormatterTestCase(TestCase):
corporation_ticker='TIKK', corporation_ticker='TIKK',
alliance_id='3456', alliance_id='3456',
alliance_name='alliance name', alliance_name='alliance name',
alliance_ticker='TIKR',
) )
self.member.profile.main_character = self.char self.member.profile.main_character = self.char
self.member.profile.save() self.member.profile.save()
@ -83,11 +84,15 @@ class NameFormatterTestCase(TestCase):
self.assertIn('alliance_name', result) self.assertIn('alliance_name', result)
self.assertEqual(result['alliance_name'], self.char.alliance_name) self.assertEqual(result['alliance_name'], self.char.alliance_name)
self.assertIn('alliance_ticker', result) self.assertIn('alliance_ticker', result)
self.assertEqual(result['alliance_ticker'], self.char.alliance.alliance_ticker) self.assertEqual(result['alliance_ticker'], self.char.alliance_ticker)
self.assertIn('alliance_id', result) self.assertIn('alliance_id', result)
self.assertEqual(result['alliance_id'], self.char.alliance_id) self.assertEqual(result['alliance_id'], self.char.alliance_id)
self.assertIn('username', result) self.assertIn('username', result)
self.assertEqual(result['username'], self.member.username) self.assertEqual(result['username'], self.member.username)
self.assertIn('alliance_or_corp_name', result)
self.assertEqual(result['alliance_or_corp_name'], self.char.alliance_name)
self.assertIn('alliance_or_corp_ticker', result)
self.assertEqual(result['alliance_or_corp_ticker'], self.char.alliance_ticker)
def test_format_name(self): def test_format_name(self):
config = NameFormatConfig.objects.create( config = NameFormatConfig.objects.create(

View File

@ -9,6 +9,7 @@ from allianceauth.authentication.models import State
class ServicesSignalsTestCase(TestCase): class ServicesSignalsTestCase(TestCase):
def setUp(self): def setUp(self):
self.member = AuthUtils.create_user('auth_member', disconnect_signals=True) self.member = AuthUtils.create_user('auth_member', disconnect_signals=True)
AuthUtils.add_main_character(self.member, 'Test', '1', '2', 'Test Corp', 'TEST')
self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True) self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True)
@mock.patch('allianceauth.services.signals.transaction') @mock.patch('allianceauth.services.signals.transaction')
@ -67,6 +68,18 @@ class ServicesSignalsTestCase(TestCase):
args, kwargs = disable_user.call_args args, kwargs = disable_user.call_args
self.assertEqual(self.member, args[0]) self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.disable_user')
def test_disable_services_on_loss_of_main_character(self, disable_user):
"""
Test a user set inactive has disable_member called
"""
self.member.profile.main_character = None
self.member.profile.save() # Signal Trigger
self.assertTrue(disable_user.called)
args, kwargs = disable_user.call_args
self.assertEqual(self.member, args[0])
@mock.patch('allianceauth.services.signals.transaction') @mock.patch('allianceauth.services.signals.transaction')
@mock.patch('allianceauth.services.signals.ServicesHook') @mock.patch('allianceauth.services.signals.ServicesHook')
def test_m2m_changed_group_permissions(self, services_hook, transaction): def test_m2m_changed_group_permissions(self, services_hook, transaction):

View File

@ -23,9 +23,9 @@ urlpatterns = [
name='mark_uncompleted'), name='mark_uncompleted'),
url(r'^request/remove/', views.srp_request_remove, url(r'^request/remove/', views.srp_request_remove,
name="request_remove"), name="request_remove"),
url(r'request/approve/', views.srp_request_approve, url(r'^request/approve/', views.srp_request_approve,
name='request_approve'), name='request_approve'),
url(r'request/reject/', views.srp_request_reject, url(r'^request/reject/', views.srp_request_reject,
name='request_reject'), name='request_reject'),
url(r'^request/(\w+)/update', views.srp_request_update_amount, url(r'^request/(\w+)/update', views.srp_request_update_amount,
name="request_update_amount"), name="request_update_amount"),

View File

@ -9,7 +9,7 @@ from allianceauth.eveonline.models import EveCharacter
from allianceauth.services.signals import m2m_changed_group_permissions, m2m_changed_user_permissions, \ from allianceauth.services.signals import m2m_changed_group_permissions, m2m_changed_user_permissions, \
m2m_changed_state_permissions m2m_changed_state_permissions
from allianceauth.services.signals import m2m_changed_user_groups, pre_save_user from allianceauth.services.signals import m2m_changed_user_groups, disable_services_on_inactive
class AuthUtils: class AuthUtils:
@ -90,7 +90,7 @@ class AuthUtils:
m2m_changed.disconnect(m2m_changed_group_permissions, sender=Group.permissions.through) m2m_changed.disconnect(m2m_changed_group_permissions, sender=Group.permissions.through)
m2m_changed.disconnect(m2m_changed_user_permissions, sender=User.user_permissions.through) m2m_changed.disconnect(m2m_changed_user_permissions, sender=User.user_permissions.through)
m2m_changed.disconnect(m2m_changed_state_permissions, sender=State.permissions.through) m2m_changed.disconnect(m2m_changed_state_permissions, sender=State.permissions.through)
pre_save.disconnect(pre_save_user, sender=User) pre_save.disconnect(disable_services_on_inactive, sender=User)
m2m_changed.disconnect(state_member_corporations_changed, sender=State.member_corporations.through) m2m_changed.disconnect(state_member_corporations_changed, sender=State.member_corporations.through)
m2m_changed.disconnect(state_member_characters_changed, sender=State.member_characters.through) m2m_changed.disconnect(state_member_characters_changed, sender=State.member_characters.through)
m2m_changed.disconnect(state_member_alliances_changed, sender=State.member_alliances.through) m2m_changed.disconnect(state_member_alliances_changed, sender=State.member_alliances.through)
@ -102,7 +102,7 @@ class AuthUtils:
m2m_changed.connect(m2m_changed_group_permissions, sender=Group.permissions.through) m2m_changed.connect(m2m_changed_group_permissions, sender=Group.permissions.through)
m2m_changed.connect(m2m_changed_user_permissions, sender=User.user_permissions.through) m2m_changed.connect(m2m_changed_user_permissions, sender=User.user_permissions.through)
m2m_changed.connect(m2m_changed_state_permissions, sender=State.permissions.through) m2m_changed.connect(m2m_changed_state_permissions, sender=State.permissions.through)
pre_save.connect(pre_save_user, sender=User) pre_save.connect(disable_services_on_inactive, sender=User)
m2m_changed.connect(state_member_corporations_changed, sender=State.member_corporations.through) m2m_changed.connect(state_member_corporations_changed, sender=State.member_corporations.through)
m2m_changed.connect(state_member_characters_changed, sender=State.member_characters.through) m2m_changed.connect(state_member_characters_changed, sender=State.member_characters.through)
m2m_changed.connect(state_member_alliances_changed, sender=State.member_alliances.through) m2m_changed.connect(state_member_alliances_changed, sender=State.member_alliances.through)

View File

@ -26,7 +26,7 @@ urlpatterns = [
# Authentication # Authentication
url(r'', include(allianceauth.authentication.urls)), url(r'', include(allianceauth.authentication.urls)),
url(r'^account/login/$', TemplateView.as_view(template_name='public/login.html'), name='auth_login_user'), url(r'^account/login/$', TemplateView.as_view(template_name='public/login.html'), name='auth_login_user'),
url(r'account/', include(hmac_urls)), url(r'^account/', include(hmac_urls)),
# Admin urls # Admin urls
url(r'^admin/', admin.site.urls), url(r'^admin/', admin.site.urls),

View File

@ -54,7 +54,7 @@ master_doc = 'index'
# General information about the project. # General information about the project.
project = u'Alliance Auth' project = u'Alliance Auth'
copyright = u'2017, Alliance Auth' copyright = u'2018, Alliance Auth'
author = u'R4stl1n' author = u'R4stl1n'
# The version info for the project you're documenting, acts as replacement for # The version info for the project you're documenting, acts as replacement for
@ -62,7 +62,7 @@ author = u'R4stl1n'
# built documents. # built documents.
# #
# The short X.Y version. # The short X.Y version.
version = u'1.14' version = u'2.0'
# The full version, including alpha/beta/rc tags. # The full version, including alpha/beta/rc tags.
# release = u'1.14.0' # release = u'1.14.0'
@ -156,7 +156,7 @@ man_pages = [
# dir menu entry, description, category) # dir menu entry, description, category)
texinfo_documents = [ texinfo_documents = [
(master_doc, 'AllianceAuth', u'Alliance Auth Documentation', (master_doc, 'AllianceAuth', u'Alliance Auth Documentation',
author, 'AllianceAuth', 'Alliance service auth to help large scale alliances manage services.', author, 'AllianceAuth', 'An auth system for EVE Online to help in-game organizations manage online service access.',
'Miscellaneous'), 'Miscellaneous'),
] ]

View File

@ -5,7 +5,7 @@ The documentation for Alliance Auth uses [Sphinx](http://www.sphinx-doc.org/) to
[readthedocs.org](https://readthedocs.org/). [readthedocs.org](https://readthedocs.org/).
Documentation was migrated from the Github wiki pages and into the repository to allow documentation changes to be Documentation was migrated from the GitHub wiki pages and into the repository to allow documentation changes to be
included with pull requests. This means that documentation can be guaranteed to be updated when a pull request is included with pull requests. This means that documentation can be guaranteed to be updated when a pull request is
accepted rather than hoping documentation is updated afterwards or relying on maintainers to do the work. It also accepted rather than hoping documentation is updated afterwards or relying on maintainers to do the work. It also
allows for documentation to be maintained at different versions more easily. allows for documentation to be maintained at different versions more easily.

View File

@ -49,7 +49,7 @@ This would register the ExampleService class which would need to be a subclass o
```eval_rst ```eval_rst
.. important:: .. important::
The hook **MUST** be registered in `yourservice.auth_hooks` along with any other hooks you are registering for Alliance Auth. The hook **MUST** be registered in ``yourservice.auth_hooks`` along with any other hooks you are registering for Alliance Auth.
``` ```
@ -97,7 +97,7 @@ Functions:
Internal name of the module, should be unique amongst modules. Internal name of the module, should be unique amongst modules.
#### self.urlpatterns #### self.urlpatterns
You should define all of your service urls internally, usually in `urls.py`. Then you can import them and set `self.urlpatterns` to your defined urlpatterns. You should define all of your service URLs internally, usually in `urls.py`. Then you can import them and set `self.urlpatterns` to your defined urlpatterns.
from . import urls from . import urls
... ...

View File

@ -24,12 +24,12 @@ When you create an autogroup config you will be given the following options:
After creating a group you wont be able to change the Corp and Alliance group prefixes, name source and the replace spaces settings. Make sure you configure these the way you want before creating the config. If you need to change these you will have to create a new autogroup config. After creating a group you wont be able to change the Corp and Alliance group prefixes, name source and the replace spaces settings. Make sure you configure these the way you want before creating the config. If you need to change these you will have to create a new autogroup config.
``` ```
- States selects which states will be added to automatic corp/alliance groups - States selects which states will be added to automatic Corp/Alliance groups
- Corp/Alliance groups checkbox toggles corp/alliance autogroups on or off for this config. - Corp/Alliance groups checkbox toggles Corp/Alliance autogroups on or off for this config.
- Corp/Alliance group prefix sets the prefix for the group name, e.g. if your corp was called `MyCorp` and your prefix was `Corp `, your autogroup name would be created as `Corp MyCorp`. This field accepts leading/trailing spaces. - Corp/Alliance group prefix sets the prefix for the group name, e.g. if your Corp was called `MyCorp` and your prefix was `Corp `, your autogroup name would be created as `Corp MyCorp`. This field accepts leading/trailing spaces.
- Corp/Alliance name source sets the source of the corp/alliance name used in creating the group name. Currently the options are Full name and Ticker. - Corp/Alliance name source sets the source of the Corp/Alliance name used in creating the group name. Currently the options are Full name and Ticker.
- Replace spaces allows you to replace spaces in the autogroup name with the value in the Replace spaces with field. This can be blank. - Replace spaces allows you to replace spaces in the autogroup name with the value in the Replace spaces with field. This can be blank.

View File

@ -1,6 +1,6 @@
# Corp Stats # Corp Stats
This module is used to check the registration status of corp members and to determine character relationships, being mains or alts. This module is used to check the registration status of Corp members and to determine character relationships, being mains or alts.
## Installation ## Installation
@ -10,13 +10,13 @@ Add `'allianceauth.corputils',` to your `INSTALLED_APPS` list in your auth proje
## Creating a Corp Stats ## Creating a Corp Stats
Upon initial install, nothing will be visible. For every corp, a model will have to be created before data can be viewed. Upon initial install, nothing will be visible. For every Corp, a model will have to be created before data can be viewed.
![nothing is visible](/_static/images/features/corpstats/blank_header.png) ![nothing is visible](/_static/images/features/corpstats/blank_header.png)
If you are a superuser, the add button will be immediate visible to you. If not, your user account requires the `add_corpstats` permission. If you are a superuser, the add button will be immediate visible to you. If not, your user account requires the `add_corpstats` permission.
Corp Stats requires an EVE SSO token to access data from the EVE Swagger Interface. Upon pressing the Add button, you will be prompted to authenticated. Please select the character who is in the corp you want data for. Corp Stats requires an EVE SSO token to access data from the EVE Swagger Interface. Upon pressing the Add button, you will be prompted to authenticated. Please select the character who is in the Corporation you want data for.
![authorize from the EVE site](/_static/images/features/corpstats/eve_sso_authorization.png) ![authorize from the EVE site](/_static/images/features/corpstats/eve_sso_authorization.png)
@ -33,7 +33,7 @@ If it fails an error message will be displayed.
![navigation bar](/_static/images/features/corpstats/navbar.png) ![navigation bar](/_static/images/features/corpstats/navbar.png)
This bar contains a dropdown menu of all available corps. If the user has the `add_corpstats` permission, a button to add a Corp Stats will be shown. This bar contains a dropdown menu of all available Corporations. If the user has the `add_corpstats` permission, a button to add a Corp Stats will be shown.
On the right of this bar is a search field. Press enter to search. It checks all characters in all Corp Stats you have view permission to and returns search results. On the right of this bar is a search field. Press enter to search. It checks all characters in all Corp Stats you have view permission to and returns search results.
@ -60,15 +60,15 @@ Each view contains a sortable and searchable table. The number of listings shown
![main list](/_static/images/features/corpstats/main_list.png) ![main list](/_static/images/features/corpstats/main_list.png)
This list contains all main characters in registered in the selected corporation and their alts. Each character has a link to [zKillboard](https://zkillboard.com). This list contains all main characters in registered in the selected Corporation and their alts. Each character has a link to [zKillboard](https://zkillboard.com).
#### Member List #### Member List
![member list](/_static/images/features/corpstats/member_list.png) ![member list](/_static/images/features/corpstats/member_list.png)
The list contains all characters in the corp. Red backgrounds means they are not registered in auth. A link to [zKillboard](https://zkillboard.com) is present for all characters. The list contains all characters in the Corporation. Red backgrounds means they are not registered in auth. A link to [zKillboard](https://zkillboard.com) is present for all characters.
If registered, the character will also have a main character, main corporation, and main alliance field. If registered, the character will also have a main character, main Corporation, and main Alliance field.
#### Unregistered List #### Unregistered List
@ -80,7 +80,7 @@ This list contains all characters not registered on auth. Each character has a l
![search results](/_static/images/features/corpstats/search_view.png) ![search results](/_static/images/features/corpstats/search_view.png)
This view is essentially the same as the Corp Stats page, but not specific to a single corp. This view is essentially the same as the Corp Stats page, but not specific to a single Corporation.
The search query is visible in the search box. The search query is visible in the search box.
Characters from all Corp Stats to which the user has view access will be displayed. APIs respect permissions. Characters from all Corp Stats to which the user has view access will be displayed. APIs respect permissions.
@ -108,7 +108,7 @@ To use this feature, users will require some of the following:
``` ```
Users who add a Corp Stats with their token will be granted permissions to view it regardless of the above permissions. View permissions are interpreted in the "OR" sense: a user can view their corp's Corp Stats without the `view_corp_corpstats` permission if they have the `view_alliance_corpstats` permission, same idea for their state. Note that these evaluate against the user's main character. Users who add a Corp Stats with their token will be granted permissions to view it regardless of the above permissions. View permissions are interpreted in the "OR" sense: a user can view their Corporations's Corp Stats without the `view_corp_corpstats` permission if they have the `view_alliance_corpstats` permission, same idea for their state. Note that these evaluate against the user's main character.
## Automatic Updating ## Automatic Updating
By default Corp Stats are only updated on demand. If you want to automatically refresh on a schedule, add an entry to your project's settings file: By default Corp Stats are only updated on demand. If you want to automatically refresh on a schedule, add an entry to your project's settings file:
@ -116,7 +116,7 @@ By default Corp Stats are only updated on demand. If you want to automatically r
CELERYBEAT_SCHEDULE['update_all_corpstats'] = { CELERYBEAT_SCHEDULE['update_all_corpstats'] = {
'task': 'allianceauth.corputils.tasks.update_all_corpstats', 'task': 'allianceauth.corputils.tasks.update_all_corpstats',
'schedule': crontab(minute=0, hour="*/6"), 'schedule': crontab(minute=0, hour="*/6"),
}, }
Adjust the crontab as desired. Adjust the crontab as desired.
@ -126,12 +126,12 @@ Adjust the crontab as desired.
>Unrecognized corporation. Please ensure it is a member of the alliance or a blue. >Unrecognized corporation. Please ensure it is a member of the alliance or a blue.
Corp Stats can only be created for corporations who have a model in the database. These only exist for tenant corps, Corp Stats can only be created for Corporations who have a model in the database. These only exist for tenant corps,
corps of tenant alliances, blue corps, and members of blue alliances. corps of tenant alliances, blue corps, and members of blue alliances.
>Selected corp already has a statistics module. >Selected corp already has a statistics module.
Only one Corp Stats may exist at a time for a given corporation. Only one Corp Stats may exist at a time for a given Corporation.
>Failed to gather corporation statistics with selected token. >Failed to gather corporation statistics with selected token.
@ -147,7 +147,7 @@ This occurs when the SSO token is invalid, which can occur when deleted by the u
>CorpStats for (corp name) cannot update with your ESI token as you have left corp. >CorpStats for (corp name) cannot update with your ESI token as you have left corp.
The SSO token's character is no longer in the corp which the Corp Stats is for, and therefore membership data cannot be retrieved. The SSO token's character is no longer in the Corporation which the Corp Stats is for, and therefore membership data cannot be retrieved.
>HTTPForbidden >HTTPForbidden

View File

@ -11,7 +11,7 @@ Additional settings are required. Append the following settings to the end of yo
FLEETUP_API_ID = '' # The API id from http://fleet-up.com/Api/MyKeys FLEETUP_API_ID = '' # The API id from http://fleet-up.com/Api/MyKeys
FLEETUP_GROUP_ID = '' # The id of the group you want to pull data from, see http://fleet-up.com/Api/Endpoints#groups_mygroupmemberships FLEETUP_GROUP_ID = '' # The id of the group you want to pull data from, see http://fleet-up.com/Api/Endpoints#groups_mygroupmemberships
Once filled out restart gunicorn and celery. Once filled out restart Gunicorn and Celery.
## Permissions ## Permissions

View File

@ -8,15 +8,15 @@ Add `'allianceauth.hrapplications',` to your `INSTALLED_APPS` list in your auth
### Creating Forms ### Creating Forms
The most common task is creating ApplicationForm models for corps. Only when such models exist will a corp be listed as a choice for applicants. This occurs in the django admin site, so only staff have access. The most common task is creating ApplicationForm models for corps. Only when such models exist will a Corporation be listed as a choice for applicants. This occurs in the Django admin site, so only staff have access.
The first step is to create questions. This is achieved by creating ApplicationQuestion models, one for each question. Titles are not unique. The first step is to create questions. This is achieved by creating ApplicationQuestion models, one for each question. Titles are not unique.
Next step is to create the actual ApplicationForm model. It requires an existing EveCorporationInfo model to which it will belong. It also requires the selection of questions. ApplicationForm models are unique per corp: only one may exist for any given corp concurrently. Next step is to create the actual ApplicationForm model. It requires an existing EveCorporationInfo model to which it will belong. It also requires the selection of questions. ApplicationForm models are unique per Corporation: only one may exist for any given Corporation concurrently.
You can adjust these questions at any time. This is the preferred method of modifying the form: deleting and recreating will cascade the deletion to all received applications from this form which is usually not intended. You can adjust these questions at any time. This is the preferred method of modifying the form: deleting and recreating will cascade the deletion to all received applications from this form which is usually not intended.
Once completed the corp will be available to receive applications. Once completed the Corporation will be available to receive applications.
### Reviewing Applications ### Reviewing Applications
@ -63,11 +63,11 @@ This is the model representation of a question. It contains a title, and a field
### ApplicationForm ### ApplicationForm
This is the template for an application. It points at a corp, with only one form allowed per corp. It also points at ApplicationQuestion models. When a user creates an application, they will be prompted with each question the form includes at the given time. Modifying questions in a form after it has been created will not be reflected in existing applications, so it's perfectly fine to adjust them as you see fit. Changing corps however is not advisable, as existing applications will point at the wrong corp after they've been submitted, confusing reviewers. This is the template for an application. It points at a Corporation, with only one form allowed per Corporation. It also points at ApplicationQuestion models. When a user creates an application, they will be prompted with each question the form includes at the given time. Modifying questions in a form after it has been created will not be reflected in existing applications, so it's perfectly fine to adjust them as you see fit. Changing Corporations however is not advisable, as existing applications will point at the wrong Corporation after they've been submitted, confusing reviewers.
### Application ### Application
This is the model representation of a completed application. It references an ApplicationForm from which it was spawned which is where the corp specificity comes from. It points at a user, contains info regarding its reviewer, and has a status. Shortcut properties also provide the applicant's main character, the applicant's APIs, and a string representation of the reviewer (for cases when the reviewer doesn't have a main character or the model gets deleted). This is the model representation of a completed application. It references an ApplicationForm from which it was spawned which is where the Corporation specificity comes from. It points at a user, contains info regarding its reviewer, and has a status. Shortcut properties also provide the applicant's main character, the applicant's APIs, and a string representation of the reviewer (for cases when the reviewer doesn't have a main character or the model gets deleted).
### ApplicationResponse ### ApplicationResponse
@ -81,7 +81,7 @@ This is a reviewer's comment on an application. Points at the application, point
### No corps accepting applications ### No corps accepting applications
Ensure there are ApplicationForm models in the admin site. Ensure the user does not already have an application to these corps. If the users wishes to re-apply they must first delete their completed application Ensure there are ApplicationForm models in the admin site. Ensure the user does not already have an application to these Corporations. If the users wishes to re-apply they must first delete their completed application
### Reviewer unable to complete application ### Reviewer unable to complete application

View File

@ -5,7 +5,7 @@
New in 2.0 New in 2.0
``` ```
Each service's username or nickname, depending on which the service supports, can be customised through the use of the Name Formatter Config provided the service supports custom formats. This config can be found in the admin panel under **Services -> Name format config** Each service's username or nickname, depending on which the service supports, can be customised through the use of the Name Formatter config provided the service supports custom formats. This config can be found in the admin panel under **Services -> Name format config**
Currently the following services support custom name formats: Currently the following services support custom name formats:
@ -37,7 +37,7 @@ Currently the following services support custom name formats:
```eval_rst ```eval_rst
.. note:: .. note::
It's important to note here, before we get into what you can do with a name formatter, that before the generated name is passed off to the service to create an account it will be sanitised to remove characters (the letters and numbers etc) that the service cannot support. This means that, despite what you configured, the service may display something different. It is up to you to test your formatter and understand how your format may be disrupted by a certain services sanitisation function. It's important to note here, before we get into what you can do with a name formatter, that before the generated name is passed off to the service to create an account it will be sanitised to remove characters (the letters and numbers etc.) that the service cannot support. This means that, despite what you configured, the service may display something different. It is up to you to test your formatter and understand how your format may be disrupted by a certain services sanitisation function.
``` ```
## Available format data ## Available format data
@ -53,12 +53,14 @@ The following fields are available from a users account and main character:
- `alliance_id` - `alliance_id`
- `alliance_name` - `alliance_name`
- `alliance_ticker` - `alliance_ticker`
- `alliance_or_corp_name` (defaults to Corporation name if there is no Alliance)
- `alliance_or_corp_ticker` (defaults to Corporation ticker if there is no Alliance)
## Building a formatter string ## Building a formatter string
The name formatter uses the advanced string formatting specified by [PEP-3101](https://www.python.org/dev/peps/pep-3101/). Anything supported by this specification is supported in a name formatter. The name formatter uses the advanced string formatting specified by [PEP-3101](https://www.python.org/dev/peps/pep-3101/). Anything supported by this specification is supported in a name formatter.
A more digestable documentation of string formatting in Python is available on the [PyFormat](https://pyformat.info/) website. A more digestible documentation of string formatting in Python is available on the [PyFormat](https://pyformat.info/) website.
Some examples of strings you could use: Some examples of strings you could use:
```eval_rst ```eval_rst

View File

@ -1,10 +1,5 @@
# Permissions Auditing # Permissions Auditing
```eval_rst
.. note::
New in 1.15
```
Access to most of Alliance Auth's features are controlled by Django's permissions system. In order to help you secure your services, Alliance Auth provides a permissions auditing tool. Access to most of Alliance Auth's features are controlled by Django's permissions system. In order to help you secure your services, Alliance Auth provides a permissions auditing tool.
## Installation ## Installation

View File

@ -2,7 +2,7 @@
## Overview ## Overview
In Alliance Auth v1 admins were able to define which corporations and alliances were to be considered "members" with full permissions and "blues" with restricted permissions. The state system is the replacement for these static definitions: admins can now create as many states as desired, as well as extend membership to specific characters. In Alliance Auth v1 admins were able to define which Corporations and Alliances were to be considered "members" with full permissions and "blues" with restricted permissions. The state system is the replacement for these static definitions: admins can now create as many states as desired, as well as extend membership to specific characters.
## Creating a State ## Creating a State
States are created through your installation's admin site. Upon install three states are created for you: `Member`, `Blue`, and `Guest`. New ones can be created like any other Django model by users with the appropriate permission (`authentication | state | Can add state`) or superusers. States are created through your installation's admin site. Upon install three states are created for you: `Member`, `Blue`, and `Guest`. New ones can be created like any other Django model by users with the appropriate permission (`authentication | state | Can add state`) or superusers.
@ -27,10 +27,10 @@ Checking this box means this state is available to all users. There isn't much u
This lets you select which characters the state is available to. Characters can be added by selecting the green plus icon. This lets you select which characters the state is available to. Characters can be added by selecting the green plus icon.
### Member Corporations ### Member Corporations
This lets you select which corporations the state is available to. Corporations can be added by selecting the green plus icon. This lets you select which Corporations the state is available to. Corporations can be added by selecting the green plus icon.
### Member Alliances ### Member Alliances
This lets you select which alliances the state is available to. Alliances can be added by selecting the gree plus icon. This lets you select which Alliances the state is available to. Alliances can be added by selecting the green plus icon.
## Determining a User's State ## Determining a User's State
States are mutually exclusive, meaning a user can only be in one at a time. States are mutually exclusive, meaning a user can only be in one at a time.
@ -45,5 +45,3 @@ Assigned states are visible in the `Users` section of the `Authentication` admin
If no states are available to a user's main character, or their account has been deactivated, they are assigned to a catch-all `Guest` state. This state cannot be deleted nor can its name be changed. If no states are available to a user's main character, or their account has been deactivated, they are assigned to a catch-all `Guest` state. This state cannot be deleted nor can its name be changed.
The `Guest` state allows permissions to be granted to users who would otherwise not get any. For example access to public services can be granted by giving the `Guest` state a service access permission. The `Guest` state allows permissions to be granted to users who would otherwise not get any. For example access to public services can be granted by giving the `Guest` state a service access permission.

View File

@ -2,12 +2,12 @@
```eval_rst ```eval_rst
.. tip:: .. tip::
If you are uncomfortable with linux permissions follow the steps below as the root user. Some commands do not behave the same when run with sudo. If you are uncomfortable with Linux permissions follow the steps below as the root user.
``` ```
## Dependencies ## Dependencies
Alliance Auth can be installed on any operating system. Dependencies are provided below for two of the most popular server platforms, Ubuntu and CentOS. To install on your favourite flavour of linux, identify and install equivalent packages to the ones listed here. Alliance Auth can be installed on any operating system. Dependencies are provided below for two of the most popular server platforms, Ubuntu and CentOS. To install on your favourite flavour of Linux, identify and install equivalent packages to the ones listed here.
```eval_rst ```eval_rst
.. hint:: .. hint::
@ -31,15 +31,20 @@ CentOS:
### Database ### Database
It's recommended to use a database service instead of sqlite. Many options are available, but this guide will use MariaDB. It's recommended to use a database service instead of SQLite. Many options are available, but this guide will use MariaDB.
Ubuntu: Ubuntu:
apt-get install mariadb-server mysql-client libmysqlclient-dev apt-get install mariadb-server mariadb-client libmysqlclient-dev
CentOS: CentOS:
yum install mariadb-server mariadb-devel mariadb yum install mariadb-server mariadb-devel mariadb-shared mariadb
```eval_rst
.. note::
If you don't plan on running the database on the same server as auth you still need to install the libmysqlclient-dev package on Ubuntu or mariadb-devel package on CentOS.
```
### Redis and Other Tools ### Redis and Other Tools
@ -55,7 +60,7 @@ CentOS:
```eval_rst ```eval_rst
.. important:: .. important::
CentOS: Make sure redis is running before continuing. :: CentOS: Make sure Redis is running before continuing. ::
systemctl enable redis.service systemctl enable redis.service
systemctl start redis.service systemctl start redis.service
@ -71,12 +76,6 @@ Alliance Auth needs a MySQL user account and database. Open an SQL shell with `m
Close the SQL shell and secure your database server with the `mysql_secure_installation` command. Close the SQL shell and secure your database server with the `mysql_secure_installation` command.
If you're updating from v1, populate this database with a copy of the data from your v1 database.
mysqldump -u root -p v1_database_name_here | mysql -u root -p alliance_auth
Note this command will prompt you for the root password twice.
## Auth Install ## Auth Install
### User Account ### User Account
@ -99,7 +98,7 @@ Create a Python virtual environment and put it somewhere convenient (e.g. `/home
```eval_rst ```eval_rst
.. warning:: .. warning::
The python3 command may not be available on all installations. Try a specific version such as python3.6 if this is the case. The python3 command may not be available on all installations. Try a specific version such as ``python3.6`` if this is the case.
``` ```
```eval_rst ```eval_rst
@ -112,14 +111,14 @@ Activate the virtualenv using `source /home/allianceserver/venv/auth/bin/activat
```eval_rst ```eval_rst
.. hint:: .. hint::
Each time you come to do maintenance on your Alliance Auth installation, you should activate your virtual environment first. When finished, deactivate it with the 'deactivate' command. Each time you come to do maintenance on your Alliance Auth installation, you should activate your virtual environment first. When finished, deactivate it with the ``deactivate`` command.
``` ```
Ensure wheel is available with `pip install wheel` before continuing. Ensure wheel is available with `pip install wheel` before continuing.
### Alliance Auth Project ### Alliance Auth Project
You can install the library using `pip install allianceauth`. This will install Alliance Auth and all its python dependencies. You should also install gunicorn with `pip install gunicorn` before proceeding. You can install the library using `pip install allianceauth`. This will install Alliance Auth and all its python dependencies. You should also install Gunicorn with `pip install gunicorn` before proceeding.
Now you need to create the application that will run the Alliance Auth install. Ensure you are in the allianceserver home directory by issuing `cd /home/allianceserver`. Now you need to create the application that will run the Alliance Auth install. Ensure you are in the allianceserver home directory by issuing `cd /home/allianceserver`.
@ -148,13 +147,13 @@ And finally ensure the allianceserver user has read/write permissions to this di
### Gunicorn ### Gunicorn
To run the auth website a [WSGI Server](https://www.fullstackpython.com/wsgi-servers.html) is required. [Gunicorn](http://gunicorn.org/) is highly recommended for its ease of configuring. It can be manually run with `gunicorn myauth.wsgi` or automatically run using supervisor. To run the auth website a [WSGI Server](https://www.fullstackpython.com/wsgi-servers.html) is required. [Gunicorn](http://gunicorn.org/) is highly recommended for its ease of configuring. It can be manually run from within your `myauth` base directory with `gunicorn --bind 0.0.0.0 myauth.wsgi` or automatically run using Supervisor.
The default configuration is good enough for most installations. Additional information is available in the [gunicorn](gunicorn.md) doc. The default configuration is good enough for most installations. Additional information is available in the [gunicorn](gunicorn.md) doc.
### Supervisor ### Supervisor
[Supervisor](http://supervisord.org/) is a process watchdog service: it makes sure other processes are started automatically and kept running. It can be used to automatically start the WSGI server and celery workers for background tasks. Installation varies by OS: [Supervisor](http://supervisord.org/) is a process watchdog service: it makes sure other processes are started automatically and kept running. It can be used to automatically start the WSGI server and Celery workers for background tasks. Installation varies by OS:
Ubuntu: Ubuntu:
@ -166,7 +165,7 @@ CentOS:
systemctl enable supervisord.service systemctl enable supervisord.service
systemctl start supervisord.service systemctl start supervisord.service
Once installed it needs a configuration file to know which processes to watch. Your Alliance Auth project comes with a ready-to-use template which will ensure the celery workers, celery task scheduler and gunicorn are all running. Once installed it needs a configuration file to know which processes to watch. Your Alliance Auth project comes with a ready-to-use template which will ensure the Celery workers, Celery task scheduler and Gunicorn are all running.
Ubuntu: Ubuntu:
@ -182,7 +181,7 @@ You can check the status of the processes with `supervisorctl status`. Logs from
```eval_rst ```eval_rst
.. note:: .. note::
Any time the code or your settings change you'll need to restart gunicorn and celery. :: Any time the code or your settings change you'll need to restart Gunicorn and Celery. ::
supervisorctl restart myauth: supervisorctl restart myauth:
``` ```
@ -209,4 +208,4 @@ Some releases come with changes to settings: update your project's settings with
Some releases come with new or changed models. Update your database to reflect this with `python /home/allianceserver/myauth/manage.py migrate`. Some releases come with new or changed models. Update your database to reflect this with `python /home/allianceserver/myauth/manage.py migrate`.
Always restart celery and gunicorn after updating. Always restart Celery and Gunicorn after updating.

View File

@ -24,7 +24,7 @@ Apache needs to be able to read the folder containing your auth project's static
Apache serves sites through defined virtual hosts. These are located in `/etc/apache2/sites-available/` on Ubuntu and `/etc/httpd/conf.d/httpd.conf` on CentOS. Apache serves sites through defined virtual hosts. These are located in `/etc/apache2/sites-available/` on Ubuntu and `/etc/httpd/conf.d/httpd.conf` on CentOS.
A virtual host for auth need only proxy requests to your WSGI server (gunicorn if you followed the install guide) and serve static files. Examples can be found below. Create your config in its own file eg `myauth.conf`. A virtual host for auth need only proxy requests to your WSGI server (Gunicorn if you followed the install guide) and serve static files. Examples can be found below. Create your config in its own file e.g. `myauth.conf`
### Ubuntu ### Ubuntu
@ -34,7 +34,7 @@ To proxy and modify headers a few mods need to be enabled.
a2enmod proxy_http a2enmod proxy_http
a2enmod headers a2enmod headers
Create a new config file for auth eg `/etc/apache2/sites-available/myauth.conf` and fill out the virtual host configuration. To enable your config use `a2ensite myauth.conf` and then reload apache with `service apache2 reload`. Create a new config file for auth e.g. `/etc/apache2/sites-available/myauth.conf` and fill out the virtual host configuration. To enable your config use `a2ensite myauth.conf` and then reload apache with `service apache2 reload`.
### CentOS ### CentOS

View File

@ -10,12 +10,12 @@ Check out the full [Gunicorn docs](http://docs.gunicorn.org/en/latest/index.html
```eval_rst ```eval_rst
.. note:: .. note::
If you're using a virtual environment (and I would encourage you to do so when hosting Alliance Auth), activate it now. `source /path/to/venv/bin/activate`. If you're using a virtual environment, activate it now. ``source /path/to/venv/bin/activate``.
``` ```
Install Gunicorn using pip, `pip install gunicorn`. Install Gunicorn using pip, `pip install gunicorn`.
In your `myauth` base directory, try running `gunicorn --bind 0.0.0.0:8000 myauth.wsgi`. You should be able to browse to http://yourserver:8000 and see your Alliance Auth installation running. Images and styling will be missing, but dont worry, your web server will provide them. In your `myauth` base directory, try running `gunicorn --bind 0.0.0.0:8000 myauth.wsgi`. You should be able to browse to http://yourserver:8000 and see your Alliance Auth installation running. Images and styling will be missing, but don't worry, your web server will provide them.
Once you validate its running, you can kill the process with Ctrl+C and continue. Once you validate its running, you can kill the process with Ctrl+C and continue.
@ -65,7 +65,7 @@ e.g. `command=/path/to/venv/bin/gunicorn myauth.wsgi`
### Starting via Supervisor ### Starting via Supervisor
Once you have your configuration all sorted, you will need to reload your supervisor config `sudo service supervisor reload` and then you can start the Gunicorn server via `sudo supervisorctl start aauth-gunicorn` (or whatever you renamed it to). You should see something like the following `aauth-gunicorn: started`. If you get some other message, you'll need to consult the Supervisor log files, usually found in `/var/log/supervisor/`. Once you have your configuration all sorted, you will need to reload your supervisor config `service supervisor reload` and then you can start the Gunicorn server via `supervisorctl start aauth-gunicorn` (or whatever you renamed it to). You should see something like the following `aauth-gunicorn: started`. If you get some other message, you'll need to consult the Supervisor log files, usually found in `/var/log/supervisor/`.
## Configuring your webserver ## Configuring your webserver
@ -76,4 +76,4 @@ Any web server capable of proxy passing should be able to sit in front of Gunico
## Restarting Gunicorn ## Restarting Gunicorn
In the past when you made changes you restarted the entire Apache server. This is no longer required. When you update or make configuration changes that ask you to restart Apache, instead you can just restart Gunicorn: In the past when you made changes you restarted the entire Apache server. This is no longer required. When you update or make configuration changes that ask you to restart Apache, instead you can just restart Gunicorn:
`sudo supervisorctl restart myauth-gunicorn`, or the service name you chose for it. `supervisorctl restart myauth-gunicorn`, or the service name you chose for it.

View File

@ -4,6 +4,7 @@
.. toctree:: .. toctree::
allianceauth allianceauth
upgradev1
gunicorn gunicorn
nginx nginx
apache apache

View File

@ -1,6 +1,6 @@
# NGINX # NGINX
## Overivew ## Overview
Nginx (engine x) is a HTTP server known for its high performance, stability, simple configuration, and low resource consumption. Unlike traditional servers (i.e. Apache), Nginx doesn't rely on threads to serve requests, rather using an asynchronous event driven approach which permits predictable resource usage and performance under load. Nginx (engine x) is a HTTP server known for its high performance, stability, simple configuration, and low resource consumption. Unlike traditional servers (i.e. Apache), Nginx doesn't rely on threads to serve requests, rather using an asynchronous event driven approach which permits predictable resource usage and performance under load.
@ -12,7 +12,7 @@ You can read more about NGINX on the [NGINX wiki](https://www.nginx.com/resource
If you're converting from Apache, here are some things to consider. If you're converting from Apache, here are some things to consider.
Nginx is lightweight for a reason. It doesn't try to do everything internally and instead concentrates on just being a good HTTP server. This means that, unlike Apache, it wont automatically run PHP scripts via mod_php and doesn't have an internal WSGI server like mod_wsgi. That doesn't mean that it can't, just that it relies on external processes to run these instead. This might be good or bad depending on your outlook. It's good because it allows you to segment your applications, restarting Alliance Auth wont impact your PHP applications. On the other hand it means more config and more management of services. For some people it will be worth it, for others losing the centralised nature of Apache may not be worth it. Nginx is lightweight for a reason. It doesn't try to do everything internally and instead concentrates on just being a good HTTP server. This means that, unlike Apache, it won't automatically run PHP scripts via mod_php and doesn't have an internal WSGI server like mod_wsgi. That doesn't mean that it can't, just that it relies on external processes to run these instead. This might be good or bad depending on your outlook. It's good because it allows you to segment your applications, restarting Alliance Auth wont impact your PHP applications. On the other hand it means more config and more management of services. For some people it will be worth it, for others losing the centralised nature of Apache may not be worth it.
```eval_rst ```eval_rst
+-----------+----------------------------------------+ +-----------+----------------------------------------+
@ -25,13 +25,20 @@ Nginx is lightweight for a reason. It doesn't try to do everything internally an
``` ```
Your .htaccess files wont work. Nginx has a separate way of managing access to folders via the server config. Everything you can do with htaccess files you can do with Nginx config. [Read more on the Nginx wiki](https://www.nginx.com/resources/wiki/start/topics/examples/likeapache-htaccess/) Your .htaccess files won't work. Nginx has a separate way of managing access to folders via the server config. Everything you can do with htaccess files you can do with Nginx config. [Read more on the Nginx wiki](https://www.nginx.com/resources/wiki/start/topics/examples/likeapache-htaccess/)
## Setting up Nginx ## Setting up Nginx
Install Nginx via your preferred package manager or other method. If you need help just search, there are plenty of guides on installing Nginx out there. Install Nginx via your preferred package manager or other method. If you need help just search, there are plenty of guides on installing Nginx out there.
Nginx needs to be able to read the folder containing your auth project's static files. On Ubuntu: `chown -R nginx:nginx /var/www/myauth/static`, and on CentOS: `chown -R nginx:nginx /var/www/myauth/static` Nginx needs to be able to read the folder containing your auth project's static files. `chown -R nginx:nginx /var/www/myauth/static`.
```eval_rst
.. tip::
Some specific distros may use www-data:www-data instead of nginx:nginx, causing static files (images, stylesheets etc) not to appear. You can confirm what user Nginx will run under by checking either its base config file `/etc/nginx/nginx.conf` for the "user" setting, or once Nginx has started `ps aux | grep nginx`.
Adjust your chown commands to the correct user if needed.
..
```
You will need to have [Gunicorn](gunicorn.md) or some other WSGI server setup for hosting Alliance Auth. You will need to have [Gunicorn](gunicorn.md) or some other WSGI server setup for hosting Alliance Auth.
@ -57,7 +64,7 @@ server {
location = /favicon.ico { access_log off; log_not_found off; } location = /favicon.ico { access_log off; log_not_found off; }
location /static/ { location /static {
alias /var/www/myauth/static; alias /var/www/myauth/static;
autoindex off; autoindex off;
} }

View File

@ -0,0 +1,85 @@
# Upgrading from v1.15
It's possible to preserve a v1 install's database and migrate it to v2. This will retain all service accounts, user accounts with their main character, but will purge API keys and alts.
## Preparing to Upgrade
Ensure your auth install is at least version 1.15 - it's preferred to be on v1.15.8 but any v1.15.x should work. If not, update following normal procedures and ensure you run migrations.
If you will not be using any apps (or they have been removed in v2) clear their database tables before beginning the v2 install procedure. From within your v1 install, `python manage.py migrate APPNAME zero`, replacing `APPNAME` with the appropriate name.
It's strongly encouraged to perform the upgrade with a **copy** of the original v1 database. If something goes wrong this allows you to roll back and try again. This can be achieved with:
mysqldump -u root -p v1_database_name_here | mysql -u root -p v2_database_name_here
Note this command will prompt you for the root password twice.
Alliance Auth v2 requires Python 3.4 or newer. If this is not available on your system be sure to install the [dependencies listed.](allianceauth.md#python)
## Installation
Follow the [normal install procedures for Alliance Auth v2](allianceauth.md). If you're coming from v1 you likely already have the user account and dependencies.
When configuring settings, ensure to enter the database information relating to the copy you made of the v1 database, but **do not run migrations**. See below before continuing.
Do not start Celery yet.
## Migration
During the upgrade process a migration is run which ports data to the new system. Its behaviour can be configured through some optional settings.
### User Accounts
A database migration is included to port users to the new SSO-based login system. It will automatically assign states and main characters.
Password for non-staff accounts are destroyed so they cannot be used to log in - only SSO is available for regular members. Staff can login using username and password through the admin site or by SSO.
### EVE Characters
Character ownership is now tracked by periodically validating a refreshable SSO token. When migrating from v1 not all users may have such a refreshable token: to allow these users to retain their existing user account their main character is set to the one present in v1, bypassing this validation mechanism.
It is essential to get your users to log in via SSO soon after migration to ensure their accounts are managed properly. Any user who does not log in will not lose their main character under any circumstance, even character sale. During a sale characters are transferred to an NPC corp - get celery tasks running within 24 hours of migration so that during a sale the user loses their state (and hence permissions). This can be an issue if you've granted service access permissions to a user or their groups: it's strongly encouraged to grant service access by state from now on.
Because character ownership is tracked separately of main character it is not possible to preserve alts unless a refreshable SSO token is present for them.
### Members and Blues
The new [state system](../../features/states.md) allows configuring dynamic membership states through the admin page. Unfortunately if you make a change after migrating it will immediately assess user states and see that no one should be a member. You can add additional settings to your auth project's settings file to generate the member and blue states as you have them defined in v1:
- `ALLIANCE_IDS = []` a list of member alliance IDs
- `CORP_IDS = []` a list of member corporation IDs
- `BLUE_ALLIANCE_IDS = []` a list of blue alliance IDs
- `BLUE_CORP_IDS = []` a list of blue corporation IDs
Put comma-separated IDs into the brackets and the migration will create states with the members and blues you had before. This will prevent unexpected state purging when you edit states via the admin site.
### Default Groups
If you used member/blue group names other than the standard "Member" and "Blue" you can enter settings to have the member/blue states created through this migration take these names.
- `DEFAULT_AUTH_GROUP = ""` the desired name of the "Member" state
- `DEFAULT_BLUE_GROUP = ""` the desired name of the "Blue" state
Any permissions assigned to these groups will be copied to the state replacing them. Because these groups are no longer managed they pose a security risk and so are deleted at the end of the migration automatically.
### Run Migrations
Once you've configured any optional settings it is now safe to run the included migrations.
## Validating Upgrade
Before starting the Celery workers it's a good idea to validate the states were created and assigned correctly. Any mistakes now will trigger deletion of service accounts: if Celery workers aren't running these tasks aren't yet processed. States can be checked through the admin site.
The site (and not Celery) can be started with `supervisorctl start myauth:gunicorn`. Then navigate to the admin site and log in with your v1 username and password. States and User Profiles can be found under the Authentication app.
Once you have confirmed states are correctly configured (or adjusted them as needed) clear the Celery task queue: from within your auth project folder, `celery -A myauth purge`
It should now be safe to bring workers online (`supervisorctl start myauth:`) and invite users to use the site.
## Securing User Accounts
After migration users will have main characters without a method to check for character sales. After a brief period to allow your users to log in (and provide a refreshable token to use), it's a good idea to clear main characters of users who haven't yet logged in. This can be achieved with an included command: `python manage.py checkmains`
A similar process can be used to ensure users who may have lost service permissions during the migration do not have active service accounts. This is done using `python manage.py validate_service_accounts`.
## Help
If something goes wrong during the migration reach out for help on [Gitter](https://gitter.im/R4stl1n/allianceauth) or open an [issue](https://github.com/allianceauth/allianceauth/issues).

View File

@ -1,6 +1,8 @@
# Discord # Discord
## Overview ## Overview
Discord is a web-based instant messaging client with voice. Kind of like teamspeak meets slack meets skype. It also has a standalone app for phones and desktop. Discord is a web-based instant messaging client with voice. Kind of like TeamSpeak meets Slack meets Skype. It also has a standalone app for phones and desktop.
Discord is very popular amongst ad-hoc small groups and larger organizations seeking a modern technology. Alternative voice communications should be investigated for larger than small-medium groups for more advanced features.
## Setup ## Setup
@ -19,20 +21,19 @@ In your auth project's settings file, do the following:
DISCORD_SYNC_NAMES = False DISCORD_SYNC_NAMES = False
### Creating a Server ### Creating a Server
*If you already have a Discord server, skip the creation step, but be sure to retrieve the server ID*
Navigate to the [Discord site](https://discordapp.com/) and register an account, or log in if you have one already. Navigate to the [Discord site](https://discordapp.com/) and register an account, or log in if you have one already.
On the left side of the screen youll see a circle with a plus sign. This is the button to create a new server. Go ahead and do that, naming it something obvious. On the left side of the screen youll see a circle with a plus sign. This is the button to create a new server. Go ahead and do that, naming it something obvious.
Now retrieve the server ID from the URL of the page youre on. The ID is the first of the very long numbers. For instance my testing servers url look like: Now retrieve the server ID [following this procedure.](https://support.discordapp.com/hc/en-us/articles/206346498-Where-can-I-find-my-User-Server-Message-ID-)
https://discordapp.com/channels/120631096835571712/120631096835571712
with a server ID of `120631096835571712`
Update your auth project's settings file, inputting the server ID as `DISCORD_GUILD_ID` Update your auth project's settings file, inputting the server ID as `DISCORD_GUILD_ID`
```eval_rst
.. note::
If you already have a Discord server skip the creation step, but be sure to retrieve the server ID
```
### Registering an Application ### Registering an Application
Navigate to the [Discord Developers site.](https://discordapp.com/developers/applications/me) Press the plus sign to create a new application. Navigate to the [Discord Developers site.](https://discordapp.com/developers/applications/me) Press the plus sign to create a new application.
@ -49,22 +50,27 @@ Update your auth project's settings file with these pieces of information from t
- From the App Bot Users panel, `DISCORD_BOT_TOKEN` is the Token - From the App Bot Users panel, `DISCORD_BOT_TOKEN` is the Token
### Preparing Auth ### Preparing Auth
Before continuing it is essential to run migrations and restart gunicorn and celery. Before continuing it is essential to run migrations and restart Gunicorn and Celery.
### Adding a Bot to the Server ### Adding a Bot to the Server
Once created, navigate to the services page of your AllianceAuth install as the superuser account. At the top there is a big green button labelled Link Discord Server. Click it, then from the drop down select the server you created, and then Authorize. Once created, navigate to the services page of your Alliance Auth install as the superuser account. At the top there is a big green button labelled Link Discord Server. Click it, then from the drop down select the server you created, and then Authorize.
This adds a new user to your Discord server with a `BOT` tag, and a new role with the same name as your Discord application. Don't touch either of these. If for some reason the bot loses permissions or is removed from the server, click this button again. This adds a new user to your Discord server with a `BOT` tag, and a new role with the same name as your Discord application. Don't touch either of these. If for some reason the bot loses permissions or is removed from the server, click this button again.
To manage roles, this bot role must be at the top of the hierarchy. Edit your Discord server, roles, and click and drag the role with the same name as your application to the top of the list. This role must stay at the top of the list for the bot to work. Finally, the owner of the bot account must enable 2 Factor Authentication (this is required from discord for kicking and modifying member roles). If you are unsure what 2FA is or how to set it up, refer to [this support page](https://support.discordapp.com/hc/en-us/articles/219576828). It is also recommended to force 2fa on your server (this forces any admins or moderators to have 2fa enabled to perform similar functions on discord). To manage roles, this bot role must be at the top of the hierarchy. Edit your Discord server, roles, and click and drag the role with the same name as your application to the top of the list. This role must stay at the top of the list for the bot to work. Finally, the owner of the bot account must enable 2 Factor Authentication (this is required from Discord for kicking and modifying member roles). If you are unsure what 2FA is or how to set it up, refer to [this support page](https://support.discordapp.com/hc/en-us/articles/219576828). It is also recommended to force 2FA on your server (this forces any admins or moderators to have 2fa enabled to perform similar functions on discord).
Note that the bot will never appear online as it does not participate in chat channels. Note that the bot will never appear online as it does not participate in chat channels.
### Linking Accounts ### Linking Accounts
Instead of the usual account creation procedure, for Discord to work we need to link accounts to AllianceAuth. When attempting to enable the Discord service, users are redirected to the official Discord site to authenticate. They will need to create an account if they don't have one prior to continuing. Upon authorization, users are redirected back to AllianceAuth with an OAuth code which is used to join the Discord server. Instead of the usual account creation procedure, for Discord to work we need to link accounts to Alliance Auth. When attempting to enable the Discord service, users are redirected to the official Discord site to authenticate. They will need to create an account if they don't have one prior to continuing. Upon authorization, users are redirected back to Alliance Auth with an OAuth code which is used to join the Discord server.
### Syncing Nicknames ### Syncing Nicknames
If you want users to have their Discord nickname changed to their in-game character name, set `DISCORD_SYNC_NAMES` to `True` If you want users to have their Discord nickname changed to their in-game character name, set `DISCORD_SYNC_NAMES` to `True`
## Managing Roles ## Managing Roles
Once users link their accounts youll notice Roles get populated on Discord. These are the equivalent to Groups on every other service. The default permissions should be enough for members to chat and use comms. Add more permissions to the roles as desired through the server management window. Once users link their accounts youll notice Roles get populated on Discord. These are the equivalent to Groups on every other service. The default permissions should be enough for members to use text and audio communications. Add more permissions to the roles as desired through the server management window.
## Troubleshooting
### "Unknown Error" on Discord site when activating service
This indicates your callback URL doesn't match. Ensure the `DISCORD_CALLBACK_URL` setting exactly matches the URL entered on the Discord developers site. This includes http(s), trailing slash, etc.

View File

@ -17,31 +17,25 @@ In your auth project's settings file, do the following:
wget -qO- https://get.docker.io/ | sh wget -qO- https://get.docker.io/ | sh
### Get docker permissions
sudo usermod -aG docker allianceserver
Logout, then back in for changes to take effect.
## Install Discourse ## Install Discourse
### Download Discourse ### Download Discourse
sudo mkdir /var/discourse mkdir /var/discourse
sudo git clone https://github.com/discourse/discourse_docker.git /var/discourse git clone https://github.com/discourse/discourse_docker.git /var/discourse
### Configure ### Configure
cd /var/discourse cd /var/discourse
sudo cp samples/standalone.yml containers/app.yml cp samples/standalone.yml containers/app.yml
sudo nano containers/app.yml nano containers/app.yml
Change the following: Change the following:
- `DISCOURSE_DEVELOPER_EMAILS` should be a list of admin account email addresses separated by commas - `DISCOURSE_DEVELOPER_EMAILS` should be a list of admin account email addresses separated by commas.
- `DISCOUSE_HOSTNAME` should be 127.0.0.1 - `DISCOUSE_HOSTNAME` should be `discourse.example.com` or something similar.
- Everything with `SMTP` depends on your mail settings. Account created through auth do not require email validation, so to ignore everything email (NOT RECOMMENDED), just change the SMTP address to something random so it'll install. Note that not setting up email means any password resets emails won't be sent, and auth cannot reset these. [There are plenty of free email services online recommended by Discourse.](https://github.com/discourse/discourse/blob/master/docs/INSTALL-email.md#recommended-email-providers-for-discourse) - Everything with `SMTP` depends on your mail settings. [There are plenty of free email services online recommended by Discourse](https://github.com/discourse/discourse/blob/master/docs/INSTALL-email.md#recommended-email-providers-for-discourse) if you haven't set one up for auth already.
To install behind apache, look for this secion: To install behind Apache/Nginx, look for this section:
... ...
## which TCP/IP ports should this container expose? ## which TCP/IP ports should this container expose?
@ -61,26 +55,26 @@ Or any other port will do, if taken. Remember this number.
### Build and launch ### Build and launch
sudo nano /etc/default/docker nano /etc/default/docker
Uncomment this line: Uncomment this line:
DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4" DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4"
Restart docker: Restart Docker:
sudo service docker restart service docker restart
Now build: Now build:
sudo ./launcher bootstrap app ./launcher bootstrap app
sudo ./launcher start app ./launcher start app
## Web Server Configuration ## Web Server Configuration
You will need to configure your web server to proxy requests to Discourse. You will need to configure your web server to proxy requests to Discourse.
A minimal apache config might look like: A minimal Apache config might look like:
<VirtualHost *:80> <VirtualHost *:80>
ServerName discourse.example.com ServerName discourse.example.com
@ -88,7 +82,7 @@ A minimal apache config might look like:
ProxyPassReverse / http://0.0.0.0:7890/ ProxyPassReverse / http://0.0.0.0:7890/
</VirtualHost> </VirtualHost>
A minimal nginx config might look like: A minimal Nginx config might look like:
server { server {
listen 80; listen 80;
@ -103,7 +97,7 @@ A minimal nginx config might look like:
### Generate admin account ### Generate admin account
From the /var/discourse folder, From the `/var/discourse` directory,
./launcher enter app ./launcher enter app
rake admin:create rake admin:create
@ -128,4 +122,4 @@ Navigate to `discourse.example.com` and log in. Back to the admin site, scroll d
Save, now set `DISCOURSE_SSO_SECRET` in your auth project's settings file to the secure key you just put in Discourse. Save, now set `DISCOURSE_SSO_SECRET` in your auth project's settings file to the secure key you just put in Discourse.
Finally run migrations and restart gunicorn and celery. Finally run migrations and restart Gunicorn and Celery.

View File

@ -1,7 +1,7 @@
# Alliance Market # Alliance Market
## Dependencies ## Dependencies
Alliance Market requires php installed in your web server. Apache has `mod_php`, NGINX requires `php-fpm`. Alliance Market requires PHP installed in your web server. Apache has `mod_php`, NGINX requires `php-fpm`.
## Prepare Your Settings ## Prepare Your Settings
In your auth project's settings file, do the following: In your auth project's settings file, do the following:
@ -21,37 +21,37 @@ In your auth project's settings file, do the following:
} }
## Setup Alliance Market ## Setup Alliance Market
Alliance Market needs a database. Create one in mysql. Default name is `alliance_market`: Alliance Market needs a database. Create one in MySQL/MariaDB. Default name is `alliance_market`:
mysql -u root -p mysql -u root -p
create database alliance_market; create database alliance_market;
grant all privileges on alliance_market . * to 'allianceserver'@'localhost'; grant all privileges on alliance_market . * to 'allianceserver'@'localhost';
exit; exit;
To clone the repo, install packages: Install required packages to clone the repository:
sudo apt-get install mercurial meld apt-get install mercurial meld
Change to the web folder: Change to the web folder:
cd /var/www cd /var/www
Now clone the repo Now clone the repository
sudo hg clone https://bitbucket.org/krojew/evernus-alliance-market hg clone https://bitbucket.org/krojew/evernus-alliance-market
Make cache and log directories Make cache and log directories
sudo mkdir evernus-alliance-market/app/cache mkdir evernus-alliance-market/app/cache
sudo mkdir evernus-alliance-market/app/logs mkdir evernus-alliance-market/app/logs
sudo chmod -R 777 evernus-alliance-market/app/cache chmod -R 777 evernus-alliance-market/app/cache
sudo chmod -R 777 evernus-alliance-market/app/logs chmod -R 777 evernus-alliance-market/app/logs
Change ownership to apache Change ownership to apache
sudo chown -R www-data:www-data evernus-alliance-market chown -R www-data:www-data evernus-alliance-market
Enter Enter directory
cd evernus-alliance-market cd evernus-alliance-market
@ -61,13 +61,13 @@ Set environment variable
Copy configuration Copy configuration
sudo cp app/config/parameters.yml.dist app/config/parameters.yml cp app/config/parameters.yml.dist app/config/parameters.yml
Edit, changing the following: Edit, changing the following:
- `database_name` to `alliance_market` - `database_name` to `alliance_market`
- `database_user` to your MySQL user (usually `allianceserver`) - `database_user` to your MySQL user (usually `allianceserver`)
- `database_password` to your MySQL user password - `database_password` to your MySQL user password
- email settings, eg gmail - email settings, eg Gmail/Mailgun etc.
Edit `app/config/config.yml` and add the following: Edit `app/config/config.yml` and add the following:
@ -79,29 +79,29 @@ Install composer [as per these instructions.](https://getcomposer.org/download/)
Update dependencies. Update dependencies.
sudo php composer.phar update --optimize-autoloader php composer.phar update --optimize-autoloader
Prepare the cache: Prepare the cache:
sudo php app/console cache:clear --env=prod --no-debug php app/console cache:clear --env=prod --no-debug
Dump assets: Dump assets:
sudo php app/console assetic:dump --env=prod --no-debug php app/console assetic:dump --env=prod --no-debug
Create DB entries Create DB entries
sudo php app/console doctrine:schema:update --force php app/console doctrine:schema:update --force
Install SDE: Install SDE:
sudo php app/console evernus:update:sde php app/console evernus:update:sde
Configure your web server to serve alliance market. Configure your web server to serve alliance market.
A minimal apache config might look like: A minimal Apache config might look like:
<VirtualHost *:80> <VirtualHost *:80>
ServerName market.example.com ServerName market.example.com
@ -113,7 +113,7 @@ A minimal apache config might look like:
</Directory> </Directory>
</VirtualHost> </VirtualHost>
A minimal nginx config might look like: A minimal Nginx config might look like:
server { server {
listen 80; listen 80;
@ -122,23 +122,40 @@ A minimal nginx config might look like:
index app.php; index app.php;
access_log /var/logs/market.access.log; access_log /var/logs/market.access.log;
location ~ \.php$ { # strip app.php/ prefix if it is present
try_files $uri =404; rewrite ^/app\.php/?(.*)$ /$1 permanent;
fastcgi_pass unix:/tmp/php.socket;
fastcgi_index index.php; location / {
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; index app.php;
try_files $uri @rewriteapp;
}
location @rewriteapp {
rewrite ^(.*)$ /app.php/$1 last;
}
# pass the PHP scripts to FastCGI server from upstream phpfcgi
location ~ ^/(app|app_dev|config)\.php(/|$) {
fastcgi_pass 127.0.0.1:9000;
fastcgi_split_path_info ^(.+\.php)(/.*)$;
include fastcgi_params; include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param HTTPS off;
}
location ~ /\.ht {
deny all;
} }
} }
Once again, set cache permissions: Once again, set cache permissions:
sudo chown -R www-data:www-data app/ chown -R www-data:www-data app/
Add a user account through auth, then make it a superuser: Add a user account through auth, then make it a superuser:
sudo php app/console fos:user:promote your_username --super php app/console fos:user:promote your_username --super
Now edit your auth project's settings file and fill in the web URL to your market as well as the database details. Now edit your auth project's settings file and fill in the web URL to your market as well as the database details.
Finally run migrations and restart gunicorn and celery. Finally run migrations and restart Gunicorn and Celery.

View File

@ -10,53 +10,54 @@ In your auth project's settings file, do the following:
MUMBLE_URL = "" MUMBLE_URL = ""
## Overview ## Overview
Mumble is a free voice chat server. While not as flashy as teamspeak, it has all the functionality and is easier to customize. And is better. I may be slightly biased. Mumble is a free voice chat server. While not as flashy as TeamSpeak, it has all the functionality and is easier to customize. And is better. I may be slightly biased.
## Dependencies ## Dependencies
The mumble server package can be retrieved from a repository we need to add, mumble/release. The mumble server package can be retrieved from a repository we need to add, mumble/release.
sudo apt-add-repository ppa:mumble/release apt-add-repository ppa:mumble/release
sudo apt-get update apt-get update
Now two packages need to be installed: Now two packages need to be installed:
sudo apt-get install python-software-properties mumble-server apt-get install python-software-properties mumble-server
Download the appropriate authenticator release from https://github.com/allianceauth/mumble-authenticator and install the python dependencies for it: Download the appropriate authenticator release from [the authenticator repository](https://github.com/allianceauth/mumble-authenticator) and install the python dependencies for it:
pip install -r requirements.txt pip install -r requirements.txt
## Configuring Mumble ## Configuring Mumble
Mumble ships with a configuration file that needs customization. By default its located at /etc/mumble-server.ini. Open it with your favourite text editor: Mumble ships with a configuration file that needs customization. By default its located at /etc/mumble-server.ini. Open it with your favourite text editor:
sudo nano /etc/mumble-server.ini nano /etc/mumble-server.ini
REQUIRED: To enable the ICE authenticator, edit the following: REQUIRED: To enable the ICE authenticator, edit the following:
- `icesecretwrite=MY_CLEVER_PASSWORD`, obviously choosing a secure password - `icesecretwrite=MY_CLEVER_PASSWORD`, obviously choosing a secure password
- ensure the line containing `Ice="tcp -h 127.0.0.1 -p 6502"` is uncommented
By default mumble operates on sqlite which is fine, but slower than a dedicated MySQL server. To customize the database, edit the following: By default mumble operates on SQLite which is fine, but slower than a dedicated MySQL server. To customize the database, edit the following:
- uncomment the database line, and change it to `database=alliance_mumble` - uncomment the database line, and change it to `database=alliance_mumble`
- `dbDriver=QMYSQL` - `dbDriver=QMYSQL`
- `dbUsername=allianceserver` or whatever you called the AllianceAuth MySQL user - `dbUsername=allianceserver` or whatever you called the Alliance Auth MySQL user
- `dbPassword=` that users password - `dbPassword=` that users password
- `dbPort=3306` - `dbPort=3306`
- `dbPrefix=murmur_` - `dbPrefix=murmur_`
To name your root channel, uncomment and set `registerName=` to whatever cool name you want To name your root channel, uncomment and set `registerName=` to whatever cool name you want
Save and close the file (control + O, control + X). Save and close the file.
To get mumble superuser account credentials, run the following: To get Mumble superuser account credentials, run the following:
sudo dpkg-reconfigure mumble-server dpkg-reconfigure mumble-server
Set the password to something youll remember and write it down. This is needed to manage ACLs. Set the password to something youll remember and write it down. This is needed to manage ACLs.
Now restart the server to see the changes reflected. Now restart the server to see the changes reflected.
sudo service mumble-server restart service mumble-server restart
Thats it! Your server is ready to be connected to at example.com:64738 Thats it! Your server is ready to be connected to at example.com:64738
@ -80,14 +81,25 @@ Test your configuration by starting it: `python authenticator.py`
## Running the Authenticator ## Running the Authenticator
The authenticator needs to be running 24/7 to validate users on Mumble. You should check the [supervisor docs](../auth/supervisor.md) on how to achieve this. The authenticator needs to be running 24/7 to validate users on Mumble. This can be achieved by adding a section to your auth project's supervisor config file like the following example:
```
[program:authenticator]
command=/path/to/venv/bin/python authenticator.py
directory=/path/to/authenticator/directory/
user=allianceserver
stdout_logfile=/path/to/authenticator/directory/authenticator.log
stderr_logfile=/path/to/authenticator/directory/authenticator.log
autostart=true
autorestart=true
startsecs=10
priority=998
```
Note that groups will only be created on Mumble automatically when a user joins who is in the group. Note that groups will only be created on Mumble automatically when a user joins who is in the group.
## Making and Managing Channels
ACL is really above the scope of this guide. Once AllianceAuth creates your groups, go ahead and follow one of the wonderful web guides available on how to set up channel ACL properly.
## Prepare Auth ## Prepare Auth
In your project's settings file, set `MUMBLE_URL` to the public address of your mumble server. Do not include any leading `http://` or `mumble://`. In your project's settings file, set `MUMBLE_URL` to the public address of your mumble server. Do not include any leading `http://` or `mumble://`.
Run migrations and restart gunicorn and celery to complete setup. Run migrations and restart Gunicorn and Celery to complete setup.

View File

@ -1,6 +1,6 @@
# Openfire # Openfire
Openfire is a jabber (XMPP) server. Openfire is a Jabber (XMPP) server.
## Prepare Your Settings ## Prepare Your Settings
- Add `'allianceauth.services.modules.openfire',` to your `INSTALLED_APPS` list - Add `'allianceauth.services.modules.openfire',` to your `INSTALLED_APPS` list
@ -17,42 +17,43 @@
BROADCAST_USER_PASSWORD = "" BROADCAST_USER_PASSWORD = ""
BROADCAST_SERVICE_NAME = "broadcast" BROADCAST_SERVICE_NAME = "broadcast"
## Overview
Openfire is a java-based xmpp server (jabber).
## Dependencies ## Dependencies
One additional package is required - openjdk8 Openfire require a Java 8 runtime environment.
Ubuntu: Ubuntu:
sudo add-apt-repository ppa:webupd8team/java -y apt-get install openjdk-8-jdk
sudo apt-get update
sudo apt-get install oracle-java8-installer
CentOS: CentOS:
sudo yum -y install java-1.8.0-openjdk java-1.8.0-openjdk-devel yum -y install java-1.8.0-openjdk java-1.8.0-openjdk-devel
## Setup ## Setup
### Download Installer ### Download Installer
Openfire is not available through repositories so we need to get a debian from the developer. Openfire is not available through repositories so we need to get a package from the developer.
On your PC, naviage to the [Ignite Realtime downloads section](https://www.igniterealtime.org/downloads/index.jsp), and under Openfire select Linux, click on the debian file (2nd from bottom of list, ends with .deb). On your PC, navigate to the [Ignite Realtime downloads section](https://www.igniterealtime.org/downloads/index.jsp), and under Openfire select Linux, click on the Ubuntu: Debian package (second from bottom of list, ends with .deb) or CentOS: RPM Package (no JRE bundled, as we have installed it on the host)
Retrieve the file location by copying the url from the “click here” link. Retrieve the file location by copying the URL from the “click here” link, depending on your browser you may have a Copy Link or similar option in your right click menu.
In the console, ensure youre in your users home directory: `cd ~` In the console, ensure youre in your users home directory: `cd ~`
Now download the package. Replace the link below with the link you got earlier. Now download the package. Replace the link below with the link you got earlier.
wget https://www.igniterealtime.org/downloadServlet?filename=openfire/openfire_4.1.1_all.deb wget https://www.igniterealtime.org/downloadServlet?filename=openfire/openfire_4.2.3_all.deb
Now install from the debian. Replace the filename with your file name (the last part of the download url is the file name) Now install from the package. Replace the filename with your filename (the last part of the download URL is the file name)
sudo dpkg -i openfire_4.1.1_all.deb Ubuntu:
dpkg -i openfire_4.2.3_all.deb
CentOS:
yum install -y openfire-4.2.3-1.noarch.rpm
### Create Database ### Create Database
Performance is best when working from a SQL database. If you installed MySQL or MariaDB alongside your auth project, go ahead and create a database for openfire: Performance is best when working from a SQL database. If you installed MySQL or MariaDB alongside your auth project, go ahead and create a database for Openfire:
mysql -u root -p mysql -u root -p
create database alliance_jabber; create database alliance_jabber;
@ -62,7 +63,7 @@ Performance is best when working from a SQL database. If you installed MySQL or
### Web Configuration ### Web Configuration
The remainder of the setup occurs through Openfires web interface. Navigate to http://example.com:9090, or if youre behind CloudFlare, go straight to your servers IP:9090. The remainder of the setup occurs through Openfires web interface. Navigate to http://example.com:9090, or if youre behind CloudFlare, go straight to your servers IP:9090.
Select your language. I sure hope its english if youre reading this guide. Select your language. I sure hope its English if youre reading this guide.
Under Server Settings, set the Domain to `example.com` replacing it with your actual domain. Dont touch the rest. Under Server Settings, set the Domain to `example.com` replacing it with your actual domain. Dont touch the rest.
@ -100,7 +101,7 @@ Select `Enabled`, and `Secret Key Auth`. Update your auth project's settings wit
Navigate to the `Users/Groups` tab and select `Create New User` from the left navigation bar. Navigate to the `Users/Groups` tab and select `Create New User` from the left navigation bar.
Pick a username (eg `broadcast`) and password for your ping user. Enter these in your auth project's settings file as `BROADCAST_USER` and `BROADCAST_USER_PASSWORD`. Note that `BROADCAST_USER` needs to be in the format `user@example.com` matching your jabber server name. Press `Create User` to save this user. Pick a username (e.g. `broadcast`) and password for your ping user. Enter these in your auth project's settings file as `BROADCAST_USER` and `BROADCAST_USER_PASSWORD`. Note that `BROADCAST_USER` needs to be in the format `user@example.com` matching your jabber server name. Press `Create User` to save this user.
Broadcasting requires a plugin. Navigate to the `plugins` tab, press the green plus for the `Broadcast` plugin. Broadcasting requires a plugin. Navigate to the `plugins` tab, press the green plus for the `Broadcast` plugin.
@ -117,7 +118,7 @@ If you have troubles getting broadcasts to work, you can try setting the optiona
### Preparing Auth ### Preparing Auth
Once all settings are entered, run migrations and restart gunicorn and celery. Once all settings are entered, run migrations and restart Gunicorn and Celery.
### Group Chat ### Group Chat
Channels are available which function like a chat room. Access can be controlled either by password or ACL (not unlike mumble). Channels are available which function like a chat room. Access can be controlled either by password or ACL (not unlike mumble).

View File

@ -1,14 +1,10 @@
# Service Permissions # Service Permissions
```eval_rst
.. note::
New in 1.15
```
In the past, access to services was dictated by a list of settings in `settings.py`, granting access to each particular service for Members and/or Blues. This meant that granting access to a service was very broad and rigidly structured around these two states. In the past, access to services was dictated by a list of settings in `settings.py`, granting access to each particular service for Members and/or Blues. This meant that granting access to a service was very broad and rigidly structured around these two states.
## Permissions based access ## Permissions based access
Instead of granting access to services by the previous rigid structure, access to services is now granted by the built in Django permissions system. This means that service access can be more granular, allowing only certain groups, for instance Corp CEOs, or even individual user access to each enabled service. Instead of granting access to services by the previous rigid structure, access to services is now granted by the built in Django permissions system. This means that service access can be more granular, allowing only certain states, certain groups, for instance Corp CEOs, or even individual user access to each enabled service.
```eval_rst ```eval_rst
.. important:: .. important::
@ -34,4 +30,4 @@ If a user no longer has permission to access the service when this permissions c
### Disabling user accounts ### Disabling user accounts
When you unset a user as active in the admin panel, all of that users service accounts will be immediately disabled or removed. This is due to the built in behaviour of Djangos permissions system, which will return False for all permissions if a users account is disabled, regardless of their actual permissions state. When you unset a user as active in the admin panel, all of that users service accounts will be immediately disabled or removed. This is due to the built in behaviour of the Django permissions system, which will return False for all permissions if a users account is disabled, regardless of their actual permissions state.

View File

@ -1,12 +1,10 @@
# phpBB3 # phpBB3
and run migrations before continuing with this guide to ensure the service is installed.
## Overview ## Overview
phpBB is a free php-based forum. Its the default forum for AllianceAuth. phpBB is a free PHP-based forum.
## Dependencies ## Dependencies
PHPBB3 requires php installed in your web server. Apache has `mod_php`, NGINX requires `php-fpm`. See [the official guide](https://www.phpbb.com/community/docs/INSTALL.html) for php package requirements. phpBB3 requires PHP installed in your web server. Apache has `mod_php`, NGINX requires `php-fpm`. See [the official guide](https://www.phpbb.com/community/docs/INSTALL.html) for PHP package requirements.
## Prepare Your Settings ## Prepare Your Settings
In your auth project's settings file, do the following: In your auth project's settings file, do the following:
@ -27,7 +25,7 @@ In your auth project's settings file, do the following:
## Setup ## Setup
### Prepare the Database ### Prepare the Database
Create a database to install phpbb3 in. Create a database to install phpBB3 in.
mysql -u root -p mysql -u root -p
create database alliance_forum; create database alliance_forum;
@ -36,12 +34,12 @@ Create a database to install phpbb3 in.
Edit your auth project's settings file and fill out the `DATABASES['phpbb3']` part. Edit your auth project's settings file and fill out the `DATABASES['phpbb3']` part.
### Download Phpbb3 ### Download phpBB3
phpBB is available as a zip from their website. Navigate to the websites [downloads section](https://www.phpbb.com/downloads/) using your PC browser and copy the URL for the latest version zip. phpBB3 is available as a zip from their website. Navigate to the websites [downloads section](https://www.phpbb.com/downloads/) using your PC browser and copy the URL for the latest version zip.
In the console, navigate to your users home directory: `cd ~` In the console, navigate to your users home directory: `cd ~`
Now download using wget, replacing the url with the url for the package you just retrieved Now download using wget, replacing the URL with the URL for the package you just retrieved
wget https://www.phpbb.com/files/release/phpBB-3.2.2.zip wget https://www.phpbb.com/files/release/phpBB-3.2.2.zip
@ -51,16 +49,23 @@ This needs to be unpackaged. Unzip it, replacing the file name with that of the
Now we need to move this to our web directory. Usually `/var/www/forums`. Now we need to move this to our web directory. Usually `/var/www/forums`.
sudo mv phpBB3 /var/www/forums mv phpBB3 /var/www/forums
The web server needs read/write permission to this folder The web server needs read/write permission to this folder
sudo chown -R www-data:www-data /var/www/forums Apache: `chown -R www-data:www-data /var/www/forums`
Nginx: `chown -R nginx:nginx /var/www/forums`
```eval_rst
.. tip::
Nginx: Some distributions use the ``www-data:www-data`` user:group instead of ``nginx:nginx``. If you run into problems with permissions try it instead.
..
```
### Configuring Web Server ### Configuring Web Server
You will need to configure you web server to serve PHPBB3 before proceeding with installation. You will need to configure you web server to serve PHPBB3 before proceeding with installation.
A minimal apache config file might look like: A minimal Apache config file might look like:
<VirtualHost *:80> <VirtualHost *:80>
ServerName forums.example.com ServerName forums.example.com
@ -71,7 +76,7 @@ A minimal apache config file might look like:
</Directory> </Directory>
</VirtualHost> </VirtualHost>
A minimal nginx config file might look like: A minimal Nginx config file might look like:
server { server {
listen 80; listen 80;
@ -117,7 +122,7 @@ Under Database Settings, set the following:
If you use a table prefix other than the standard `phpbb_` you need to add an additional setting to your auth project's settings file, `PHPBB3_TABLE_PREFIX = ''`, and enter the prefix. If you use a table prefix other than the standard `phpbb_` you need to add an additional setting to your auth project's settings file, `PHPBB3_TABLE_PREFIX = ''`, and enter the prefix.
You should see `Succesful Connection` and proceed. You should see `Successful Connection` and proceed.
Enter administrator credentials on the next page. Enter administrator credentials on the next page.
@ -128,7 +133,7 @@ phpBB will then write its own config file.
### Open the Forums ### Open the Forums
Before users can see the forums, we need to remove the install directory Before users can see the forums, we need to remove the install directory
sudo rm -rf /var/www/forums/install rm -rf /var/www/forums/install
### Enabling Avatars ### Enabling Avatars
AllianceAuth sets user avatars to their character portrait when the account is created or password reset. We need to allow external URLs for avatars for them to behave properly. Navigate to the admin control panel for phpbb3, and under the `General` tab, along the left navigation bar beneath `Board Configuration`, select `Avatar Settings`. Set `Enable Remote Avatars` to `Yes` and then `Submit`. AllianceAuth sets user avatars to their character portrait when the account is created or password reset. We need to allow external URLs for avatars for them to behave properly. Navigate to the admin control panel for phpbb3, and under the `General` tab, along the left navigation bar beneath `Board Configuration`, select `Avatar Settings`. Set `Enable Remote Avatars` to `Yes` and then `Submit`.
@ -140,4 +145,4 @@ You can allow members to overwrite the portrait with a custom image if desired.
![location of change avatar setting](/_static/images/installation/services/phpbb3/avatar_permissions.png) ![location of change avatar setting](/_static/images/installation/services/phpbb3/avatar_permissions.png)
### Prepare Auth ### Prepare Auth
Once settings have been configured, run migrations and restart gunicorn and celery. Once settings have been configured, run migrations and restart Gunicorn and Celery.

View File

@ -1,10 +1,10 @@
# SMF # SMF
## Overview ## Overview
SMF is a free php-based forum. SMF is a free PHP-based forum.
## Dependencies ## Dependencies
SMF requires php installed in your web server. Apache has `mod_php`, NGINX requires `php-fpm`. More details can be found in the [SMF requirements page.](https://download.simplemachines.org/requirements.php) SMF requires PHP installed in your web server. Apache has `mod_php`, NGINX requires `php-fpm`. More details can be found in the [SMF requirements page.](https://download.simplemachines.org/requirements.php)
## Prepare Your Settings ## Prepare Your Settings
In your auth project's settings file, do the following: In your auth project's settings file, do the following:
@ -25,23 +25,31 @@ In your auth project's settings file, do the following:
## Setup ## Setup
### Download SMF ### Download SMF
Using your browser, you can download the latest version of SMF to your desktop computer. All SMF downloads can be found at SMF Downloads. The latest recommended version will always be available at http://www.simplemachines.org/download/index.php/latest/install/. Using your browser, you can download the latest version of SMF to your desktop computer. All SMF downloads can be found at SMF Downloads. The latest recommended version will always be available at http://www.simplemachines.org/download/index.php/latest/install/. Retrieve the file location from the hyperlinked box icon for the zip full install, depending on your browser you may have a Copy Link or similar option in your right click menu.
Download using wget, replacing the url with the url for the package you just retrieved
wget http://download.simplemachines.org/index.php?thanks;filename=smf_2-0-13_install.zip Download using wget, replacing the URL with the URL for the package you just retrieved
wget https://download.simplemachines.org/index.php?thanks;filename=smf_2-0-15_install.zip
This needs to be unpackaged. Unzip it, replacing the file name with that of the file you just downloaded This needs to be unpackaged. Unzip it, replacing the file name with that of the file you just downloaded
unzip smf_2-0-13_install.zip unzip smf_2-0-15_install.zip
Now we need to move this to our web directory. Usually `/var/www/forums`. Now we need to move this to our web directory. Usually `/var/www/forums`.
sudo mv smf /var/www/forums mv smf /var/www/forums
The web server needs read/write permission to this folder The web server needs read/write permission to this folder
sudo chown -R www-data:www-data /var/www/forums Apache: `chown -R www-data:www-data /var/www/forums`
Nginx: `chown -R nginx:nginx /var/www/forums`
```eval_rst
.. tip::
Nginx: Some distributions use the ``www-data:www-data`` user:group instead of ``nginx:nginx``. If you run into problems with permissions try it instead.
..
```
### Database Preparation ### Database Preparation
SMF needs a database. Create one: SMF needs a database. Create one:
@ -56,7 +64,7 @@ Enter the database information into the `DATABASES['smf']` section of your auth
### Web Server Configuration ### Web Server Configuration
Your web server needs to be configured to serve Alliance Market. Your web server needs to be configured to serve Alliance Market.
A minimal apache config might look like: A minimal Apache config might look like:
<VirtualHost *:80> <VirtualHost *:80>
ServerName forums.example.com ServerName forums.example.com
@ -66,13 +74,13 @@ A minimal apache config might look like:
</Directory> </Directory>
</VirtualHost> </VirtualHost>
A minimal nginx config might look like: A minimal Nginx config might look like:
server { server {
listen 80; listen 80;
server_name forums.example.com; server_name forums.example.com;
root /var/www/forums; root /var/www/forums;
index app.php; index index.php;
access_log /var/logs/forums.access.log; access_log /var/logs/forums.access.log;
location ~ \.php$ { location ~ \.php$ {
@ -86,9 +94,6 @@ A minimal nginx config might look like:
Enter the web address to your forums into the `SMF_URL` setting in your auth project's settings file. Enter the web address to your forums into the `SMF_URL` setting in your auth project's settings file.
### Preparing Auth
Once settings are entered, apply migrations and restart gunicorn and celery.
### Web Install ### Web Install
Navigate to your forums address where you will be presented with an installer. Navigate to your forums address where you will be presented with an installer.
@ -107,3 +112,6 @@ Under Database Settings, set the following:
If you use a table prefix other than the standard `smf_` you need to add an additional setting to your auth project's settings file, `SMF_TABLE_PREFIX = ''`, and enter the prefix. If you use a table prefix other than the standard `smf_` you need to add an additional setting to your auth project's settings file, `SMF_TABLE_PREFIX = ''`, and enter the prefix.
Follow the directions in the installer. Follow the directions in the installer.
### Preparing Auth
Once settings are entered, apply migrations and restart Gunicorn and Celery.

View File

@ -1,7 +1,7 @@
# Teamspeak 3 # TeamSpeak 3
## Overview ## Overview
Teamspeak3 is the most popular VOIP program for gamers. TeamSpeak3 is the most popular VOIP program for gamers.
But have you considered using Mumble? Not only is it free, but it has features and performance far superior to Teamspeak3. But have you considered using Mumble? Not only is it free, but it has features and performance far superior to Teamspeak3.
@ -28,39 +28,39 @@ In your auth project's settings file, do the following:
} }
### Download Installer ### Download Installer
To install we need a copy of the server. You can find the latest version from [this dl server](http://dl.4players.de/ts/releases/) (Id recommed getting the latest stable version find this version number from the [TeamSpeak site](https://www.teamspeak.com/downloads#)). Be sure to get a link to the linux version. To install we need a copy of the server. You can find the latest version from [this dl server](http://dl.4players.de/ts/releases/) (Id recommend getting the latest stable version find this version number from the [TeamSpeak site](https://www.teamspeak.com/downloads#)). Be sure to get a link to the Linux version.
Download the server, replacing the link with the link you got earlier. Download the server, replacing the link with the link you got earlier.
http://dl.4players.de/ts/releases/3.1.0/teamspeak3-server_linux_amd64-3.1.0.tar.bz2 http://dl.4players.de/ts/releases/3.1.1/teamspeak3-server_linux_amd64-3.1.1.tar.bz2
Now we need to extract the file. Now we need to extract the file.
tar -xf teamspeak3-server_linux_amd64-3.1.0.tar.bz2 tar -xf teamspeak3-server_linux_amd64-3.1.0.tar.bz2
### Create User ### Create User
Teamspeak needs its own user. TeamSpeak needs its own user.
sudo adduser --disabled-login teamspeak adduser --disabled-login teamspeak
### Install Binary ### Install Binary
Now we move the server binary somewhere more accessible and change its ownership to the new user. Now we move the server binary somewhere more accessible and change its ownership to the new user.
sudo mv teamspeak3-server_linux_amd64 /usr/local/teamspeak mv teamspeak3-server_linux_amd64 /usr/local/teamspeak
sudo chown -R teamspeak:teamspeak /usr/local/teamspeak chown -R teamspeak:teamspeak /usr/local/teamspeak
### Startup ### Startup
Now we generate a startup script so teamspeak comes up with the server. Now we generate a startup script so TeamSpeak comes up with the server.
sudo ln -s /usr/local/teamspeak/ts3server_startscript.sh /etc/init.d/teamspeak ln -s /usr/local/teamspeak/ts3server_startscript.sh /etc/init.d/teamspeak
sudo update-rc.d teamspeak defaults update-rc.d teamspeak defaults
Finally we start the server. Finally we start the server.
sudo service teamspeak start service teamspeak start
### Update Settings ### Update Settings
The console will spit out a block of text. If it does not appear, it can be found with `sudo service teamspeak status`. **SAVE THIS**. The console will spit out a block of text. If it does not appear, it can be found with `service teamspeak status`. **SAVE THIS**.
If you plan on claiming the ServerAdmin token, do so with a different TeamSpeak client profile than the one used for your auth account, or you will lose your admin status. If you plan on claiming the ServerAdmin token, do so with a different TeamSpeak client profile than the one used for your auth account, or you will lose your admin status.
@ -68,12 +68,12 @@ Edit the settings you added to your auth project's settings file earlier, enteri
- `TEAMSPEAK3_SERVERQUERY_USER` is `loginname` from that block of text it just spat out (usually `serveradmin`) - `TEAMSPEAK3_SERVERQUERY_USER` is `loginname` from that block of text it just spat out (usually `serveradmin`)
- `TEAMSPEAK3_SERVERQUERY_PASSWORD` is `password` from that block of text it just spat out - `TEAMSPEAK3_SERVERQUERY_PASSWORD` is `password` from that block of text it just spat out
- `TEAMSPEAK_VIRTUAL_SERVER` is the virtual server ID of the server to be managed - it will only ever not be 1 if your server is hosted by a professional company - `TEAMSPEAK_VIRTUAL_SERVER` is the virtual server ID of the server to be managed - it will only ever not be 1 if your server is hosted by a professional company
- `TEAMSPEAK3_PUBLIC_URL` is the public address of your teamspeak server. Do not include any leading http:// or teamspeak:// - `TEAMSPEAK3_PUBLIC_URL` is the public address of your TeamSpeak server. Do not include any leading http:// or teamspeak://
Once settings are entered, run migrations and restart gunicorn and celery. Once settings are entered, run migrations and restart Gunicorn and Celery.
### Generate User Account ### Generate User Account
And now we can generate ourselves a user account. Navigate to the services in AllianceAuth for your user account and press the checkmark for TeamSpeak 3. And now we can generate ourselves a user account. Navigate to the services in Alliance Auth for your user account and press the checkmark for TeamSpeak 3.
Click the URL provided to automatically connect to our server. It will prompt you to redeem the serveradmin token, enter the `token` from startup. Click the URL provided to automatically connect to our server. It will prompt you to redeem the serveradmin token, enter the `token` from startup.

View File

@ -37,4 +37,4 @@ The settings you created earlier now need to be filled out.
`XENFORO_API_KEY` is the API key value you set earlier. `XENFORO_API_KEY` is the API key value you set earlier.
Once these are entered, run migrations and restart gunicorn and celery. Once these are entered, run migrations and restart Gunicorn and Celery.

View File

@ -8,15 +8,15 @@ When installing Alliance Auth you are instructed to run the `allianceauth start`
The first folder created is the root directory of your auth project. This folder contains: The first folder created is the root directory of your auth project. This folder contains:
- the `manage.py` management script used to interact with Django - the `manage.py` management script used to interact with Django
- a preconfigured `supervisor.conf` supervisor config for running celery (and optionally gunicorn) automatically - a preconfigured `supervisor.conf` Supervisor config for running Celery (and optionally Gunicorn) automatically
- a `log` folder which contains log files generated by Alliance Auth - a `log` folder which contains log files generated by Alliance Auth
### The myauth Subfolder ### The myauth Subfolder
Within your auth project root folder is another folder of the same name (a quirk of django project structures). This folder contains: Within your auth project root folder is another folder of the same name (a quirk of Django project structures). This folder contains:
- a celery app definition in `celery.py` for registering tasks with the background workers - a Celery app definition in `celery.py` for registering tasks with the background workers
- a web server gateway interface script `wsgi.py` for processing web requests - a web server gateway interface script `wsgi.py` for processing web requests
- the root url config `urls.py` which Django uses to direct requests to the appropriate view - the root URL config `urls.py` which Django uses to direct requests to the appropriate view
There are also two subfolders for `static` and `templates` which allow adding new content and overriding default content shipped with Alliance Auth or Django. There are also two subfolders for `static` and `templates` which allow adding new content and overriding default content shipped with Alliance Auth or Django.
@ -35,15 +35,15 @@ The local settings file is referred to as "your auth project's settings file" an
Your auth project comes with four log file definitions by default. These are created in the `myauth/log/` folder at runtime. Your auth project comes with four log file definitions by default. These are created in the `myauth/log/` folder at runtime.
- `allianceauth.log` contains all `INFO` level and above logging messages from Alliance Auth. This is useful for tracking who is making changes to the site, what is happening to users, and debugging any errors that may occur. - `allianceauth.log` contains all `INFO` level and above logging messages from Alliance Auth. This is useful for tracking who is making changes to the site, what is happening to users, and debugging any errors that may occur.
- `worker.log` contains logging messages from the celery background task workers. This is useful for monitoring background processes such as group syncing to services. - `worker.log` contains logging messages from the Celery background task workers. This is useful for monitoring background processes such as group syncing to services.
- `beat.log` contains logging messages from the background task scheduler. This is of limited use unless the scheduler isn't starting. - `beat.log` contains logging messages from the background task scheduler. This is of limited use unless the scheduler isn't starting.
- `gunicorn.log` contains logging messages from gunicorn workers. This contains all web-sourced messages found in `allianceauth.log` as well as runtime errors from the workers themselves. - `gunicorn.log` contains logging messages from Gunicorn workers. This contains all web-sourced messages found in `allianceauth.log` as well as runtime errors from the workers themselves.
When asking for assistance with your auth project be sure to first read the logs, and share any relevant entries. When asking for assistance with your auth project be sure to first read the logs, and share any relevant entries.
## Custom Static and Templates ## Custom Static and Templates
Within your auth project exists two folders named `static` and `templates`. These are used by Django for rendering web pages. Static refers to content Django does not need to parse before displaying, such as CSS styling or images. When running via a WSGI worker such as gunicorn static files are copied to a location for the web server to read from. Templates are always read from the template folders, rendered with additional context from a view function, and then displayed to the user. Within your auth project exists two folders named `static` and `templates`. These are used by Django for rendering web pages. Static refers to content Django does not need to parse before displaying, such as CSS styling or images. When running via a WSGI worker such as Gunicorn static files are copied to a location for the web server to read from. Templates are always read from the template folders, rendered with additional context from a view function, and then displayed to the user.
You can add extra static or templates by putting files in these folders. Note that changes to static requires running the `python manage.py collectstatic` command to copy to the web server directory. You can add extra static or templates by putting files in these folders. Note that changes to static requires running the `python manage.py collectstatic` command to copy to the web server directory.
@ -86,3 +86,12 @@ urlpatterns = [
url(r'', include(allianceauth.urls)), url(r'', include(allianceauth.urls)),
] ]
``` ```
## Adding and Removing Apps
Your auth project is just a regular Django project - you can add in [other Django apps](https://djangopackages.org/) as desired. Most come with dedicated setup guides, but in general:
- add `'appname',` to your `INSTALLED_APPS` setting
- run `python manage.py migrate`
- run `python manage.py collectstatic`
If you ever want to remove an app, you should first clear it from the database to avoid dangling foreign keys: `python manage.py migrate appname zero`. Then you can remove it from your auth project's `INSTALLED_APPS` list.

View File

@ -2,25 +2,27 @@
## Something broken? Stuck on an issue? Can't get it set up? ## Something broken? Stuck on an issue? Can't get it set up?
Start by checking the [issues](https://github.com/allianceauth/allianceauth/issues?utf8=%E2%9C%93&q=is%3Aissue) - especially closed ones. Start by checking the [issues](https://github.com/allianceauth/allianceauth/issues?q=is%3Aissue) - especially closed ones.
No answer? No answer?
- open an [issue](https://github.com/allianceauth/allianceauth/issues) - open an [issue](https://github.com/allianceauth/allianceauth/issues)
- harass us on [gitter](https://gitter.im/R4stl1n/allianceauth) - harass us on [gitter](https://gitter.im/R4stl1n/allianceauth)
## Logging
In its default configuration your auth project logs INFO and above messages to myauth/log/allianceauth.log. If you're encountering issues it's a good idea to view DEBUG messages as these greatly assist the troubleshooting process. These are printed to the console with manually starting the webserver via `python manage.py runserver`.
To record DEBUG messages in the log file, alter a setting in your auth project's settings file: `LOGGING['handlers']['log_file']['level'] = 'DEBUG'`. After restarting gunicorn and celery your log file will record all logging messages.
## Common Problems ## Common Problems
### `pip install -r requirements.txt` is failing
It's probably a permissions issue. Ensure your current user can write to the virtual environment folder. That, or you're missing a dependency of some kind which will be indicated in the error message.
### I'm getting an error 500 trying to connect to the website on a new install ### I'm getting an error 500 trying to connect to the website on a new install
*Great.* Error 500 is the generic message given by your web server when *anything* breaks. The actual error message is hidden in one of your auth project's log files. Read them to identify it. *Great.* Error 500 is the generic message given by your web server when *anything* breaks. The actual error message is hidden in one of your auth project's log files. Read them to identify it.
### Failed to configure log handler ### Failed to configure log handler
Make sure the log directory is write-able by the allianceserver user: `chmown -R allianceserver:allianceserver /path/to/myauth/log/`, then restart the auth supervisor processes. Make sure the log directory is writeable by the allianceserver user: `chmown -R allianceserver:allianceserver /path/to/myauth/log/`, then restart the auth supervisor processes.
### Groups aren't syncing to services ### Groups aren't syncing to services
@ -40,3 +42,11 @@ Now start the worker again with `supervisorctl start myauth:worker`
### Proxy timeout when entering email address ### Proxy timeout when entering email address
This usually indicates an issue with your email settings. Ensure these are correct and your email server/service is properly configured. This usually indicates an issue with your email settings. Ensure these are correct and your email server/service is properly configured.
### No images are available to users accessing the website
This is likely due to a permissions mismatch. Check the setup guide for your web server. Additionally ensure the user who owns `/var/www/myauth/static` is the same user as running your webserver, as this can be non-standard.
### Unable to execute 'gunicorn myauth.wsgi' or ImportError: No module named 'myauth.wsgi'
Gunicorn needs to have context for its running location, `/home/alllianceserver/myauth/gunicorn myauth.wsgi` will not work, instead `cd /home/alllianceserver/myauth` then `gunicorn myauth.wsgi` is needed to boot Gunicorn. This is handled in the Supervisor config, but this may be encountered running Gunicorn manually for testing.

View File

@ -14,6 +14,7 @@ install_requires = [
'redis', 'redis',
'celery>=4.0.2', 'celery>=4.0.2',
'celery_once',
'django>=1.11', 'django>=1.11',
'django-bootstrap-form', 'django-bootstrap-form',
@ -25,7 +26,7 @@ install_requires = [
'openfire-restapi', 'openfire-restapi',
'sleekxmpp', 'sleekxmpp',
'adarnauth-esi>=1.4,<2.0', 'adarnauth-esi>=1.4.10,<2.0',
] ]
testing_extras = [ testing_extras = [

View File

@ -1,4 +1,4 @@
import os, sys import os
from celery import Celery from celery import Celery
# set the default Django settings module for the 'celery' program. # set the default Django settings module for the 'celery' program.
@ -11,6 +11,11 @@ app = Celery('devauth')
# Using a string here means the worker don't have to serialize # Using a string here means the worker don't have to serialize
# the configuration object to child processes. # the configuration object to child processes.
app.config_from_object('django.conf:settings') app.config_from_object('django.conf:settings')
app.conf.ONCE = {
'backend': 'allianceauth.services.tasks.DjangoBackend',
'settings': {}
}
# Load task modules from all registered Django app configs. # Load task modules from all registered Django app configs.
app.autodiscover_tasks(lambda: settings.INSTALLED_APPS) app.autodiscover_tasks(lambda: settings.INSTALLED_APPS)

View File

@ -14,7 +14,7 @@ basepython =
deps= deps=
dj111: Django>=1.11.1,<2.0 dj111: Django>=1.11.1,<2.0
dj20: Django>=2.0 dj20: Django>=2.0
dj20: https://github.com/celery/django-celery-beat/zipball/master#egg=django-celery-beat py37: https://github.com/yaml/pyyaml/zipball/master#egg=pyyaml
dj20: https://github.com/Adarnof/adarnauth-esi/zipball/master#egg=adarnauth-esi py37: https://github.com/celery/kombu/zipball/master#egg=kombu
install_command = pip install -e ".[testing]" -U {opts} {packages} install_command = pip install -e ".[testing]" -U {opts} {packages}
commands=coverage run runtests.py -v 2 commands=coverage run runtests.py -v 2