Restructure Alliance Auth package (#867)

* Refactor allianceauth into its own package

* Add setup

* Add missing default_app_config declarations

* Fix timerboard namespacing

* Remove obsolete future imports

* Remove py2 mock support

* Remove six

* Add experimental 3.7 support and multiple Dj versions

* Remove python_2_unicode_compatible

* Add navhelper as local package

* Update requirements
This commit is contained in:
Basraah
2017-09-19 09:46:40 +10:00
committed by GitHub
parent d10580b56b
commit 786859294d
538 changed files with 1197 additions and 1523 deletions

View File

@@ -0,0 +1 @@
default_app_config = 'allianceauth.authentication.apps.AuthenticationConfig'

View File

@@ -0,0 +1,156 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User, Permission
from django.utils.text import slugify
from allianceauth.services.hooks import ServicesHook
from allianceauth.authentication.models import State, get_guest_state, CharacterOwnership, UserProfile
from allianceauth.hooks import get_hooks
def make_service_hooks_update_groups_action(service):
"""
Make a admin action for the given service
:param service: services.hooks.ServicesHook
:return: fn to update services groups for the selected users
"""
def update_service_groups(modeladmin, request, queryset):
for user in queryset: # queryset filtering doesn't work here?
service.update_groups(user)
update_service_groups.__name__ = str('update_{}_groups'.format(slugify(service.name)))
update_service_groups.short_description = "Sync groups for selected {} accounts".format(service.title)
return update_service_groups
def make_service_hooks_sync_nickname_action(service):
"""
Make a sync_nickname admin action for the given service
:param service: services.hooks.ServicesHook
:return: fn to sync nickname for the selected users
"""
def sync_nickname(modeladmin, request, queryset):
for user in queryset: # queryset filtering doesn't work here?
service.sync_nickname(user)
sync_nickname.__name__ = str('sync_{}_nickname'.format(slugify(service.name)))
sync_nickname.short_description = "Sync nicknames for selected {} accounts".format(service.title)
return sync_nickname
class UserAdmin(BaseUserAdmin):
"""
Extending Django's UserAdmin model
"""
def get_actions(self, request):
actions = super(BaseUserAdmin, self).get_actions(request)
for hook in get_hooks('services_hook'):
svc = hook()
# Check update_groups is redefined/overloaded
if svc.update_groups.__module__ != ServicesHook.update_groups.__module__:
action = make_service_hooks_update_groups_action(svc)
actions[action.__name__] = (action,
action.__name__,
action.short_description)
# Create sync nickname action if service implements it
if svc.sync_nickname.__module__ != ServicesHook.sync_nickname.__module__:
action = make_service_hooks_sync_nickname_action(svc)
actions[action.__name__] = (action,
action.__name__,
action.short_description)
return actions
list_filter = BaseUserAdmin.list_filter + ('profile__state',)
@admin.register(State)
class StateAdmin(admin.ModelAdmin):
fieldsets = (
(None, {
'fields': ('name', 'permissions', 'priority'),
}),
('Membership', {
'fields': ('public', 'member_characters', 'member_corporations', 'member_alliances'),
})
)
filter_horizontal = ['member_characters', 'member_corporations', 'member_alliances', 'permissions']
list_display = ('name', 'priority', 'user_count')
def has_delete_permission(self, request, obj=None):
if obj == get_guest_state():
return False
return super(StateAdmin, self).has_delete_permission(request, obj=obj)
def get_fieldsets(self, request, obj=None):
if obj == get_guest_state():
return (
(None, {
'fields': ('permissions', 'priority'),
}),
)
return super(StateAdmin, self).get_fieldsets(request, obj=obj)
@staticmethod
def user_count(obj):
return obj.userprofile_set.all().count()
@admin.register(UserProfile)
class UserProfileAdmin(admin.ModelAdmin):
readonly_fields = ('user', 'state')
search_fields = ('user__username', 'main_character__character_name')
list_filter = ('state',)
list_display = ('user', 'main_character')
actions = None
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
@admin.register(CharacterOwnership)
class CharacterOwnershipAdmin(admin.ModelAdmin):
list_display = ('user', 'character')
search_fields = ('user__username', 'character__character_name', 'character__corporation_name', 'character__alliance_name')
readonly_fields = ('owner_hash', 'character')
class PermissionAdmin(admin.ModelAdmin):
actions = None
readonly_fields = [field.name for field in Permission._meta.fields]
list_display = ('admin_name', 'name', 'codename', 'content_type')
list_filter = ('content_type__app_label',)
@staticmethod
def admin_name(obj):
return str(obj)
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
# Hack to allow registration of django.contrib.auth models in our authentication app
class ProxyUser(User):
class Meta:
proxy = True
verbose_name = User._meta.verbose_name
verbose_name_plural = User._meta.verbose_name_plural
class ProxyPermission(Permission):
class Meta:
proxy = True
verbose_name = Permission._meta.verbose_name
verbose_name_plural = Permission._meta.verbose_name_plural
try:
admin.site.unregister(User)
finally:
admin.site.register(ProxyUser, UserAdmin)
admin.site.register(ProxyPermission, PermissionAdmin)

View File

@@ -0,0 +1,12 @@
from django.apps import AppConfig
from django.core.checks import register, Tags
class AuthenticationConfig(AppConfig):
name = 'allianceauth.authentication'
label = 'authentication'
def ready(self):
super(AuthenticationConfig, self).ready()
from allianceauth.authentication import checks
register(Tags.security)(checks.check_login_scopes_setting)

View File

@@ -0,0 +1,73 @@
from django.contrib.auth.backends import ModelBackend
from django.contrib.auth.models import Permission
from django.contrib.auth.models import User
from .models import UserProfile, CharacterOwnership
class StateBackend(ModelBackend):
@staticmethod
def _get_state_permissions(user_obj):
profile_state_field = UserProfile._meta.get_field('state')
user_state_query = 'state__%s__user' % profile_state_field.related_query_name()
return Permission.objects.filter(**{user_state_query: user_obj})
def get_state_permissions(self, user_obj, obj=None):
return self._get_permissions(user_obj, obj, 'state')
def get_all_permissions(self, user_obj, obj=None):
if not user_obj.is_active or user_obj.is_anonymous or obj is not None:
return set()
if not hasattr(user_obj, '_perm_cache'):
user_obj._perm_cache = self.get_user_permissions(user_obj)
user_obj._perm_cache.update(self.get_group_permissions(user_obj))
user_obj._perm_cache.update(self.get_state_permissions(user_obj))
return user_obj._perm_cache
def authenticate(self, token=None):
if not token:
return None
try:
ownership = CharacterOwnership.objects.get(character__character_id=token.character_id)
if ownership.owner_hash == token.character_owner_hash:
return ownership.user
else:
ownership.delete()
return self.create_user(token)
except CharacterOwnership.DoesNotExist:
try:
# insecure legacy main check for pre-sso registration auth installs
profile = UserProfile.objects.get(main_character__character_id=token.character_id)
# attach an ownership
CharacterOwnership.objects.create_by_token(token)
return profile.user
except UserProfile.DoesNotExist:
pass
return self.create_user(token)
def create_user(self, token):
username = self.iterate_username(token.character_name) # build unique username off character name
user = User.objects.create_user(username)
user.set_unusable_password() # prevent login via password
user.is_active = False # prevent login until email set
user.save()
token.user = 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.save()
return user
@staticmethod
def iterate_username(name):
name = str.replace(name, "'", "")
name = str.replace(name, ' ', '_')
if User.objects.filter(username__startswith=name).exists():
u = User.objects.filter(username__startswith=name)
num = len(u)
username = "%s_%s" % (name, num)
while u.filter(username=username).exists():
num += 1
username = "%s_%s" % (name, num)
else:
username = name
return username

View File

@@ -0,0 +1,12 @@
from django.core.checks import Error
from django.conf import settings
def check_login_scopes_setting(*args, **kwargs):
errors = []
try:
assert settings.LOGIN_TOKEN_SCOPES
except (AssertionError, AttributeError):
errors.append(Error('LOGIN_TOKEN_SCOPES setting cannot be blank.',
hint='SSO tokens used for logging in must require scopes to be refreshable.'))
return errors

View File

@@ -0,0 +1,6 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
class RegistrationForm(forms.Form):
email = forms.EmailField(label=_('Email'), max_length=254, required=True)

View File

@@ -0,0 +1,14 @@
from django.conf.urls import url, include
from allianceauth.authentication import views
urlpatterns = [
url(r'^activate/complete/$', views.activation_complete, name='registration_activation_complete'),
# The activation key can make use of any character from the
# URL-safe base64 alphabet, plus the colon as a separator.
url(r'^activate/(?P<activation_key>[-:\w]+)/$', views.ActivationView.as_view(), name='registration_activate'),
url(r'^register/$', views.RegistrationView.as_view(), name='registration_register'),
url(r'^register/complete/$', views.registration_complete, name='registration_complete'),
url(r'^register/closed/$', views.registration_closed, name='registration_disallowed'),
url(r'', include('registration.auth_urls')),
]

View File

@@ -0,0 +1,76 @@
import logging
from django.db import transaction
from django.db.models import Manager, QuerySet, Q
from allianceauth.eveonline.managers import EveManager
from allianceauth.eveonline.models import EveCharacter
logger = logging.getLogger(__name__)
def available_states_query(character):
query = Q(public=True)
if character.character_id:
query |= Q(member_characters__character_id=character.character_id)
if character.corporation_id:
query |= Q(member_corporations__corporation_id=character.corporation_id)
if character.alliance_id:
query |= Q(member_alliances__alliance_id=character.alliance_id)
return query
class CharacterOwnershipManager(Manager):
def create_by_token(self, token):
if not EveCharacter.objects.filter(character_id=token.character_id).exists():
EveManager.create_character(token.character_id)
return self.create(character=EveCharacter.objects.get(character_id=token.character_id), user=token.user,
owner_hash=token.character_owner_hash)
class StateQuerySet(QuerySet):
def available_to_character(self, character):
return self.filter(available_states_query(character))
def available_to_user(self, user):
if user.profile.main_character:
return self.available_to_character(user.profile.main_character)
else:
return self.none()
def get_for_user(self, user):
states = self.available_to_user(user)
if states.exists():
return states[0]
else:
from allianceauth.authentication.models import get_guest_state
return get_guest_state()
def delete(self):
with transaction.atomic():
for state in self:
for profile in state.userprofile_set.all():
profile.assign_state(state=self.model.objects.exclude(pk=state.pk).get_for_user(profile.user))
super(StateQuerySet, self).delete()
class StateManager(Manager):
def get_queryset(self):
return StateQuerySet(self.model, using=self._db)
def available_to_character(self, character):
return self.get_queryset().available_to_character(character)
def available_to_user(self, user):
return self.get_queryset().available_to_user(user)
def get_for_character(self, character):
states = self.get_queryset().available_to_character(character)
if states.exists():
return states[0]
else:
from allianceauth.authentication.models import get_guest_state
return get_guest_state()
def get_for_user(self, user):
return self.get_queryset().get_for_user(user)

View File

@@ -0,0 +1,52 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-05 21:38
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='AuthServicesInfo',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('ipboard_username', models.CharField(blank=True, default=b'', max_length=254)),
('ipboard_password', models.CharField(blank=True, default=b'', max_length=254)),
('xenforo_username', models.CharField(blank=True, default=b'', max_length=254)),
('xenforo_password', models.CharField(blank=True, default=b'', max_length=254)),
('forum_username', models.CharField(blank=True, default=b'', max_length=254)),
('forum_password', models.CharField(blank=True, default=b'', max_length=254)),
('jabber_username', models.CharField(blank=True, default=b'', max_length=254)),
('jabber_password', models.CharField(blank=True, default=b'', max_length=254)),
('mumble_username', models.CharField(blank=True, default=b'', max_length=254)),
('mumble_password', models.CharField(blank=True, default=b'', max_length=254)),
('teamspeak3_uid', models.CharField(blank=True, default=b'', max_length=254)),
('teamspeak3_perm_key', models.CharField(blank=True, default=b'', max_length=254)),
('discord_uid', models.CharField(blank=True, default=b'', max_length=254)),
('discourse_username', models.CharField(blank=True, default=b'', max_length=254)),
('discourse_password', models.CharField(blank=True, default=b'', max_length=254)),
('ips4_username', models.CharField(blank=True, default=b'', max_length=254)),
('ips4_password', models.CharField(blank=True, default=b'', max_length=254)),
('ips4_id', models.CharField(blank=True, default=b'', max_length=254)),
('smf_username', models.CharField(blank=True, default=b'', max_length=254)),
('smf_password', models.CharField(blank=True, default=b'', max_length=254)),
('market_username', models.CharField(blank=True, default=b'', max_length=254)),
('market_password', models.CharField(blank=True, default=b'', max_length=254)),
('pathfinder_username', models.CharField(blank=True, default=b'', max_length=254)),
('pathfinder_password', models.CharField(blank=True, default=b'', max_length=254)),
('main_char_id', models.CharField(blank=True, default=b'', max_length=64)),
('is_blue', models.BooleanField(default=False)),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@@ -0,0 +1,23 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-07 19:14
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentication', '0001_initial'),
]
operations = [
migrations.RemoveField(
model_name='authservicesinfo',
name='pathfinder_password',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='pathfinder_username',
),
]

View File

@@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 20:29
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0002_auto_20160907_1914'),
]
operations = [
migrations.AddField(
model_name='authservicesinfo',
name='state',
field=models.CharField(blank=True, choices=[(None, b'None'), (b'Blue', b'Blue'), (b'Member', b'Member')], default=None, max_length=10, null=True),
),
]

View File

@@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 23:19
from __future__ import unicode_literals
from django.db import migrations
def create_permissions(apps, schema_editor):
User = apps.get_model('auth', 'User')
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
ct = ContentType.objects.get_for_model(User)
Permission.objects.get_or_create(codename="member", content_type=ct, name="member")
Permission.objects.get_or_create(codename="group_management", content_type=ct, name="group_management")
Permission.objects.get_or_create(codename="jabber_broadcast", content_type=ct, name="jabber_broadcast")
Permission.objects.get_or_create(codename="jabber_broadcast_all", content_type=ct, name="jabber_broadcast_all")
Permission.objects.get_or_create(codename="fleetactivitytracking", content_type=ct, name="fleetactivitytracking")
Permission.objects.get_or_create(codename="fleetactivitytracking_statistics", content_type=ct, name="fleetactivitytracking_statistics")
Permission.objects.get_or_create(codename="human_resources", content_type=ct, name="human_resources")
Permission.objects.get_or_create(codename="blue_member", content_type=ct, name="blue_member")
Permission.objects.get_or_create(codename="alliance_apis", content_type=ct, name="alliance_apis")
Permission.objects.get_or_create(codename="corp_apis", content_type=ct, name="corp_apis")
Permission.objects.get_or_create(codename="timer_management", content_type=ct, name="timer_management")
Permission.objects.get_or_create(codename="timer_view", content_type=ct, name="timer_view")
Permission.objects.get_or_create(codename="srp_management", content_type=ct, name="srp_management")
Permission.objects.get_or_create(codename="signature_management", content_type=ct, name="signature_management")
Permission.objects.get_or_create(codename="signature_view", content_type=ct, name="signature_view")
Permission.objects.get_or_create(codename="optimer_management", content_type=ct, name="optimer_management")
Permission.objects.get_or_create(codename="optimer_view", content_type=ct, name="optimer_view")
Permission.objects.get_or_create(codename="logging_notifications", content_type=ct, name="logging_notifications")
def reverse(apps, schema_editor):
#too lazy
pass
class Migration(migrations.Migration):
dependencies = [
('authentication', '0003_authservicesinfo_state'),
]
operations = [
migrations.RunPython(create_permissions, reverse)
]

View File

@@ -0,0 +1,31 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 23:11
from __future__ import unicode_literals
from django.db import migrations
def delete_permissions(apps, schema_editor):
User = apps.get_model('auth', 'User')
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
ct = ContentType.objects.get_for_model(User)
Permission.objects.filter(content_type=ct).filter(codename__in=['member', 'blue_member', 'signature_management', 'signature_view']).delete()
def create_permissions(apps, schema_editor):
User = apps.get_model('auth', 'User')
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
ct = ContentType.objects.get_for_model(User)
Permission.objects.get_or_create(codename="member", content_type=ct, name="member")
Permission.objects.get_or_create(codename="blue_member", content_type=ct, name="blue_member")
class Migration(migrations.Migration):
dependencies = [
('authentication', '0004_create_permissions'),
]
operations = [
migrations.RunPython(delete_permissions, create_permissions),
]

View File

@@ -0,0 +1,51 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-10 05:42
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentication', '0005_delete_perms'),
]
operations = [
migrations.RemoveField(
model_name='authservicesinfo',
name='discourse_password',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='forum_password',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='ipboard_password',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='ips4_password',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='jabber_password',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='market_password',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='mumble_password',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='smf_password',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='xenforo_password',
),
]

View File

@@ -0,0 +1,19 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-10 21:50
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentication', '0006_auto_20160910_0542'),
]
operations = [
migrations.RemoveField(
model_name='authservicesinfo',
name='is_blue',
),
]

View File

@@ -0,0 +1,17 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-12 13:04
from __future__ import unicode_literals
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('authentication', '0007_remove_authservicesinfo_is_blue'),
('eveonline', '0001_initial'),
('auth', '0001_initial'),
]
operations = [
]

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-10-21 02:28
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('authentication', '0008_set_state'),
]
operations = [
migrations.RemoveField(
model_name='authservicesinfo',
name='discourse_username',
),
migrations.AddField(
model_name='authservicesinfo',
name='discourse_enabled',
field=models.BooleanField(default=False),
),
]

View File

@@ -0,0 +1,40 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2017-01-07 06:47
from __future__ import unicode_literals
from django.db import migrations
def count_completed_fields(model):
return len([True for key, value in model.__dict__.items() if bool(value)])
def forward(apps, schema_editor):
# this ensures only one model exists per user
AuthServicesInfo = apps.get_model('authentication', 'AuthServicesInfo')
users = set([a.user for a in AuthServicesInfo.objects.all()])
for u in users:
auths = AuthServicesInfo.objects.filter(user=u)
if auths.count() > 1:
pk = auths[0].pk
largest = 0
for auth in auths:
completed = count_completed_fields(auth)
if completed > largest:
largest = completed
pk = auth.pk
auths.exclude(pk=pk).delete()
# ensure all users have a model
User = apps.get_model('auth', 'User')
for u in User.objects.exclude(pk__in=[user.pk for user in users]):
AuthServicesInfo.objects.create(user=u)
class Migration(migrations.Migration):
dependencies = [
('authentication', '0009_auto_20161021_0228'),
]
operations = [
migrations.RunPython(forward, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,22 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2017-01-07 07:11
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('authentication', '0010_only_one_authservicesinfo'),
]
operations = [
migrations.AlterField(
model_name='authservicesinfo',
name='user',
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
),
]

View File

@@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-01-12 00:59
from __future__ import unicode_literals
from django.db import migrations, models
def remove_permissions(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
AuthServicesInfo = apps.get_model('authentication', 'AuthServicesInfo')
# delete the add and remove permissions for AuthServicesInfo
ct = ContentType.objects.get_for_model(AuthServicesInfo)
Permission.objects.filter(content_type=ct).filter(codename__in=['add_authservicesinfo', 'delete_authservicesinfo']).delete()
def add_permissions(apps, schema_editor):
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
AuthServicesInfo = apps.get_model('authentication', 'AuthServicesInfo')
# recreate the add and remove permissions for AuthServicesInfo
ct = ContentType.objects.get_for_model(AuthServicesInfo)
Permission.objects.create(content_type=ct, codename='add_authservicesinfo', name='Can add auth services info')
Permission.objects.create(content_type=ct, codename='delete_authservicesinfo', name='Can delete auth services info')
class Migration(migrations.Migration):
dependencies = [
('authentication', '0011_authservicesinfo_user_onetoonefield'),
]
operations = [
migrations.AlterModelOptions(
name='authservicesinfo',
options={'default_permissions': ('change',)},
),
migrations.RunPython(remove_permissions, add_permissions),
]

View File

@@ -0,0 +1,321 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.2 on 2016-12-11 23:14
from __future__ import unicode_literals
from django.db import migrations
from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist
import logging
logger = logging.getLogger(__name__)
def optional_dependencies():
"""
Only require these migrations if the given app
is installed. If the app isn't installed then
the relevant AuthServicesInfo field will be LOST
when the data migration is run.
"""
installed_apps = settings.INSTALLED_APPS
dependencies = []
# Skip adding module dependencies if the settings specifies that services have been migrated
if not getattr(settings, 'SERVICES_MIGRATED', False):
if 'services.modules.xenforo' in installed_apps:
dependencies.append(('xenforo', '0001_initial'))
if 'services.modules.discord' in installed_apps:
dependencies.append(('discord', '0001_initial'))
if 'services.modules.discourse' in installed_apps:
dependencies.append(('discourse', '0001_initial'))
if 'services.modules.ipboard' in installed_apps:
dependencies.append(('ipboard', '0001_initial'))
if 'services.modules.ips4' in installed_apps:
dependencies.append(('ips4', '0001_initial'))
if 'services.modules.market' in installed_apps:
dependencies.append(('market', '0001_initial'))
if 'services.modules.openfire' in installed_apps:
dependencies.append(('openfire', '0001_initial'))
if 'services.modules.smf' in installed_apps:
dependencies.append(('smf', '0001_initial'))
if 'services.modules.teamspeak3' in installed_apps:
dependencies.append(('teamspeak3', '0003_teamspeak3user'))
if 'services.modules.mumble' in installed_apps:
dependencies.append(('mumble', '0003_mumbleuser_user'))
if 'services.modules.phpbb3' in installed_apps:
dependencies.append(('phpbb3', '0001_initial'))
return dependencies
class DatalossException(Exception):
def __init__(self, field, app):
message = "This migration would cause a loss of data for the %s field, ensure the %s app is installed and" \
" run the migration again" % (field, app)
super(Exception, self).__init__(message)
def check_for_dataloss(authservicesinfo):
"""
Check if any of the authservicesinfo contain a field which contains data
that would be lost because the target module for migration is not installed.
If a record is found with no matching target installed an exception is raised.
:param authservicesinfo: AuthServicesInfo records to check
"""
installed_apps = settings.INSTALLED_APPS
for authinfo in authservicesinfo:
if authinfo.xenforo_username and 'services.modules.xenforo' not in installed_apps:
raise DatalossException('xenforo_username', 'services.modules.xenforo')
if authinfo.discord_uid and 'services.modules.discord' not in installed_apps:
raise DatalossException('discord_uid', 'services.modules.discord')
if authinfo.discourse_enabled and 'services.modules.discourse' not in installed_apps:
raise DatalossException('discourse_enabled', 'services.modules.discourse')
if authinfo.ipboard_username and 'services.modules.ipboard' not in installed_apps:
raise DatalossException('ipboard_username', 'services.modules.ipboard')
if authinfo.ips4_id and 'services.modules.ips4' not in installed_apps:
raise DatalossException('ips4_id', 'services.modules.ips4')
if authinfo.market_username and 'services.modules.market' not in installed_apps:
raise DatalossException('market_username', 'services.modules.market')
if authinfo.jabber_username and 'services.modules.openfire' not in installed_apps:
raise DatalossException('jabber_username', 'services.modules.openfire')
if authinfo.smf_username and 'services.modules.smf' not in installed_apps:
raise DatalossException('smf_username', 'services.modules.smf')
if authinfo.teamspeak3_uid and 'services.modules.teamspeak3' not in installed_apps:
raise DatalossException('teamspeak3_uid', 'services.modules.teamspeak3')
if authinfo.mumble_username and 'services.modules.mumble' not in installed_apps:
raise DatalossException('mumble_username', 'services.modules.mumble')
if authinfo.forum_username and 'services.modules.phpbb3' not in installed_apps:
raise DatalossException('forum_username', 'services.modules.phpbb3')
def forward(apps, schema_editor):
installed_apps = settings.INSTALLED_APPS
AuthServicesInfo = apps.get_model("authentication", "AuthServicesInfo")
# Check if any records would result in dataloss
check_for_dataloss(AuthServicesInfo.objects.all())
XenforoUser = apps.get_model('xenforo', 'XenforoUser') if 'services.modules.xenforo' in installed_apps else None
DiscordUser = apps.get_model('discord', 'DiscordUser') if 'services.modules.discord' in installed_apps else None
DiscourseUser = apps.get_model('discourse', 'DiscourseUser') if 'services.modules.discourse' in installed_apps else None
IpboardUser = apps.get_model('ipboard', 'IpboardUser') if 'services.modules.ipboard' in installed_apps else None
Ips4User = apps.get_model('ips4', 'Ips4User') if 'services.modules.ips4' in installed_apps else None
MarketUser = apps.get_model('market', 'MarketUser') if 'services.modules.market' in installed_apps else None
OpenfireUser = apps.get_model('openfire', 'OpenfireUser') if 'services.modules.openfire' in installed_apps else None
SmfUser = apps.get_model('smf', 'SmfUser') if 'services.modules.smf' in installed_apps else None
Teamspeak3User = apps.get_model('teamspeak3', 'Teamspeak3User') if 'services.modules.teamspeak3' in installed_apps else None
MumbleUser = apps.get_model('mumble', 'MumbleUser') if 'services.modules.mumble' in installed_apps else None
Phpbb3User = apps.get_model('phpbb3', 'Phpbb3User') if 'services.modules.phpbb3' in installed_apps else None
for authinfo in AuthServicesInfo.objects.all():
user = authinfo.user
if XenforoUser is not None and authinfo.xenforo_username:
logging.debug('Updating Xenforo info for %s' % user.username)
xfu = XenforoUser()
xfu.user = user
xfu.username = authinfo.xenforo_username
xfu.save()
if DiscordUser is not None and authinfo.discord_uid:
logging.debug('Updating Discord info for %s' % user.username)
du = DiscordUser()
du.user = user
du.uid = authinfo.discord_uid
du.save()
if DiscourseUser is not None and authinfo.discourse_enabled:
logging.debug('Updating Discourse info for %s' % user.username)
du = DiscourseUser()
du.user = user
du.enabled = authinfo.discourse_enabled
du.save()
if IpboardUser is not None and authinfo.ipboard_username:
logging.debug('Updating IPBoard info for %s' % user.username)
ipb = IpboardUser()
ipb.user = user
ipb.username = authinfo.ipboard_username
ipb.save()
if Ips4User is not None and authinfo.ips4_id:
logging.debug('Updating Ips4 info for %s' % user.username)
ips = Ips4User()
ips.user = user
ips.id = authinfo.ips4_id
ips.username = authinfo.ips4_username
ips.save()
if MarketUser is not None and authinfo.market_username:
logging.debug('Updating Market info for %s' % user.username)
mkt = MarketUser()
mkt.user = user
mkt.username = authinfo.market_username
mkt.save()
if OpenfireUser is not None and authinfo.jabber_username:
logging.debug('Updating Openfire (jabber) info for %s' % user.username)
ofu = OpenfireUser()
ofu.user = user
ofu.username = authinfo.jabber_username
ofu.save()
if SmfUser is not None and authinfo.smf_username:
logging.debug('Updating SMF info for %s' % user.username)
smf = SmfUser()
smf.user = user
smf.username = authinfo.smf_username
smf.save()
if Teamspeak3User is not None and authinfo.teamspeak3_uid:
logging.debug('Updating Teamspeak3 info for %s' % user.username)
ts3 = Teamspeak3User()
ts3.user = user
ts3.uid = authinfo.teamspeak3_uid
ts3.perm_key = authinfo.teamspeak3_perm_key
ts3.save()
if MumbleUser is not None and authinfo.mumble_username:
logging.debug('Updating mumble info for %s' % user.username)
try:
mbl = MumbleUser.objects.get(username=authinfo.mumble_username)
mbl.user = user
mbl.save()
except ObjectDoesNotExist:
logger.warn('AuthServiceInfo mumble_username for {} but no '
'corresponding record in MumbleUser, dropping'.format(user.username))
if Phpbb3User is not None and authinfo.forum_username:
logging.debug('Updating phpbb3 info for %s' % user.username)
phb = Phpbb3User()
phb.user = user
phb.username = authinfo.forum_username
phb.save()
def reverse(apps, schema_editor):
User = apps.get_model('auth', 'User')
AuthServicesInfo = apps.get_model("authentication", "AuthServicesInfo")
for user in User.objects.all():
authinfo, c = AuthServicesInfo.objects.get_or_create(user=user)
if hasattr(user, 'xenforo'):
logging.debug('Reversing xenforo for %s' % user.username)
authinfo.xenforo_username = user.xenforo.username
if hasattr(user, 'discord'):
logging.debug('Reversing discord for %s' % user.username)
authinfo.discord_uid = user.discord.uid
if hasattr(user, 'discourse'):
logging.debug('Reversing discourse for %s' % user.username)
authinfo.discourse_enabled = user.discourse.enabled
if hasattr(user, 'ipboard'):
logging.debug('Reversing ipboard for %s' % user.username)
authinfo.ipboard_username = user.ipboard.username
if hasattr(user, 'ips4'):
logging.debug('Reversing ips4 for %s' % user.username)
authinfo.ips4_id = user.ips4.id
authinfo.ips4_username = user.ips4.username
if hasattr(user, 'market'):
logging.debug('Reversing market for %s' % user.username)
authinfo.market_username = user.market.username
if hasattr(user, 'openfire'):
logging.debug('Reversing openfire (jabber) for %s' % user.username)
authinfo.jabber_username = user.openfire.username
if hasattr(user, 'smf'):
logging.debug('Reversing smf for %s' % user.username)
authinfo.smf_username = user.smf.username
if hasattr(user, 'teamspeak3'):
logging.debug('Reversing teamspeak3 for %s' % user.username)
authinfo.teamspeak3_uid = user.teamspeak3.uid
authinfo.teamspeak3_perm_key = user.teamspeak3.perm_key
if hasattr(user, 'mumble'):
logging.debug('Reversing mumble for %s' % user.username)
try:
authinfo.mumble_username = user.mumble.all()[:1].get().username
except ObjectDoesNotExist:
logging.debug('Failed to reverse mumble for %s' % user.username)
if hasattr(user, 'phpbb3'):
logging.debug('Reversing phpbb3 for %s' % user.username)
authinfo.forum_username = user.phpbb3.username
logging.debug('Saving AuthServicesInfo for %s ' % user.username)
authinfo.save()
class Migration(migrations.Migration):
dependencies = optional_dependencies() + [
('authentication', '0012_remove_add_delete_authservicesinfo_permissions'),
]
operations = [
# Migrate data
migrations.RunPython(forward, reverse),
# Remove fields
migrations.RemoveField(
model_name='authservicesinfo',
name='discord_uid',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='discourse_enabled',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='forum_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='ipboard_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='ips4_id',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='ips4_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='jabber_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='market_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='mumble_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='smf_username',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='teamspeak3_perm_key',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='teamspeak3_uid',
),
migrations.RemoveField(
model_name='authservicesinfo',
name='xenforo_username',
),
]

View File

@@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.1 on 2016-09-09 23:19
from __future__ import unicode_literals
from django.db import migrations
def create_permission(apps, schema_editor):
User = apps.get_model('auth', 'User')
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
ct = ContentType.objects.get_for_model(User)
Permission.objects.get_or_create(codename="view_fleetup", content_type=ct, name="view_fleetup")
class Migration(migrations.Migration):
dependencies = [
('authentication', '0013_service_modules'),
]
operations = [
migrations.RunPython(create_permission, migrations.RunPython.noop)
]

View File

@@ -0,0 +1,263 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.5 on 2017-03-22 23:09
from __future__ import unicode_literals
import allianceauth.authentication.models
import django.db.models.deletion
from django.conf import settings
from django.contrib.auth.hashers import make_password
from django.db import migrations, models
def create_guest_state(apps, schema_editor):
State = apps.get_model('authentication', 'State')
State.objects.update_or_create(name='Guest', defaults={'priority': 0, 'public': True})
def create_member_state(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
State = apps.get_model('authentication', 'State')
EveAllianceInfo = apps.get_model('eveonline', 'EveAllianceInfo')
EveCorporationInfo = apps.get_model('eveonline', 'EveCorporationInfo')
member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member')
s = State.objects.update_or_create(name=member_state_name, defaults={'priority': 100, 'public': False})[0]
try:
# move group permissions to state
g = Group.objects.get(name=member_state_name)
[s.permissions.add(p.pk) for p in g.permissions.all()]
g.delete()
except Group.DoesNotExist:
pass
# auto-populate member IDs
CORP_IDS = getattr(settings, 'CORP_IDS', [])
ALLIANCE_IDS = getattr(settings, 'ALLIANCE_IDS', [])
[s.member_corporations.add(c.pk) for c in EveCorporationInfo.objects.filter(corporation_id__in=CORP_IDS)]
[s.member_alliances.add(a.pk) for a in EveAllianceInfo.objects.filter(alliance_id__in=ALLIANCE_IDS)]
def create_member_group(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
State = apps.get_model('authentication', 'State')
member_state_name = getattr(settings, 'DEFAULT_AUTH_GROUP', 'Member')
try:
g = Group.objects.get(name=member_state_name)
# move permissions back
state = State.objects.get(name=member_state_name)
[g.permissions.add(p.pk) for p in state.permissions.all()]
# move users back
for profile in state.userprofile_set.all().select_related('user'):
profile.user.groups.add(g.pk)
except (Group.DoesNotExist, State.DoesNotExist):
pass
def create_blue_state(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
State = apps.get_model('authentication', 'State')
EveAllianceInfo = apps.get_model('eveonline', 'EveAllianceInfo')
EveCorporationInfo = apps.get_model('eveonline', 'EveCorporationInfo')
blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue')
s = State.objects.update_or_create(name=blue_state_name, defaults={'priority': 50, 'public': False})[0]
try:
# move group permissions to state
g = Group.objects.get(name=blue_state_name)
[s.permissions.add(p.pk) for p in g.permissions.all()]
g.permissions.clear()
except Group.DoesNotExist:
pass
# auto-populate blue member IDs
BLUE_CORP_IDS = getattr(settings, 'BLUE_CORP_IDS', [])
BLUE_ALLIANCE_IDS = getattr(settings, 'BLUE_ALLIANCE_IDS', [])
[s.member_corporations.add(c.pk) for c in EveCorporationInfo.objects.filter(corporation_id__in=BLUE_CORP_IDS)]
[s.member_alliances.add(a.pk) for a in EveAllianceInfo.objects.filter(alliance_id__in=BLUE_ALLIANCE_IDS)]
def create_blue_group(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
State = apps.get_model('authentication', 'State')
blue_state_name = getattr(settings, 'DEFAULT_BLUE_GROUP', 'Blue')
try:
g = Group.objects.get(name=blue_state_name)
# move permissions back
state = State.objects.get(name=blue_state_name)
[g.permissions.add(p.pk) for p in state.permissions.all()]
# move users back
for profile in state.userprofile_set.all().select_related('user'):
profile.user.groups.add(g.pk)
except (Group.DoesNotExist, State.DoesNotExist):
pass
def populate_ownerships(apps, schema_editor):
Token = apps.get_model('esi', 'Token')
CharacterOwnership = apps.get_model('authentication', 'CharacterOwnership')
EveCharacter = apps.get_model('eveonline', 'EveCharacter')
unique_character_owners = [t['character_id'] for t in
Token.objects.all().values('character_id').annotate(n=models.Count('user')) if
t['n'] == 1 and EveCharacter.objects.filter(character_id=t['character_id'].exists())]
tokens = Token.objects.filter(character_id__in=unique_character_owners)
for c_id in unique_character_owners:
ts = tokens.filter(character_id=c_id).order_by('created')
for t in ts:
if t.can_refresh:
# find newest refreshable token and use it as basis for CharacterOwnership
CharacterOwnership.objecs.create_by_token(t)
break
def create_profiles(apps, schema_editor):
AuthServicesInfo = apps.get_model('authentication', 'AuthServicesInfo')
State = apps.get_model('authentication', 'State')
UserProfile = apps.get_model('authentication', 'UserProfile')
EveCharacter = apps.get_model('eveonline', 'EveCharacter')
# grab AuthServicesInfo if they have a unique main_char_id and the EveCharacter exists
unique_mains = [auth['main_char_id'] for auth in
AuthServicesInfo.objects.exclude(main_char_id='').values('main_char_id').annotate(
n=models.Count('main_char_id')) if
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')
for auth in auths:
# carry states and mains forward
state = State.objects.get(name=auth.state if auth.state else 'Guest')
char = EveCharacter.objects.get(character_id=auth.main_char_id)
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'):
# prepare empty profiles
state = State.objects.get(name='Guest')
UserProfile.objects.create(user=auth.user, state=state)
def recreate_authservicesinfo(apps, schema_editor):
AuthServicesInfo = apps.get_model('authentication', 'AuthServicesInfo')
UserProfile = apps.get_model('authentication', 'UserProfile')
User = apps.get_model('auth', 'User')
# recreate all missing AuthServicesInfo models
AuthServicesInfo.objects.bulk_create([AuthServicesInfo(user=u.pk) for u in User.objects.all()])
# repopulate main characters
for profile in UserProfile.objects.exclude(main_character__isnull=True).select_related('user', 'main_character'):
AuthServicesInfo.objects.update_or_create(user=profile.user,
defaults={'main_char_id': profile.main_character.character_id})
# repopulate states we understand
for profile in UserProfile.objects.exclude(state__name='Guest').filter(
state__name__in=['Member', 'Blue']).select_related('user', 'state'):
AuthServicesInfo.objects.update_or_create(user=profile.user, defaults={'state': profile.state.name})
def disable_passwords(apps, schema_editor):
User = apps.get_model('auth', 'User')
for u in User.objects.exclude(is_staff=True):
# remove passwords for non-staff users to prevent password-based authentication
# set_unusable_password is unavailable in migrations because :reasons:
u.password = make_password(None)
u.save()
class Migration(migrations.Migration):
dependencies = [
('auth', '0008_alter_user_username_max_length'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('eveonline', '0008_remove_apikeys'),
('authentication', '0014_fleetup_permission'),
('esi', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='CharacterOwnership',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('owner_hash', models.CharField(max_length=28, unique=True)),
('character', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='character_ownership', to='eveonline.EveCharacter')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='character_ownerships', to=settings.AUTH_USER_MODEL)),
],
options={
'default_permissions': ('change', 'delete'),
'ordering': ['user', 'character__character_name'],
},
),
migrations.CreateModel(
name='State',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, unique=True)),
('priority', models.IntegerField(help_text='Users get assigned the state with the highest priority available to them.', unique=True)),
('public', models.BooleanField(default=False, help_text='Make this state available to any character.')),
('member_alliances', models.ManyToManyField(blank=True, help_text='Alliances to whose members this state is available.', to='eveonline.EveAllianceInfo')),
('member_characters', models.ManyToManyField(blank=True, help_text='Characters to which this state is available.', to='eveonline.EveCharacter')),
('member_corporations', models.ManyToManyField(blank=True, help_text='Corporations to whose members this state is available.', to='eveonline.EveCorporationInfo')),
('permissions', models.ManyToManyField(blank=True, to='auth.Permission')),
],
options={
'default_permissions': ('change',),
'ordering': ['-priority'],
},
),
migrations.CreateModel(
name='UserProfile',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('main_character', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='eveonline.EveCharacter')),
('state', models.ForeignKey(default=allianceauth.authentication.models.get_guest_state_pk, on_delete=django.db.models.deletion.SET_DEFAULT, to='authentication.State')),
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='profile', to=settings.AUTH_USER_MODEL)),
],
options={
'default_permissions': ('change',),
},
),
migrations.RunPython(create_guest_state, migrations.RunPython.noop),
migrations.RunPython(create_member_state, create_member_group),
migrations.RunPython(create_blue_state, create_blue_group),
migrations.RunPython(populate_ownerships, migrations.RunPython.noop),
migrations.RunPython(create_profiles, recreate_authservicesinfo),
migrations.RemoveField(
model_name='authservicesinfo',
name='user',
),
migrations.DeleteModel(
name='AuthServicesInfo',
),
migrations.RunPython(disable_passwords, migrations.RunPython.noop),
migrations.CreateModel(
name='ProxyPermission',
fields=[
],
options={
'proxy': True,
'verbose_name': 'permission',
'verbose_name_plural': 'permissions',
},
bases=('auth.permission',),
managers=[
('objects', django.contrib.auth.models.PermissionManager()),
],
),
migrations.CreateModel(
name='ProxyUser',
fields=[
],
options={
'proxy': True,
'verbose_name': 'user',
'verbose_name_plural': 'users',
},
bases=('auth.user',),
managers=[
('objects', django.contrib.auth.models.UserManager()),
],
),
]

View File

@@ -0,0 +1,98 @@
import logging
from django.contrib.auth.models import User, Permission
from django.db import models, transaction
from django.utils.translation import ugettext_lazy as _
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from allianceauth.notifications import notify
from .managers import CharacterOwnershipManager, StateManager
logger = logging.getLogger(__name__)
class State(models.Model):
name = models.CharField(max_length=20, unique=True)
permissions = models.ManyToManyField(Permission, blank=True)
priority = models.IntegerField(unique=True,
help_text="Users get assigned the state with the highest priority available to them.")
member_characters = models.ManyToManyField(EveCharacter, blank=True,
help_text="Characters to which this state is available.")
member_corporations = models.ManyToManyField(EveCorporationInfo, blank=True,
help_text="Corporations to whose members this state is available.")
member_alliances = models.ManyToManyField(EveAllianceInfo, blank=True,
help_text="Alliances to whose members this state is available.")
public = models.BooleanField(default=False, help_text="Make this state available to any character.")
objects = StateManager()
class Meta:
ordering = ['-priority']
def __str__(self):
return self.name
def available_to_character(self, character):
return self in State.objects.available_to_character(character)
def available_to_user(self, user):
return self in State.objects.available_to_user(user)
def delete(self, **kwargs):
with transaction.atomic():
for profile in self.userprofile_set.all():
profile.assign_state(state=State.objects.exclude(pk=self.pk).get_for_user(profile.user))
super(State, self).delete(**kwargs)
def get_guest_state():
try:
return State.objects.get(name='Guest')
except State.DoesNotExist:
return State.objects.create(name='Guest', priority=0, public=True)
def get_guest_state_pk():
return get_guest_state().pk
class UserProfile(models.Model):
class Meta:
default_permissions = ('change',)
user = models.OneToOneField(User, related_name='profile', on_delete=models.CASCADE)
main_character = models.OneToOneField(EveCharacter, blank=True, null=True, on_delete=models.SET_NULL)
state = models.ForeignKey(State, on_delete=models.SET_DEFAULT, default=get_guest_state_pk)
def assign_state(self, state=None, commit=True):
if not state:
state = State.objects.get_for_user(self.user)
if self.state != state:
self.state = state
if commit:
logger.info('Updating {} state to {}'.format(self.user, self.state))
self.save(update_fields=['state'])
notify(self.user, _('State Changed'),
_('Your user state has been changed to %(state)s') % ({'state': state}),
'info')
from allianceauth.authentication.signals import state_changed
state_changed.send(sender=self.__class__, user=self.user, state=self.state)
def __str__(self):
return str(self.user)
class CharacterOwnership(models.Model):
class Meta:
default_permissions = ('change', 'delete')
ordering = ['user', 'character__character_name']
character = models.OneToOneField(EveCharacter, on_delete=models.CASCADE, related_name='character_ownership')
owner_hash = models.CharField(max_length=28, unique=True)
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='character_ownerships')
objects = CharacterOwnershipManager()
def __str__(self):
return "%s: %s" % (self.user, self.character)

View File

@@ -0,0 +1,130 @@
import logging
from .models import CharacterOwnership, UserProfile, get_guest_state, State
from django.contrib.auth.models import User
from django.db.models import Q
from django.db.models.signals import post_save, pre_delete, m2m_changed, pre_save
from django.dispatch import receiver, Signal
from esi.models import Token
from allianceauth.eveonline.managers import EveManager
from allianceauth.eveonline.models import EveCharacter
logger = logging.getLogger(__name__)
state_changed = Signal(providing_args=['user', 'state'])
def trigger_state_check(state):
# evaluate all current members to ensure they still have access
for profile in state.userprofile_set.all():
profile.assign_state()
# we may now be available to others with lower states
check_states = State.objects.filter(priority__lt=state.priority)
for profile in UserProfile.objects.filter(state__in=check_states):
if state.available_to_user(profile.user):
profile.state = state
profile.save(update_fields=['state'])
state_changed.send(sender=state.__class__, user=profile.user, state=state)
@receiver(m2m_changed, sender=State.member_characters.through)
def state_member_characters_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'):
trigger_state_check(instance)
@receiver(m2m_changed, sender=State.member_corporations.through)
def state_member_corporations_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'):
trigger_state_check(instance)
@receiver(m2m_changed, sender=State.member_alliances.through)
def state_member_alliances_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'):
trigger_state_check(instance)
@receiver(post_save, sender=State)
def state_saved(sender, instance, *args, **kwargs):
trigger_state_check(instance)
# Is there a smarter way to intercept pre_save with a diff main_character or state?
@receiver(post_save, sender=UserProfile)
def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
# catches post_save from profiles to trigger necessary service and state checks
if not created:
update_fields = kwargs.pop('update_fields', []) or []
if 'state' not in update_fields:
instance.assign_state()
@receiver(post_save, sender=User)
def create_required_models(sender, instance, created, *args, **kwargs):
# ensure all users have a model
if created:
UserProfile.objects.get_or_create(user=instance)
@receiver(post_save, sender=Token)
def record_character_ownership(sender, instance, created, *args, **kwargs):
if created:
if instance.user:
query = Q(owner_hash=instance.character_owner_hash) & Q(user=instance.user)
else:
query = Q(owner_hash=instance.character_owner_hash)
# purge ownership records if the hash or auth user account has changed
CharacterOwnership.objects.filter(character__character_id=instance.character_id).exclude(query).delete()
# create character if needed
if EveCharacter.objects.filter(character_id=instance.character_id).exists() is False:
EveManager.create_character(instance.character_id)
char = EveCharacter.objects.get(character_id=instance.character_id)
# check if we need to create ownership
if instance.user and not CharacterOwnership.objects.filter(character__character_id=instance.character_id).exists():
CharacterOwnership.objects.update_or_create(character=char,
defaults={'owner_hash': instance.character_owner_hash,
'user': instance.user})
@receiver(pre_delete, sender=CharacterOwnership)
def validate_main_character(sender, instance, *args, **kwargs):
if instance.user.profile.main_character == instance.character:
# clear main character as user no longer owns them
instance.user.profile.main_character = None
instance.user.profile.save()
@receiver(pre_delete, sender=Token)
def validate_main_character_token(sender, instance, *args, **kwargs):
if UserProfile.objects.filter(main_character__character_id=instance.character_id).exists():
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).exclude(pk=instance.pk).exists():
# clear main character as we can no longer verify ownership
profile.main_character = None
profile.save()
@receiver(pre_save, sender=User)
def assign_state_on_active_change(sender, instance, *args, **kwargs):
# set to guest state if inactive, assign proper state if reactivated
if instance.pk:
old_instance = User.objects.get(pk=instance.pk)
if old_instance.is_active != instance.is_active:
if instance.is_active:
instance.profile.assign_state()
else:
instance.profile.state = get_guest_state()
instance.profile.save(update_fields=['state'])
@receiver(post_save, sender=EveCharacter)
def check_state_on_character_update(sender, instance, *args, **kwargs):
# if this is a main character updating, check that user's state
try:
instance.userprofile.assign_state()
except UserProfile.DoesNotExist:
pass

Binary file not shown.

After

Width:  |  Height:  |  Size: 158 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -0,0 +1,37 @@
import logging
from esi.errors import TokenExpiredError, TokenInvalidError
from esi.models import Token
from allianceauth.authentication.models import CharacterOwnership
from allianceauth.celeryapp import app
logger = logging.getLogger(__name__)
@app.task
def check_character_ownership(owner_hash):
tokens = Token.objects.filter(character_owner_hash=owner_hash)
if tokens:
for t in tokens:
old_hash = t.character_owner_hash
try:
t.update_token_data(commit=False)
except (TokenExpiredError, TokenInvalidError):
t.delete()
continue
if t.character_owner_hash == old_hash:
break
else:
logger.info('Character %s has changed ownership. Revoking %s tokens.' % (t.character_name, tokens.count()))
tokens.delete()
else:
logger.info('No tokens found with owner hash %s. Revoking ownership.' % owner_hash)
CharacterOwnership.objects.filter(owner_hash=owner_hash).delete()
@app.task
def check_all_character_ownership():
for c in CharacterOwnership.objects.all().only('owner_hash'):
check_character_ownership.delay(c.owner_hash)

View File

@@ -0,0 +1,95 @@
{% extends "registered/base.html" %}
{% load staticfiles %}
{% load i18n %}
{% block title %}{% trans "Dashboard" %}{% endblock %}
{% block content %}
<div class="col-lg-12">
<h1 class="page-header text-center">{% trans "Dashboard" %}</h1>
<div class="col-lg-12 container">
<div class="row">
<div class="col-lg-6 text-center">
<div class="panel panel-primary">
<div class="panel-heading">{% trans "Main Character" %}</div>
<div class="panel-body">
{% if request.user.profile.main_character %}
{% with request.user.profile.main_character as main %}
<div class="col-lg-4 col-sm-2">
<table class="table">
<tr><td class="text-center"><img class="ra-avatar" src="https://image.eveonline.com/Character/{{ main.character_id }}_128.jpg"></td></tr>
<tr><td class="text-center">{{ main.character_name }}</td></tr>
</table>
</div>
<div class="col-lg-4 col-sm-2">
<table class="table">
<tr><td class="text-center"><img class="ra-avatar" src="https://image.eveonline.com/Corporation/{{ main.corporation_id }}_128.png"></td></tr>
<tr><td class="text-center">{{ main.corporation_name }}</td></tr>
</table>
</div>
<div class="col-lg-4 col-sm-2">
{% if main.alliance_id %}
<table class="table">
<tr><td class="text-center"><img class="ra-avatar" src="https://image.eveonline.com/Alliance/{{ main.alliance_id }}_128.png"></td></tr>
<tr><td class="text-center">{{ main.alliance_name }}</td><tr>
</table>
{% endif %}
</div>
{% endwith %}
{% else %}
<div class="alert alert-danger" role="alert">{% trans "Missing main character model." %}</div>
{% endif %}
<div class="clearfix"></div>
<div class="col-xs-6">
<a href="{% url 'authentication:add_character' %}" class="btn btn-block btn-info" title="Add Character">{% trans 'Add Character' %}</a>
</div>
<div class="col-xs-6">
<a href="{% url 'authentication:change_main_character' %}" class="btn btn-block btn-info" title="Change Main Character">{% trans "Change Main" %}</a>
</div>
</div>
</div>
</div>
<div class="col-lg-6 text-center">
<div class="panel panel-success">
<div class="panel-heading">{% trans "Groups" %}</div>
<div class="panel-body">
<div style="height: 236px;overflow:-moz-scrollbars-vertical;overflow-y:auto;">
<table class="table table-striped">
{% for group in user.groups.all %}
<tr>
<td>{{ group.name }}</td>
</tr>
{% endfor %}
</table>
</div>
</div>
</div>
</div>
<div>
</div>
<div class="clearfix"></div>
<div class="panel panel-default">
<div class="panel-heading" style="display:flex;">{% trans 'Characters' %}</div>
<div class="panel-body">
<table class="table table-hover">
<tr>
<th class="text-center"></th>
<th class="text-center">{% trans 'Name' %}</th>
<th class="text-center">{% trans 'Corp' %}</th>
<th class="text-center">{% trans 'Alliance' %}</th>
</tr>
{% for ownership in request.user.character_ownerships.all %}
{% with ownership.character as char %}
<tr>
<td class="text-center"><img class="ra-avatar img-circle" src="https://image.eveonline.com/Character/{{ char.character_id }}_32.jpg"></td>
<td class="text-center">{{ char.character_name }}</td>
<td class="text-center">{{ char.corporation_name }}</td>
<td class="text-center">{{ char.alliance_name }}</td>
</tr>
{% endwith %}
{% endfor %}
</table>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,50 @@
{% load static %}
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>{% block title %}{{ SITE_NAME }}{% endblock %}</title>
{% include 'bundles/bootstrap-css.html' %}
{% include 'bundles/fontawesome.html' %}
{% block extra_include %}
{% endblock %}
<style>
body {
background: url('{% static 'authentication/img/background.jpg' %}') no-repeat scroll;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
background-size: cover;
}
.panel-transparent {
background: rgba(48, 48, 48, 0.7);
color: #ffffff;
}
.panel-body {
}
#lang_select {
width: 40%;
margin-left: auto;
margin-right: auto;
}
{% block extra_style %}
{% endblock %}
</style>
</head>
<body>
<div class="container" style="margin-top:150px">
{% block content %}
{% endblock %}
</div>
</body>
</html>

View File

@@ -0,0 +1,15 @@
{% load i18n %}
<div class="dropdown">
<form action="{% url 'set_language' %}" method="post">
{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path|slice:'3:' }}" />
<select onchange="this.form.submit()" class="form-control" id="lang_select" name="language">
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected="selected"{% endif %}>
{{ language.name_local }} ({{ language.code }})
</option>
{% endfor %}
</select>
</form>
</div>

View File

@@ -0,0 +1,10 @@
{% extends 'public/middle_box.html' %}
{% load static %}
{% block title %}Login{% endblock %}
{% block middle_box_content %}
<p style="text-align:center">
<a href="{% url 'auth_sso_login' %}">
<img src="{% static 'img/sso/EVE_SSO_Login_Buttons_Large_Black.png' %}" border=0>
</a>
</p>
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% extends 'public/base.html' %}
{% load static %}
{% block title %}Login{% endblock %}
{% block content %}
<div class="col-md-4 col-md-offset-4">
{% if messages %}
{% for message in messages %}
<div class="alert alert-{{ message.level_tag}}">{{ message }}</div>
{% endfor %}
{% endif %}
<div class="panel panel-default panel-transparent">
<div class="panel-body">
<div class="col-md-12">
{% block middle_box_content %}
{% endblock %}
</div>
</div>
{% include 'public/lang_select.html' %}
</div>
</div>
{% endblock %}
{% block extra_include %}
{% include 'bundles/bootstrap-js.html' %}
{% endblock %}

View File

@@ -0,0 +1,24 @@
{% load staticfiles %}
{% load bootstrap %}
{% load i18n %}
{% extends 'public/base.html' %}
{% block title %}Registration{% endblock %}
{% block extra_include %}
{% include 'bundles/bootstrap-css.html' %}
{% include 'bundles/fontawesome.html' %}
{% include 'bundles/bootstrap-js.html' %}
{% endblock %}
{% block content %}
<div class="col-md-4 col-md-offset-4">
<div class="panel panel-default panel-transparent">
<div class="panel-body">
<form method="POST">
{% csrf_token %}
{{ form|bootstrap }}
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Register" %}</button>
</form>
</div>
</div>
{% include 'public/lang_select.html' %}
</div>
{% endblock %}

View File

@@ -0,0 +1,143 @@
{% load static %}
{% load i18n %}
{% load navactive %}
{% load menu_items %}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>{% block title %}{% block page_title %}{% endblock page_title %} - Alliance Auth{% endblock title %}</title>
{% include 'bundles/bootstrap-css.html' %}
{% include 'bundles/fontawesome.html' %}
<link href="{% static 'css/auth-base.css' %}" type="text/css" rel="stylesheet">
{% block extra_css %}{% endblock extra_css %}
</head>
<body>
{% if user.is_authenticated %}
<div id="wrapper">
<!-- Navigation -->
<nav class="navbar navbar-inverse navbar-static-top auth-navbar-top" role="navigation">
<div class="container-fluid">
<a class="navbar-brand">
{{ SITE_NAME }}
</a>
<!-- /.navbar-header -->
<ul class="nav navbar-top-links navbar-right">
<li class="nav-link">
<form id="f_lang_select" action="{% url 'set_language' %}" method="post">
{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path|slice:'3:' }}"/>
<select onchange="this.form.submit()" class="form-control" id="lang_select" name="language">
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
<option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %}
selected="selected"{% endif %}>
{{ language.name_local }} ({{ language.code }})
</option>
{% endfor %}
</select>
</form>
</li>
{% if notifications %}
<li class="nav-link active">
<a href="{% url 'auth_notification_list' %}">
<span class="fa-stack">
<i class="fa fa-bell fa-stack-2x"></i>
<i class="fa fa-inverse fa-stack-1x">{{ notifications }}</i>
</span>
</a>
</li>
{% else %}
<li class="nav-link"><a href="{% url 'auth_notification_list' %}">
<i class="fa fa-bell-o"></i></a>
</li>
{% endif %}
{% if user.is_authenticated %}
{% if user.is_staff %}
<li><a href="{% url 'admin:index' %}">{% trans "Admin" %}</a></li>
{% endif %}
<li><a href="{% url 'auth_logout' %}">{% trans "Logout" %}</a></li>
{% else %}
<li><a href="{% url 'authentication:login' %}">{% trans "Login" %}</a></li>
{% endif %}
</ul>
</div>
<!-- /.navbar-top-links -->
<div class="navbar-default sidebar auth-sidebar" role="navigation">
<div class="sidebar-nav navbar-collapse">
<ul class="nav" id="side-menu">
<li>
<a class="{% navactive request 'authentication:dashboard' %}"
href="{% url 'authentication:dashboard' %}">
<i class="fa fa-dashboard fa-fw grayiconecolor"></i>{% trans " Dashboard" %}
</a>
</li>
<li>
<a class="{% navactive request 'auth_groups' %}" href="{% url 'auth_groups' %}">
<i class="fa fa-cogs fa-fw fa-sitemap grayiconecolor"></i>{% trans " Groups" %}
</a>
</li>
{% if can_manage_groups %}
<li>
<a class="{% navactive request 'auth_group_management auth_group_membership auth_group_membership_list' %}"
href="{% url 'auth_group_management' %}">
<i class="fa fa-lock fa-sitemap fa-fw grayiconecolor"></i>{% trans " Group Management" %}
</a>
</li>
{% endif %}
{% menu_items %}
<li>
<a class="{% navactive request 'authentication:help' %}"
href="{% url 'authentication:help' %}">
<i class="fa fa-question fa-fw grayiconecolor"></i>{% trans " Help" %}
</a>
</li>
</ul>
</div>
<!-- /.sidebar-collapse -->
</div>
<!-- /.navbar-static-side -->
</nav>
<div id="page-wrapper">
<div class="container">
{% if messages %}
<br>
{% for message in messages %}
<div class="alert alert-{{ message.level_tag }}">{{ message }}</div>
{% endfor %}
{% endif %}
{% block content %}
{% endblock content %}
</div>
</div>
</div>
{% endif %}
{% include 'bundles/bootstrap-js.html' %}
{% block extra_javascript %}
{% endblock extra_javascript %}
<script>
{% block extra_script %}
{% endblock extra_script %}
</script>
</body>
</html>

View File

@@ -0,0 +1,5 @@
{% extends 'public/middle_box.html' %}
{% load i18n %}
{% block middle_box_content %}
<div class="alert alert-danger">{% trans 'Invalid or expired activation link.' %}</div>
{% endblock %}

View File

@@ -0,0 +1,9 @@
You're receiving this email because someone has entered this email address while registering for an account on {{ site.domain }}
If this was you, please go to the following URL to confirm your email address:
{{ url }}
This link will expire in {{ expiration_days }} day(s).
If this was not you, it is safe to ignore this email.

View File

@@ -0,0 +1 @@
Confirm your Alliance Auth account email address

View File

@@ -0,0 +1,15 @@
{% load i18n %}{% autoescape off %}
{% blocktrans %}You're receiving this email because you requested a password reset for your
user account.{% endblocktrans %}
{% trans "Please go to the following page and choose a new password:" %}
{% block reset_link %}
{{domain}}{% url 'password_reset_confirm' uidb64=uid token=token %}
{% endblock %}
{% trans "Your username, in case you've forgotten:" %} {{ user.get_username }}
{% trans "Thanks for using our site!" %}
{% blocktrans %}Your IT Team{% endblocktrans %}
{% endautoescape %}

View File

@@ -0,0 +1,14 @@
{% extends 'public/middle_box.html' %}
{% load bootstrap %}
{% load i18n %}
{% load static %}
{% block title %}Register{% endblock %}
{% block middle_box_content %}
<form class="form-signin" role="form" action="" method="POST">
{% csrf_token %}
{{ form|bootstrap }}
<br/>
<button class="btn btn-lg btn-primary btn-block" type="submit">{% trans "Submit" %}</button>
<br/>
</form>
{% endblock %}

View File

@@ -0,0 +1,288 @@
from unittest import mock
from django.test import TestCase
from django.contrib.auth.models import User
from allianceauth.tests.auth_utils import AuthUtils
from .models import CharacterOwnership, UserProfile, State, get_guest_state
from .backends import StateBackend
from .tasks import check_character_ownership
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from esi.models import Token
class BackendTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.main_character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.alt_character = EveCharacter.objects.create(
character_id=2,
character_name='Alt Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.unclaimed_character = EveCharacter.objects.create(
character_id=3,
character_name='Unclaimed Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
cls.user = AuthUtils.create_user('test_user', disconnect_signals=True)
AuthUtils.disconnect_signals()
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')
UserProfile.objects.update_or_create(user=cls.user, defaults={'main_character': cls.main_character})
AuthUtils.connect_signals()
def test_authenticate_main_character(self):
t = Token(character_id=self.main_character.character_id, character_owner_hash='1')
user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user)
def test_authenticate_alt_character(self):
t = Token(character_id=self.alt_character.character_id, character_owner_hash='2')
user = StateBackend().authenticate(token=t)
self.assertEquals(user, self.user)
def test_authenticate_unclaimed_character(self):
t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3')
user = StateBackend().authenticate(token=t)
self.assertNotEqual(user, self.user)
self.assertEqual(user.username, 'Unclaimed_Character')
self.assertEqual(user.profile.main_character, self.unclaimed_character)
def test_iterate_username(self):
t = Token(character_id=self.unclaimed_character.character_id,
character_name=self.unclaimed_character.character_name, character_owner_hash='3')
username = StateBackend().authenticate(token=t).username
t.character_owner_hash = '4'
username_1 = StateBackend().authenticate(token=t).username
t.character_owner_hash = '5'
username_2 = StateBackend().authenticate(token=t).username
self.assertNotEqual(username, username_1, username_2)
self.assertTrue(username_1.endswith('_1'))
self.assertTrue(username_2.endswith('_2'))
class CharacterOwnershipTestCase(TestCase):
@classmethod
def setUpTestData(cls):
cls.user = AuthUtils.create_user('user', disconnect_signals=True)
cls.alt_user = AuthUtils.create_user('alt_user', disconnect_signals=True)
cls.character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
def test_create_ownership(self):
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(character=self.character)
self.assertEquals(co.user, self.user)
self.assertEquals(co.owner_hash, '1')
def test_transfer_ownership(self):
Token.objects.create(
user=self.user,
character_id=self.character.character_id,
character_name=self.character.character_name,
character_owner_hash='1',
)
Token.objects.create(
user=self.alt_user,
character_id=self.character.character_id,
character_name=self.character.character_name,
character_owner_hash='2',
)
co = CharacterOwnership.objects.get(character=self.character)
self.assertNotEqual(self.user, co.user)
self.assertEquals(self.alt_user, co.user)
def test_clear_main_character(self):
Token.objects.create(
user=self.user,
character_id=self.character.character_id,
character_name=self.character.character_name,
character_owner_hash='1',
)
self.user.profile.main_character = self.character
self.user.profile.save()
Token.objects.create(
user=self.alt_user,
character_id=self.character.character_id,
character_name=self.character.character_name,
character_owner_hash='2',
)
self.user = User.objects.get(pk=self.user.pk)
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):
@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.guest_state = get_guest_state()
cls.test_character = EveCharacter.objects.get(character_id='1')
cls.test_corporation = EveCorporationInfo.objects.create(corporation_id='1', corporation_name='Test Corp',
corporation_ticker='TEST', member_count=1)
cls.test_alliance = EveAllianceInfo.objects.create(alliance_id='1', alliance_name='Test Alliance',
alliance_ticker='TEST', executor_corp_id='1')
cls.member_state = State.objects.create(
name='Test Member',
priority=150,
)
def _refresh_user(self):
self.user = User.objects.get(pk=self.user.pk)
def test_state_assignment_on_character_change(self):
self.member_state.member_characters.add(self.test_character)
self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state)
self.member_state.member_characters.remove(self.test_character)
self._refresh_user()
self.assertEquals(self.user.profile.state, self.guest_state)
def test_state_assignment_on_corporation_change(self):
self.member_state.member_corporations.add(self.test_corporation)
self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state)
self.member_state.member_corporations.remove(self.test_corporation)
self._refresh_user()
self.assertEquals(self.user.profile.state, self.guest_state)
def test_state_assignment_on_alliance_addition(self):
self.member_state.member_alliances.add(self.test_alliance)
self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state)
self.member_state.member_alliances.remove(self.test_alliance)
self._refresh_user()
self.assertEquals(self.user.profile.state, self.guest_state)
def test_state_assignment_on_higher_priority_state_creation(self):
self.member_state.member_characters.add(self.test_character)
higher_state = State.objects.create(
name='Higher State',
priority=200,
)
higher_state.member_characters.add(self.test_character)
self._refresh_user()
self.assertEquals(higher_state, self.user.profile.state)
higher_state.member_characters.clear()
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
self.member_state.member_characters.clear()
def test_state_assignment_on_lower_priority_state_creation(self):
self.member_state.member_characters.add(self.test_character)
lower_state = State.objects.create(
name='Lower State',
priority=125,
)
lower_state.member_characters.add(self.test_character)
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
lower_state.member_characters.clear()
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
self.member_state.member_characters.clear()
def test_state_assignment_on_priority_change(self):
self.member_state.member_characters.add(self.test_character)
lower_state = State.objects.create(
name='Lower State',
priority=125,
)
lower_state.member_characters.add(self.test_character)
self._refresh_user()
lower_state.priority = 500
lower_state.save()
self._refresh_user()
self.assertEquals(lower_state, self.user.profile.state)
lower_state.priority = 125
lower_state.save()
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
def test_state_assignment_on_state_deletion(self):
self.member_state.member_characters.add(self.test_character)
higher_state = State.objects.create(
name='Higher State',
priority=200,
)
higher_state.member_characters.add(self.test_character)
self._refresh_user()
self.assertEquals(higher_state, self.user.profile.state)
higher_state.delete()
self.assertFalse(State.objects.filter(name='Higher State').count())
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
def test_state_assignment_on_public_toggle(self):
self.member_state.member_characters.add(self.test_character)
higher_state = State.objects.create(
name='Higher State',
priority=200,
)
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
higher_state.public = True
higher_state.save()
self._refresh_user()
self.assertEquals(higher_state, self.user.profile.state)
higher_state.public = False
higher_state.save()
self._refresh_user()
self.assertEquals(self.member_state, self.user.profile.state)
def test_state_assignment_on_active_changed(self):
self.member_state.member_characters.add(self.test_character)
self.user.is_active = False
self.user.save()
self._refresh_user()
self.assertEquals(self.user.profile.state, self.guest_state)
self.user.is_active = True
self.user.save()
self._refresh_user()
self.assertEquals(self.user.profile.state, self.member_state)

View File

@@ -0,0 +1,17 @@
from django.conf.urls import url
from django.contrib.auth.decorators import login_required
from django.views.generic.base import TemplateView
from . import views
app_name = 'authentication'
urlpatterns = [
url(r'^$', login_required(TemplateView.as_view(template_name='authentication/dashboard.html')),),
url(r'^account/login/$', TemplateView.as_view(template_name='public/login.html'), name='login'),
url(r'^account/characters/main/$', views.main_character_change, name='change_main_character'),
url(r'^account/characters/add/$', views.add_character, name='add_character'),
url(r'^help/$', login_required(TemplateView.as_view(template_name='registered/help.html')), name='help'),
url(r'^dashboard/$',
login_required(TemplateView.as_view(template_name='authentication/dashboard.html')), name='dashboard'),
]

View File

@@ -0,0 +1,151 @@
import logging
from django.conf import settings
from django.contrib import messages
from django.contrib.auth import login, authenticate
from django.contrib.auth.decorators import login_required
from django.contrib.auth.models import User
from django.core import signing
from django.core.urlresolvers import reverse
from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from esi.decorators import token_required
from registration.backends.hmac.views import RegistrationView as BaseRegistrationView, \
ActivationView as BaseActivationView, REGISTRATION_SALT
from registration.signals import user_registered
from .models import CharacterOwnership
from .forms import RegistrationForm
logger = logging.getLogger(__name__)
@login_required
@token_required(scopes=settings.LOGIN_TOKEN_SCOPES)
def main_character_change(request, token):
logger.debug("main_character_change called by user %s for character %s" % (request.user, token.character_name))
try:
co = CharacterOwnership.objects.get(character__character_id=token.character_id, user=request.user)
except CharacterOwnership.DoesNotExist:
if not CharacterOwnership.objects.filter(character__character_id=token.character_id).exists():
co = CharacterOwnership.objects.create_by_token(token)
else:
messages.error(request, 'Cannot change main character to %(char)s: character owned by a different account.' % ({'char': token.character_name}))
co = None
if co:
request.user.profile.main_character = co.character
request.user.profile.save(update_fields=['main_character'])
messages.success(request, _('Changed main character to %(char)s') % {"char": co.character})
logger.info('Changed user %(user)s main character to %(char)s' % ({'user': request.user, 'char': co.character}))
return redirect("authentication:dashboard")
@token_required(new=True, scopes=settings.LOGIN_TOKEN_SCOPES)
def add_character(request, token):
if CharacterOwnership.objects.filter(character__character_id=token.character_id).filter(
owner_hash=token.character_owner_hash).filter(user=request.user).exists():
messages.success(request, _('Added %(name)s to your account.'% ({'name': token.character_name})))
else:
messages.error(request, _('Failed to add %(name)s to your account: they already have an account.' % ({'name': token.character_name})))
return redirect('authentication:dashboard')
"""
Override the HMAC two-step registration view to accommodate the three-step registration required.
Step 1: OAuth token to create user and profile.
Step 2: Get email and send activation link (but do not save email).
Step 3: Get link, save email and activate.
Step 1 is necessary to automatically assign character ownership and a main character, both of which require a saved User
model - this means the ensuing registration form cannot create the user because it already exists.
Email is not saved to the user model in Step 2 as a way of differentiating users who have not yet completed registration
(is_active=False) and users who have been disabled by an admin (is_active=False, email present).
Because of this, the email address needs to be assigned in Step 3 after clicking the link, which means the link must
have the email address embedded much like the username. Key creation and decoding is overridden to support this action.
"""
# Step 1
@token_required(new=True, scopes=settings.LOGIN_TOKEN_SCOPES)
def sso_login(request, token):
user = authenticate(token=token)
if user and user.is_active:
login(request, user)
return redirect(request.POST.get('next', request.GET.get('next', 'authentication:dashboard')))
elif user and not user.email:
# Store the new user PK in the session to enable us to identify the registering user in Step 2
request.session['registration_uid'] = user.pk
# Go to Step 2
return redirect('registration_register')
else:
messages.error(request, _('Unable to authenticate as the selected character.'))
return redirect(settings.LOGIN_URL)
# Step 2
class RegistrationView(BaseRegistrationView):
form_class = RegistrationForm
success_url = 'authentication:dashboard'
def dispatch(self, *args, **kwargs):
# 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(
pk=self.request.session.get('registration_uid')).exists():
messages.error(self.request, _('Registration token has expired.'))
return redirect(settings.LOGIN_URL)
return super(RegistrationView, self).dispatch(*args, **kwargs)
def register(self, form):
user = User.objects.get(pk=self.request.session.get('registration_uid'))
user.email = form.cleaned_data['email']
user_registered.send(self.__class__, user=user, request=self.request)
# Go to Step 3
self.send_activation_email(user)
return user
def get_activation_key(self, user):
return signing.dumps(obj=[getattr(user, User.USERNAME_FIELD), user.email], salt=REGISTRATION_SALT)
def get_email_context(self, activation_key):
context = super(RegistrationView, self).get_email_context(activation_key)
context['url'] = context['site'].domain + reverse('registration_activate', args=[activation_key])
return context
# Step 3
class ActivationView(BaseActivationView):
def validate_key(self, activation_key):
try:
dump = signing.loads(activation_key, salt=REGISTRATION_SALT,
max_age=settings.ACCOUNT_ACTIVATION_DAYS * 86400)
return dump
except signing.BadSignature:
return None
def activate(self, *args, **kwargs):
dump = self.validate_key(kwargs.get('activation_key'))
if dump:
user = self.get_user(dump[0])
if user:
user.email = dump[1]
user.is_active = True
user.save()
return user
return False
def registration_complete(request):
messages.success(request, _('Sent confirmation email. Please follow the link to confirm your email address.'))
return redirect('authentication:login')
def activation_complete(request):
messages.success(request, _('Confirmed your email address. Please login to continue.'))
return redirect('authentication:dashboard')
def registration_closed(request):
messages.error(request, _('Registraion of new accounts it not allowed at this time.'))
return redirect('authentication:login')