Compare commits

...

101 Commits

Author SHA1 Message Date
Adarnof
ce66bdcbd4 Copy v1 database after creating new one for v2 if updating. 2018-03-02 20:18:23 -05:00
Adarnof
f65e563c0c Update project setup description to match repo and docs.
Thanks @soratidus999
2018-03-02 11:26:35 -05:00
Adarnof
e860ba6c22 Remove pre-v1.13 changelog. It's on the wiki. 2018-03-02 04:03:01 -05:00
Adarnof
50b6605a43 Set folder permissions once user is created.
Remove redundant gunicorn webserver config.

Closes #984
2018-03-02 03:37:14 -05:00
Adarnof
d181200642 Mention Discord bot will never come online.
Update phpbb3 version.
2018-03-02 03:12:36 -05:00
Adarnof
386ba25a44 Add explanation of auth project structure.
Update troubleshooting guide for v2.
Remove homoglyph data files included with confusable-homoglyphs>=3.0
2018-03-02 02:58:55 -05:00
Adarnof
5331d194df Instruct selection of only necessary SSO scopes.
Standardize instructions of adding app to settings.
2018-03-02 01:52:16 -05:00
Adarnof
814ecd233e Most apps use menu item hooks now. 2018-02-28 16:49:39 -05:00
Adarnof
f9a8ac4e9b Bump version to b3 in anticipation of next release. 2018-02-28 13:20:36 -05:00
Adarnof
1bd5eecd54 Correct old template URLs.
Remove redundant name from fatlink.
Remove optimer app dependency.
And other general cleanup.

Thanks @TargetZ3R0
2018-02-28 13:16:54 -05:00
Adarnof
2fa1d9998d Handle custom table prefixes on service databases.
Closes #987

Thanks @Ric878
2018-02-28 10:56:30 -05:00
Adarnof
9d9cfebd9e Specify character set in database to avoid key length errors.
Default database setting uncommented.

Addresses #985
2018-02-27 19:56:19 -05:00
Adarnof
cc8a7a18d2 Hook URLs require logged in user with a main character.
Should prevent anything else like #983

Heavily inspired by https://gist.github.com/garrypolley/3762045#gistcomment-2089316
2018-02-26 22:50:58 -05:00
Adarnof
552c795041 Update project description. 2018-02-24 01:46:53 -05:00
Adarnof
3d757e8d90 Make sure wheel is in venv.
It's hit-and-miss when venvs are created it seems. Doesn't hut to install even if it's already there.
2018-02-24 01:26:31 -05:00
Adarnof
1b5ecaed80 Requirement to sign license agreement for contributing.
Restructure user lists.
Remove reference to long-dead alliance in description.
2018-02-24 00:13:47 -05:00
Adarnof
77c93ed96b Correct broken template tags. 2018-02-23 22:23:42 -05:00
Adarnof
3eeed99af2 Basic fleetup instructions including settings. 2018-02-23 21:47:55 -05:00
Adarnof
a143dfbb37 Add Timerboard Structures, step 2 (#976)
Added additional labels for added structure types
(cherry picked from commit d8f4d56dd8)
2018-02-23 21:36:57 -05:00
Adarnof
6b1da3b18a Briefly document the state system.
Ensure add and delete permissions are created. Not sure why I prevented them - maybe a holdover from an earlier iteration of the state system?
2018-02-23 21:34:48 -05:00
Adarnof
f0894f3415 Update group management docs showing merged admin pages.
Give groupmanagement app a more friendly display name.
2018-02-23 21:01:34 -05:00
Adarnof
539295c1b7 Remove unpopulated first/last name fields from list display.
Include only useful information in list display.
2018-02-23 20:33:07 -05:00
Adarnof
54f91a5bfb Simplify admin inline titles.
More descriptive name format config admin list.
2018-02-23 20:25:59 -05:00
Adarnof
f3c0d05c39 Embed authgroup into group admin.
Mirror authgroup admin permissions from group model.
Delete authgroup permissions.
2018-02-23 18:21:58 -05:00
Adarnof
9f9cc7ed42 Embed profile into user admin.
Restrict main character choices to non-main characters or current main.
If superuser, allow choosing any non-main character.
Proxy user permissions to base model.
Allow all staff to see permission list but not edit.
2018-02-23 17:58:14 -05:00
Adarnof
814b2da0ca Redirect all signals from admin proxy models. 2018-02-23 14:44:12 -05:00
Adarnof
7a9bb0c84b Centralize portrait/logo URL creation. 2018-02-23 12:54:21 -05:00
Adarnof
36ae2af29b Deduplicate login tokens. 2018-02-23 12:25:06 -05:00
Adarnof
d192f23e6e Require exactly django-registration==2.4
This is the newest version which allows installation on Django 2.0 (and indeed does work) that also provides the scheme context to emails.
2018-02-23 11:57:06 -05:00
Adarnof
67cd0cd55c Reassess user groups on state change. 2018-02-23 01:50:03 -05:00
Adarnof
9e53d8b429 Correct migration dependency.
I have no idea what 0016 is, but I'm nuking my dev env to be safe.
2018-02-23 01:26:59 -05:00
Adarnof
f5abf82b95 Allow mapping of states to Teamspeak3 groups.
Addresses #950

Happy now, @colcrunch ?
2018-02-23 01:22:51 -05:00
Adarnof
8dd3a25b52 Remove mentions of no longer used invite code. 2018-02-22 18:50:59 -05:00
Adarnof
d0aa46db08 Accept any ordering of groups in test.
I have no idea why the order is reversing itself. Doesn't matter for functionality which of them comes first. This just checks they're both in there and a comma to separate them.
2018-02-22 18:10:24 -05:00
Adarnof
f0ff70566b Include expected state group in test. 2018-02-22 18:01:14 -05:00
Adarnof
efecf5113b Correct celery eagerness during tests.
I have no idea why this setting name has to be changed. The docs for Celery 4.1.0 (installed) indicate it should be called CELERY_TASK_ALWAYS_EAGER - even with namespace removal TASK_ALWAYS_EAGER doesn't work, but the "old" name of CELERY_ALWAYS_EAGER does.
2018-02-22 18:00:03 -05:00
Adarnof
980569de68 Do not attempt to serialize User models 2018-02-22 17:54:35 -05:00
Adarnof
9c74952607 Correct CorpStats tests. 2018-02-22 17:44:59 -05:00
Adarnof
70c2a4a6e4 Use new endpoint for adding Discord users.
Closes #974
2018-02-22 17:41:38 -05:00
Adarnof
99b136b824 Delete Discord users if they've left the server.
Closes #968
2018-02-22 15:37:29 -05:00
Adarnof
ae4116c0f6 Create new role with desired attributes in one call. 2018-02-22 15:22:35 -05:00
Adarnof
3080d7d868 Prevent new roles from being sorted separately.
Addresses #969
2018-02-22 14:43:59 -05:00
Adarnof
08cf8ae1d6 Capture permission changes from proxy model on admin 2018-02-22 14:28:23 -05:00
Adarnof
3ed0f873f3 Capture signals sent by admin proxy models.
This will prevent those weird missing UserProfile and AuthGroup errors.
Add logging to authentication signals.
Correct reverse migration authservicesinfo creation.
Rename proxy models so they look better on the admin site.
2018-02-22 14:25:43 -05:00
Adarnof
5060d3f408 Ensure login tokens always get attached to the user. 2018-02-22 13:40:02 -05:00
Adarnof
ef24bea562 Put missing logout redirect setting back.
Not sure what I did with it.
2018-02-22 13:31:50 -05:00
Adarnof
c18efaa33d Default login scopes to publicData
We need a refresh token to monitor character ownership but don't need any scopes explicitly. publicData provides no private information but grants a refresh token.

https://github.com/ccpgames/sso-issues/issues/17

Rumor has it this scope isn't going away with CREST.

adarnauth-esi will automatically create a new scope model when it encounters one it doesn't recognize.
2018-02-22 13:27:47 -05:00
Adarnof
b6b14f6f1c Ensure all columns are perfectly aligned.
Prevent sorting/searching portraits and killboard links.
Default sorting to character names.
2018-02-22 13:06:07 -05:00
Adarnof
a90a52f426 Ensure api backoff returns result of decorated function
(cherry picked from commit 91ec924acc)
2018-02-22 02:10:27 -05:00
Adarnof
bd5ea38446 Add a warning against editing base.py
Beautify local.py by removing big block comments.
Move some settings back to base.py which don't need to be in local.py
2018-02-21 22:32:23 -05:00
Adarnof
f8248f46e5 Update docs to reflect refreshing changes 2018-02-21 22:08:45 -05:00
Adarnof
b09c454bf0 Can be updated by any user who can view
Thanks @ghoti
2018-02-21 22:02:46 -05:00
colcrunch
d825689da4 Add settings section to service docs. Remove references to settings.py. (#942)
Standardized the addition of settings instructions.
Changed all references of local.py to a more generic 'auth project settings file'.
Included basic apache and nginx configs.
Include database creation steps.
Instruct users to restart gunicorn and celery after altering settings.
Include missing TS3 celerybeat schedule.
2018-02-21 20:39:54 -05:00
Adarnof
a64dda2a2e Handle HTTP429 on nickname API endpoint
Closes #971
2018-02-21 17:47:20 -05:00
colcrunch
8ce8789631 Discord Sanitization Removal (#947)
No need to sanitize, just prune.
2018-02-21 17:40:41 -05:00
Adarnof
2b2f367c30 Updated Strucure Choices
Added Refineries, and a Moon Mining Option
Also changed spacing to be consistent and be easier to read
(cherry picked from commit 0474fa6d17)
2018-02-21 17:23:36 -05:00
Adarnof
4d194457d8 Include state in service group sync.
The "empty" group will never appear as all users have a state so it has been removed.

I haven't yet found a good way to apply this to Teamspeak - perhaps go back to the token generation logic and create one with a user's state instead of "Member" and exempt those names from group sync?

Addresses #950
2018-02-21 17:11:22 -05:00
Adarnof
6f7cf8805d Correct background resizing on Firefox
https://stackoverflow.com/a/24104710
2018-02-20 15:29:56 -05:00
Adarnof
36e39503c8 Use symbolic links for supervisor conf 2018-02-20 14:46:28 -05:00
Adarnof
e7a24c9cd4 Explicitly forbid logging in as allianceserver 2018-02-20 13:07:16 -05:00
Adarnof
bd8a8922cc Detailed superuser main character instructions 2018-02-20 12:52:00 -05:00
Adarnof
396b2e0fb6 Select all esi scopes when registering application
Thanks @RacerX330
2018-02-20 12:35:11 -05:00
Adarnof
36e382fadb Move SSL header instructions to SSL block 2018-02-20 12:29:12 -05:00
Adarnof
d2666f2440 Instructions for accessing superuser account 2018-02-20 12:20:07 -05:00
Adarnof
397ca97f0f Add missing context to teamspeak join template.
Closes #967
2018-02-13 18:07:34 -05:00
Adarnof
631bb439a4 Remove celery setting namespace.
Somehow it prevents celerybeat tasks from being registered.
Doesn't work with or without the namespace prefix on CELERYBEAT_SCHEDULE

Thanks @warlof
2018-02-12 21:55:19 -05:00
Adarnof
a4003e188e Correct string formatting format
Thanks @warlof
2018-02-12 18:53:23 -05:00
Adarnof
f4a9ba2db8 Remove reference to deleted function. 2018-02-09 01:16:27 -05:00
Adarnof
895a62c475 Include leading http(s) on activation link.
Closes #961
2018-02-09 01:11:35 -05:00
Basraah
ac5a0d9dcb Remove obsolete function call 2018-02-04 19:15:10 +10:00
Adarnof
b8644d5c93 Remove unnecessary next URL from lang select.
This is automatically determined by the lang select view if not specified.
https://docs.djangoproject.com/en/2.0/topics/i18n/translation/#the-set-language-redirect-view
Closes #958
2018-02-02 19:55:59 -05:00
ghoti
4d8baf1af0 V2 Fix redirect issues in HRApps (#951)
Fix redirect issues in HRApps
Allow HR managers to delete reviewed apps
2018-01-11 19:06:10 -05:00
Basraah
f70987de09 Version bump 2018-01-09 12:38:49 +10:00
Basraah
9d02b1530c Update contributors 2018-01-09 12:34:21 +10:00
Basraah
3d532dae01 Fix celery in tests
There's actually a better way to structure tests involving celery since
4.0, but that can wait for some time in the future.
2018-01-09 12:11:54 +10:00
Adarnof
02247b067f Include INSTALLED_APPS setting
Clean up section headers and add a few more.
2018-01-08 10:50:24 -05:00
Basraah
63c2668171 Change static path to match default install 2018-01-08 21:15:19 +10:00
Basraah
5575039126 Remove obsolete section on automatic groups 2018-01-08 21:07:29 +10:00
Basraah
f97c8f2ce4 Fix issue causing queue length query to hang 2018-01-08 18:20:29 +10:00
Adarnof
6baab1d006 New apache guide for proxying to wsgi worker 2018-01-08 00:52:10 -05:00
Adarnof
17adf04860 Correct CentOS supervisor config folder.
Install gunicorn before starting auth project.
2018-01-08 00:14:17 -05:00
Adarnof
f871ecb425 Namespace celery settings to correct broker issues 2018-01-07 19:50:34 -05:00
Adarnof
4a425cde78 Set the email from address for SparkPost
Thanks @colcrunch
2018-01-07 19:39:27 -05:00
Basraah
f56252b0cc Fix celery broker url config 2018-01-06 12:16:30 +10:00
Basraah
7ae6c66beb Fix registration of services signals 2018-01-06 12:08:53 +10:00
Basraah
be90fb96ea Remove unnecessary param 2017-12-28 10:18:50 +10:00
Basraah
dd3350b169 Fix field name 2017-12-28 09:20:11 +10:00
Basraah
cdd1ba1fe3 Use coerce to allow PEP440 to partially work 2017-12-23 15:05:51 +10:00
Basraah
10ea12c867 Prevent error on bad version 2017-12-23 14:44:26 +10:00
Basraah
e6358d948a Fix bug preventing users being added to alliance autogroups
Will need `python manage.py migrate eve_autogroups zero` before updating
2017-12-23 14:43:05 +10:00
Adarnof
1101572f78 Set token user to allow ownership creation 2017-12-22 12:20:55 -05:00
Adarnof
0cf8836832 Use primary key for FK assignment 2017-12-22 11:32:45 -05:00
Adarnof
6e4562b0e6 Don't rely on manager in migration 2017-12-22 11:19:53 -05:00
Basraah
856f1e176a Fix copy paste error, add extra debug output 2017-12-22 11:35:49 +10:00
Adarnof
1653a57e7b Do not set request main character
Thanks @GhostMB
2017-12-21 19:28:39 -05:00
Adarnof
5f03e580c2 Make manager available in migrations
Thanks @mmolitor87
2017-12-21 15:37:54 -05:00
Adarnof
d370ae48a2 Full import path
Python doesn't want to play nice.
2017-12-21 15:32:42 -05:00
Adarnof
38baeba254 Load signals with app 2017-12-21 15:25:53 -05:00
Adarnof
478f9b9390 Fix typo
Thanks @mmolitor87
2017-12-21 15:20:20 -05:00
Adarnof
82ad3821c4 Avoid using model property in migration
https://stackoverflow.com/a/3315547

Thanks @mmolitor87
2017-12-21 15:15:01 -05:00
Adarnof
07afaf12d5 Fix bracket.
Thanks @mmolitor87
2017-12-21 14:13:16 -05:00
115 changed files with 1579 additions and 1106 deletions

View File

@@ -7,8 +7,7 @@ Alliance Auth
[![Coverage Status](https://coveralls.io/repos/github/allianceauth/allianceauth/badge.svg?branch=master)](https://coveralls.io/github/allianceauth/allianceauth?branch=master)
EVE service auth to help corps, alliances, and coalitions manage services.
Built for "The 99 Percent" open for anyone to use.
An auth system for EVE Online to help in-game organizations manage online service access.
[Read the docs here.](http://allianceauth.rtfd.io)
@@ -17,21 +16,18 @@ Built for "The 99 Percent" open for anyone to use.
Active Developers:
- [Adarnof](https://github.com/Adarnof)
- [Basraah](https://github.com/basraah)
- [Adarnof](https://github.com/adarnof/)
- [Basraah](https://github.com/basraah/)
Beta Testers / Bug Fixers:
- [ghoti](https://github.com/ghoti)
- [ghoti](https://github.com/ghoti/)
- [mmolitor87](https://github.com/mmolitor87/)
- [kaezon](https://github.com/kaezon/)
- [orbitroom](https://github.com/orbitroom/)
- [tehfiend](https://github.com/tehfiend/)
Special thanks to [Nikdoof](https://github.com/nikdoof/), as his [auth](https://github.com/nikdoof/test-auth) was the foundation for the original work on this project.
Past Beta Testers / Bug Fixers:
- TrentBartlem (Testing and Bug Fixes)
- IskFiend (Bug Fixes and Server Configuration)
- Mr McClain (Bug Fixes and server configuration)
Special Thanks:
- Thanks to Nikdoof, without his old auth implementation this project wouldn't be as far as it is now.
### Contributing
Make sure you have signed the [License Agreement](https://developers.eveonline.com/resource/license-agreement) by logging in at [https://developers.eveonline.com](https://developers.eveonline.com) before submitting any pull requests.

View File

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

View File

@@ -1,11 +1,15 @@
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.models import User, Permission
from django.contrib.auth.models import User as BaseUser, Permission as BasePermission
from django.utils.text import slugify
from django.db.models import Q
from allianceauth.services.hooks import ServicesHook
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
from django.dispatch import receiver
from allianceauth.authentication.models import State, get_guest_state, CharacterOwnership, UserProfile
from allianceauth.hooks import get_hooks
from allianceauth.eveonline.models import EveCharacter
from django.forms import ModelForm
def make_service_hooks_update_groups_action(service):
@@ -38,6 +42,47 @@ def make_service_hooks_sync_nickname_action(service):
return sync_nickname
class QuerysetModelForm(ModelForm):
# allows specifying FK querysets through kwarg
def __init__(self, querysets=None, *args, **kwargs):
querysets = querysets or {}
super().__init__(*args, **kwargs)
for field, qs in querysets.items():
self.fields[field].queryset = qs
class UserProfileInline(admin.StackedInline):
model = UserProfile
readonly_fields = ('state',)
form = QuerysetModelForm
verbose_name = ''
verbose_name_plural = 'Profile'
def get_formset(self, request, obj=None, **kwargs):
# main_character field can only show current value or unclaimed alts
# if superuser, allow selecting from any unclaimed main
query = Q()
if obj and obj.profile.main_character:
query |= Q(pk=obj.profile.main_character_id)
if request.user.is_superuser:
query |= Q(userprofile__isnull=True)
else:
query |= Q(character_ownership__user=obj)
qs = EveCharacter.objects.filter(query)
formset = super().get_formset(request, obj=obj, **kwargs)
def get_kwargs(self, index):
return {'querysets': {'main_character': EveCharacter.objects.filter(query)}}
formset.get_form_kwargs = get_kwargs
return formset
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
class UserAdmin(BaseUserAdmin):
"""
Extending Django's UserAdmin model
@@ -62,6 +107,25 @@ class UserAdmin(BaseUserAdmin):
return actions
list_filter = BaseUserAdmin.list_filter + ('profile__state',)
inlines = BaseUserAdmin.inlines + [UserProfileInline]
list_display = ('username', 'email', 'get_main_character', 'get_state', 'is_active')
def get_main_character(self, obj):
return obj.profile.main_character
get_main_character.short_description = "Main Character"
def get_state(self, obj):
return obj.profile.state
get_state.short_description = "State"
def has_change_permission(self, request, obj=None):
return request.user.has_perm('auth.change_user')
def has_add_permission(self, request, obj=None):
return request.user.has_perm('auth.add_user')
def has_delete_permission(self, request, obj=None):
return request.user.has_perm('auth.delete_user')
@admin.register(State)
@@ -96,31 +160,19 @@ class StateAdmin(admin.ModelAdmin):
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')
def has_add_permission(self, request):
return False
class PermissionAdmin(admin.ModelAdmin):
actions = None
readonly_fields = [field.name for field in Permission._meta.fields]
readonly_fields = [field.name for field in BasePermission._meta.fields]
list_display = ('admin_name', 'name', 'codename', 'content_type')
list_filter = ('content_type__app_label',)
@@ -134,23 +186,61 @@ class PermissionAdmin(admin.ModelAdmin):
def has_delete_permission(self, request, obj=None):
return False
def has_module_permission(self, request):
return True
def has_change_permission(self, request, obj=None):
# can see list but not edit it
return not obj
# Hack to allow registration of django.contrib.auth models in our authentication app
class ProxyUser(User):
class User(BaseUser):
class Meta:
proxy = True
verbose_name = User._meta.verbose_name
verbose_name_plural = User._meta.verbose_name_plural
verbose_name = BaseUser._meta.verbose_name
verbose_name_plural = BaseUser._meta.verbose_name_plural
class ProxyPermission(Permission):
class Permission(BasePermission):
class Meta:
proxy = True
verbose_name = Permission._meta.verbose_name
verbose_name_plural = Permission._meta.verbose_name_plural
verbose_name = BasePermission._meta.verbose_name
verbose_name_plural = BasePermission._meta.verbose_name_plural
try:
admin.site.unregister(User)
admin.site.unregister(BaseUser)
finally:
admin.site.register(ProxyUser, UserAdmin)
admin.site.register(ProxyPermission, PermissionAdmin)
admin.site.register(User, UserAdmin)
admin.site.register(Permission, PermissionAdmin)
@receiver(pre_save, sender=User)
def redirect_pre_save(sender, signal=None, *args, **kwargs):
pre_save.send(BaseUser, *args, **kwargs)
@receiver(post_save, sender=User)
def redirect_post_save(sender, signal=None, *args, **kwargs):
post_save.send(BaseUser, *args, **kwargs)
@receiver(pre_delete, sender=User)
def redirect_pre_delete(sender, signal=None, *args, **kwargs):
pre_delete.send(BaseUser, *args, **kwargs)
@receiver(post_delete, sender=User)
def redirect_post_delete(sender, signal=None, *args, **kwargs):
post_delete.send(BaseUser, *args, **kwargs)
@receiver(m2m_changed, sender=User.groups.through)
def redirect_m2m_changed_groups(sender, signal=None, *args, **kwargs):
m2m_changed.send(BaseUser, *args, **kwargs)
@receiver(m2m_changed, sender=User.user_permissions.through)
def redirect_m2m_changed_permissions(sender, signal=None, *args, **kwargs):
m2m_changed.send(BaseUser, *args, **kwargs)

View File

@@ -39,6 +39,7 @@ class StateBackend(ModelBackend):
# insecure legacy main check for pre-sso registration auth installs
profile = UserProfile.objects.get(main_character__character_id=token.character_id)
# attach an ownership
token.user = profile.user
CharacterOwnership.objects.create_by_token(token)
return profile.user
except UserProfile.DoesNotExist:

View File

@@ -0,0 +1,37 @@
from django.conf.urls import include
from functools import wraps
from django.shortcuts import redirect
from django.contrib import messages
from django.utils.translation import gettext_lazy as _
from django.contrib.auth.decorators import login_required
def user_has_main_character(user):
return bool(user.profile.main_character)
def decorate_url_patterns(urls, decorator):
url_list, app_name, namespace = include(urls)
def process_patterns(url_patterns):
for pattern in url_patterns:
if hasattr(pattern, 'url_patterns'):
# this is an include - apply to all nested patterns
process_patterns(pattern.url_patterns)
else:
# this is a pattern
pattern.callback = decorator(pattern.callback)
process_patterns(url_list)
return url_list, app_name, namespace
def main_character_required(view_func):
@wraps(view_func)
def _wrapped_view(request, *args, **kwargs):
if user_has_main_character(request.user):
return view_func(request, *args, **kwargs)
messages.error(request, _('A main character is required to perform that action. Add one below.'))
return redirect('authentication:dashboard')
return login_required(_wrapped_view)

View File

@@ -103,16 +103,16 @@ def populate_ownerships(apps, schema_editor):
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())]
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
# find newest refreshable token and use it as basis for CharacterOwnership
ts = tokens.filter(character_id=c_id).exclude(refresh_token__isnull=True).order_by('created')
if ts.exists():
token = ts[0]
char = EveCharacter.objects.get(character_id=token.character_id)
CharacterOwnership.objects.create(user_id=token.user_id, character_id=char.id, owner_hash=token.character_owner_hash)
def create_profiles(apps, schema_editor):
@@ -145,7 +145,7 @@ def recreate_authservicesinfo(apps, schema_editor):
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()])
AuthServicesInfo.objects.bulk_create([AuthServicesInfo(user_id=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'):
@@ -203,7 +203,6 @@ class Migration(migrations.Migration):
('permissions', models.ManyToManyField(blank=True, to='auth.Permission')),
],
options={
'default_permissions': ('change',),
'ordering': ['-priority'],
},
),
@@ -233,7 +232,7 @@ class Migration(migrations.Migration):
),
migrations.RunPython(disable_passwords, migrations.RunPython.noop),
migrations.CreateModel(
name='ProxyPermission',
name='Permission',
fields=[
],
options={
@@ -247,7 +246,7 @@ class Migration(migrations.Migration):
],
),
migrations.CreateModel(
name='ProxyUser',
name='User',
fields=[
],
options={

View File

@@ -3,7 +3,7 @@ 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.db.models.signals import pre_save, post_save, pre_delete, m2m_changed
from django.dispatch import receiver, Signal
from esi.models import Token
@@ -11,7 +11,6 @@ from allianceauth.eveonline.models import EveCharacter
logger = logging.getLogger(__name__)
state_changed = Signal(providing_args=['user', 'state'])
@@ -32,23 +31,27 @@ def trigger_state_check(state):
@receiver(m2m_changed, sender=State.member_characters.through)
def state_member_characters_changed(sender, instance, action, *args, **kwargs):
if action.startswith('post_'):
logger.debug('State {} member characters changed. Re-evaluating membership.'.format(instance))
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_'):
logger.debug('State {} member corporations changed. Re-evaluating membership.'.format(instance))
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_'):
logger.debug('State {} member alliances changed. Re-evaluating membership.'.format(instance))
trigger_state_check(instance)
@receiver(post_save, sender=State)
def state_saved(sender, instance, *args, **kwargs):
logger.debug('State {} saved. Re-evaluating membership.'.format(instance))
trigger_state_check(instance)
@@ -59,6 +62,7 @@ def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
if not created:
update_fields = kwargs.pop('update_fields', []) or []
if 'state' not in update_fields:
logger.debug('Profile for {} saved without state change. Re-evaluating state.'.format(instance.user))
instance.assign_state()
@@ -66,12 +70,15 @@ def reassess_on_profile_save(sender, instance, created, *args, **kwargs):
def create_required_models(sender, instance, created, *args, **kwargs):
# ensure all users have a model
if created:
logger.debug('User {} created. Creating default UserProfile.'.format(instance))
UserProfile.objects.get_or_create(user=instance)
@receiver(post_save, sender=Token)
def record_character_ownership(sender, instance, created, *args, **kwargs):
if created:
logger.debug('New token for {0} character {1} saved. Evaluating ownership.'.format(instance.user,
instance.character_name))
if instance.user:
query = Q(owner_hash=instance.character_owner_hash) & Q(user=instance.user)
else:
@@ -80,10 +87,15 @@ def record_character_ownership(sender, instance, created, *args, **kwargs):
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:
logger.debug('Token is for a new character. Creating model for {0} ({1})'.format(instance.character_name,
instance.character_id))
EveCharacter.objects.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():
if instance.user and not CharacterOwnership.objects.filter(
character__character_id=instance.character_id).exists():
logger.debug("Character {0} is not yet owned. Assigning ownership to {1}".format(instance.character_name,
instance.user))
CharacterOwnership.objects.update_or_create(character=char,
defaults={'owner_hash': instance.character_owner_hash,
'user': instance.user})
@@ -92,6 +104,8 @@ def record_character_ownership(sender, instance, created, *args, **kwargs):
@receiver(pre_delete, sender=CharacterOwnership)
def validate_main_character(sender, instance, *args, **kwargs):
if instance.user.profile.main_character == instance.character:
logger.debug("Ownership of a main character {0} has been revoked. Resetting {1} main character.".format(
instance.character, instance.user))
# clear main character as user no longer owns them
instance.user.profile.main_character = None
instance.user.profile.save()
@@ -100,8 +114,15 @@ def validate_main_character(sender, instance, *args, **kwargs):
@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():
logger.debug(
"Token for a main character {0} is being deleted. Ensuring there are valid tokens to refresh.".format(
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).exclude(pk=instance.pk).exists():
if not Token.objects.filter(character_id=instance.character_id).filter(user=profile.user).exclude(
pk=instance.pk).require_valid().exists():
logger.debug(
"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()
@@ -114,8 +135,11 @@ def assign_state_on_active_change(sender, instance, *args, **kwargs):
old_instance = User.objects.get(pk=instance.pk)
if old_instance.is_active != instance.is_active:
if instance.is_active:
logger.debug("User {0} has been activated. Assigning state.".format(instance))
instance.profile.assign_state()
else:
logger.debug(
"User {0} has been deactivated. Revoking state and assigning to guest state.".format(instance))
instance.profile.state = get_guest_state()
instance.profile.save(update_fields=['state'])
@@ -124,6 +148,8 @@ def assign_state_on_active_change(sender, instance, *args, **kwargs):
def check_state_on_character_update(sender, instance, *args, **kwargs):
# if this is a main character updating, check that user's state
try:
logger.debug("Character {0} has been saved. Assessing owner's state for changes.".format(instance))
instance.userprofile.assign_state()
except UserProfile.DoesNotExist:
logger.debug("Character {0} is not a main character. No state assessment required.".format(instance))
pass

View File

@@ -21,7 +21,7 @@
<table class="table">
<tr>
<td class="text-center"><img class="ra-avatar"
src="https://image.eveonline.com/Character/{{ main.character_id }}_128.jpg">
src="{{ main.portrait_url_128 }}">
</td>
</tr>
<tr>
@@ -57,7 +57,7 @@
</div>
{% endwith %}
{% else %}
<div class="alert alert-danger" role="alert">{% trans "Missing main character model." %}</div>
<div class="alert alert-danger" role="alert">{% trans "No main character set." %}</div>
{% endif %}
<div class="clearfix"></div>
<div class="col-xs-6">
@@ -102,8 +102,7 @@
{% 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 class="text-center"><img class="ra-avatar img-circle" src="{{ char.portrait_url_32 }}">
</td>
<td class="text-center">{{ char.character_name }}</td>
<td class="text-center">{{ char.corporation_name }}</td>

View File

@@ -17,7 +17,7 @@
<style>
body {
background: url('{% static 'authentication/img/background.jpg' %}') no-repeat scroll;
background: url('{% static 'authentication/img/background.jpg' %}') no-repeat center center fixed;
-webkit-background-size: cover;
-moz-background-size: cover;
-o-background-size: cover;
@@ -48,4 +48,4 @@
{% endblock %}
</div>
</body>
</html>
</html>

View File

@@ -2,7 +2,6 @@
<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 %}
@@ -12,4 +11,4 @@
{% endfor %}
</select>
</form>
</div>
</div>

View File

@@ -2,7 +2,7 @@ You're receiving this email because someone has entered this email address while
If this was you, please go to the following URL to confirm your email address:
{{ url }}
{{ scheme }}://{{ url }}
This link will expire in {{ expiration_days }} day(s).

View File

@@ -8,6 +8,61 @@ from .backends import StateBackend
from .tasks import check_character_ownership
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
from esi.models import Token
from allianceauth.authentication.decorators import main_character_required
from django.test.client import RequestFactory
from django.http.response import HttpResponse
from django.contrib.auth.models import AnonymousUser
from django.conf import settings
from django.shortcuts import reverse
from urllib import parse
MODULE_PATH = 'allianceauth.authentication'
class DecoratorTestCase(TestCase):
@staticmethod
@main_character_required
def dummy_view(*args, **kwargs):
return HttpResponse(status=200)
@classmethod
def setUpTestData(cls):
cls.main_user = AuthUtils.create_user('main_user', disconnect_signals=True)
cls.no_main_user = AuthUtils.create_user('no_main_user', disconnect_signals=True)
main_character = EveCharacter.objects.create(
character_id=1,
character_name='Main Character',
corporation_id=1,
corporation_name='Corp',
corporation_ticker='CORP',
)
CharacterOwnership.objects.create(user=cls.main_user, character=main_character, owner_hash='1')
cls.main_user.profile.main_character = main_character
def setUp(self):
self.request = RequestFactory().get('/test/')
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_login_redirect(self, m):
setattr(self.request, 'user', AnonymousUser())
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
self.assertEqual(parse.urlparse(url).path, reverse(settings.LOGIN_URL))
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_main_character_redirect(self, m):
setattr(self.request, 'user', self.no_main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 302)
url = getattr(response, 'url', None)
self.assertEqual(url, reverse('authentication:dashboard'))
@mock.patch(MODULE_PATH + '.decorators.messages')
def test_successful_request(self, m):
setattr(self.request, 'user', self.main_user)
response = self.dummy_view(self.request)
self.assertEqual(response.status_code, 200)
class BackendTestCase(TestCase):

View File

@@ -10,6 +10,7 @@ from django.urls import reverse
from django.shortcuts import redirect
from django.utils.translation import ugettext_lazy as _
from esi.decorators import token_required
from esi.models import Token
from registration.backends.hmac.views import RegistrationView as BaseRegistrationView, \
ActivationView as BaseActivationView, REGISTRATION_SALT
from registration.signals import user_registered
@@ -71,17 +72,22 @@ have the email address embedded much like the username. Key creation and decodin
@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)
if user:
token.user = user
if Token.objects.exclude(pk=token.pk).equivalent_to(token).require_valid().exists():
token.delete()
else:
token.save()
if user.is_active:
login(request, user)
return redirect(request.POST.get('next', request.GET.get('next', 'authentication:dashboard')))
elif 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')
messages.error(request, _('Unable to authenticate as the selected character.'))
return redirect(settings.LOGIN_URL)
# Step 2

View File

@@ -121,8 +121,11 @@ class CorpStats(models.Model):
m.main_character and int(m.main_character.character_id) == int(
m.character_id)])
def visible_to(self, user):
return CorpStats.objects.filter(pk=self.pk).visible_to(user).exists()
def can_update(self, user):
return user.is_superuser or user == self.token.user
return self.token.user == user or self.visible_to(user)
def corp_logo(self, size=128):
return "https://image.eveonline.com/Corporation/%s_%s.png" % (self.corp.corporation_id, size)
@@ -179,4 +182,4 @@ class CorpMember(models.Model):
if item.startswith('portrait_url_'):
size = item.strip('portrait_url_')
return self.portrait_url(size)
return super(CorpMember, self).__getattr__(item)
return self.__getattribute__(item)

View File

@@ -9,9 +9,9 @@
<tr>
<td class="text-center col-lg-6
{% if corpstats.corp.alliance %}{% else %}col-lg-offset-3{% endif %}"><img
class="ra-avatar" src="{{ corpstats.corp_logo }}"></td>
class="ra-avatar" src="{{ corpstats.corp.logo_url_128 }}"></td>
{% if corpstats.corp.alliance %}
<td class="text-center col-lg-6"><img class="ra-avatar" src="{{ corpstats.alliance_logo }}">
<td class="text-center col-lg-6"><img class="ra-avatar" src="{{ corpstats.alliance.logo_url_128 }}">
</td>
{% endif %}
</tr>
@@ -70,6 +70,7 @@
{% for alt in main.alts %}
{% if forloop.first %}
<tr>
<th></th>
<th class="text-center">{% trans "Character" %}</th>
<th class="text-center">{% trans "Corporation" %}</th>
<th class="text-center">{% trans "Alliance" %}</th>
@@ -77,10 +78,15 @@
</tr>
{% endif %}
<tr>
<td class="text-center">{{ alt.character_name }}</td>
<td class="text-center">{{ alt.corporation_name }}</td>
<td class="text-center">{{ alt.alliance_name }}</td>
<td class="text-center">
<td class="text-center" style="width:5%">
<div class="thumbnail" style="border: 0 none; box-shadow: none; background: transparent;">
<img src="https://image.eveonline.com/Character/{{ alt.character_id }}_32.jpg" class="img-circle">
</div>
</td>
<td class="text-center" style="width:30%">{{ alt.character_name }}</td>
<td class="text-center" style="width:30%">{{ alt.corporation_name }}</td>
<td class="text-center" style="width:30%">{{ alt.alliance_name }}</td>
<td class="text-center" style="width:5%">
<a href="https://zkillboard.com/character/{{ alt.character_id }}/"
class="label label-danger" target="_blank">
{% trans "Killboard" %}
@@ -175,9 +181,25 @@
{% endblock %}
{% block extra_script %}
$(document).ready(function(){
$('#table-mains').DataTable();
$('#table-members').DataTable();
$('#table-unregistered').DataTable();
$('#table-mains').DataTable({
"columnDefs": [
{ "sortable": false, "targets": [1] },
],
});
$('#table-members').DataTable({
"columnDefs": [
{ "searchable": false, "targets": [0, 2] },
{ "sortable": false, "targets": [0, 2] },
],
"order": [[ 1, "asc" ]],
});
$('#table-unregistered').DataTable({
"columnDefs": [
{ "searchable": false, "targets": [0, 2] },
{ "sortable": false, "targets": [0, 2] },
],
"order": [[ 1, "asc" ]],
});
});
{% endblock %}

View File

@@ -4,3 +4,6 @@ from django.apps import AppConfig
class EveAutogroupsConfig(AppConfig):
name = 'allianceauth.eveonline.autogroups'
label = 'eve_autogroups'
def ready(self):
import allianceauth.eveonline.autogroups.signals

View File

@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.5 on 2017-09-29 14:44
# Generated by Django 1.11.6 on 2017-12-23 04:30
from __future__ import unicode_literals
from django.db import migrations, models
@@ -29,40 +29,31 @@ class Migration(migrations.Migration):
('alliance_name_source', models.CharField(choices=[('ticker', 'Ticker'), ('name', 'Full name')], default='name', max_length=20)),
('replace_spaces', models.BooleanField(default=False)),
('replace_spaces_with', models.CharField(blank=True, default='', help_text='Any spaces in the group name will be replaced with this.', max_length=10)),
('states', models.ManyToManyField(related_name='autogroups', to='authentication.State')),
],
),
migrations.CreateModel(
name='ManagedGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
],
),
migrations.CreateModel(
name='ManagedAllianceGroup',
fields=[
('managedgroup_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='eve_autogroups.ManagedGroup')),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('alliance', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.EveAllianceInfo')),
('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eve_autogroups.AutogroupsConfig')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
],
bases=('eve_autogroups.managedgroup',),
options={
'abstract': False,
},
),
migrations.CreateModel(
name='ManagedCorpGroup',
fields=[
('managedgroup_ptr', models.OneToOneField(auto_created=True, on_delete=django.db.models.deletion.CASCADE, parent_link=True, primary_key=True, serialize=False, to='eve_autogroups.ManagedGroup')),
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('config', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eve_autogroups.AutogroupsConfig')),
('corp', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eveonline.EveCorporationInfo')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group')),
],
bases=('eve_autogroups.managedgroup',),
),
migrations.AddField(
model_name='managedgroup',
name='config',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='eve_autogroups.AutogroupsConfig'),
),
migrations.AddField(
model_name='managedgroup',
name='group',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='auth.Group'),
options={
'abstract': False,
},
),
migrations.AddField(
model_name='autogroupsconfig',
@@ -74,4 +65,9 @@ class Migration(migrations.Migration):
name='corp_managed_groups',
field=models.ManyToManyField(help_text="A list of corporation groups created and maintained by this AutogroupConfig. You should not edit this list unless you know what you're doing.", related_name='corp_managed_config', through='eve_autogroups.ManagedCorpGroup', to='auth.Group'),
),
migrations.AddField(
model_name='autogroupsconfig',
name='states',
field=models.ManyToManyField(related_name='autogroups', to='authentication.State'),
),
]

View File

@@ -110,11 +110,12 @@ class AutogroupsConfig(models.Model):
group = None
try:
if not self.alliance_groups or not self.user_entitled_to_groups(user):
logger.debug('User {} does not have required state'.format(user))
logger.debug('User {} does not have required state for alliance group membership'.format(user))
return
else:
alliance = user.profile.main_character.alliance
if alliance is None:
logger.debug('User {} alliance is None, cannot update group membership'.format(user))
return
group = self.get_alliance_group(alliance)
except EveAllianceInfo.DoesNotExist:
@@ -123,8 +124,9 @@ class AutogroupsConfig(models.Model):
except AttributeError:
logger.warning('User {} does not have a main character. Group membership not updated'.format(user))
finally:
self.remove_user_from_corp_groups(user, except_group=group)
self.remove_user_from_alliance_groups(user, except_group=group)
if group is not None:
logger.debug('Adding user {} to alliance group {}'.format(user, group))
user.groups.add(group)
@transaction.atomic
@@ -132,7 +134,7 @@ class AutogroupsConfig(models.Model):
group = None
try:
if not self.corp_groups or not self.user_entitled_to_groups(user):
logger.debug('User {} does not have required state'.format(user))
logger.debug('User {} does not have required state for corp group membership'.format(user))
else:
corp = user.profile.main_character.corporation
group = self.get_corp_group(corp)
@@ -144,6 +146,7 @@ class AutogroupsConfig(models.Model):
finally:
self.remove_user_from_corp_groups(user, except_group=group)
if group is not None:
logger.debug('Adding user {} to corp group {}'.format(user, group))
user.groups.add(group)
@transaction.atomic
@@ -184,13 +187,15 @@ class AutogroupsConfig(models.Model):
"""
Deletes ALL managed alliance groups
"""
self.alliance_managed_groups.all().delete()
for g in self.alliance_managed_groups.all():
g.delete()
def delete_corp_managed_groups(self):
"""
Deletes ALL managed corp groups
"""
self.corp_managed_groups.all().delete()
for g in self.corp_managed_groups.all():
g.delete()
def get_alliance_group_name(self, alliance: EveAllianceInfo) -> str:
if self.alliance_name_source == self.OPT_TICKER:
@@ -225,6 +230,9 @@ class ManagedGroup(models.Model):
group = models.ForeignKey(Group, on_delete=models.CASCADE)
config = models.ForeignKey(AutogroupsConfig, on_delete=models.CASCADE)
class Meta:
abstract = True
class ManagedCorpGroup(ManagedGroup):
corp = models.ForeignKey(EveCorporationInfo, on_delete=models.CASCADE)

View File

@@ -82,6 +82,7 @@ class AutogroupsConfigTestCase(TestCase):
# Act
obj.update_alliance_group_membership(self.member)
obj.update_corp_group_membership(self.member) # check for no side effects
group = obj.create_alliance_group(self.alliance)
group_qs = Group.objects.filter(pk=group.pk)

View File

@@ -35,6 +35,15 @@ class EveAllianceInfo(models.Model):
def __str__(self):
return self.alliance_name
def logo_url(self, size=32):
return "https://image.eveonline.com/Alliance/%s_%s.png" % (self.alliance_id, size)
def __getattr__(self, item):
if item.startswith('logo_url_'):
size = item.strip('logo_url_')
return self.logo_url(size)
return self.__getattribute__(item)
class EveCorporationInfo(models.Model):
corporation_id = models.CharField(max_length=254, unique=True)
@@ -60,6 +69,15 @@ class EveCorporationInfo(models.Model):
def __str__(self):
return self.corporation_name
def logo_url(self, size=32):
return "https://image.eveonline.com/Corporation/%s_%s.png" % (self.corporation_id, size)
def __getattr__(self, item):
if item.startswith('logo_url_'):
size = item.strip('logo_url_')
return self.logo_url(size)
return self.__getattribute__(item)
class EveCharacter(models.Model):
character_id = models.CharField(max_length=254, unique=True)
@@ -107,3 +125,12 @@ class EveCharacter(models.Model):
def __str__(self):
return self.character_name
def portrait_url(self, size=32):
return "https://image.eveonline.com/Character/%s_%s.jpg" % (self.character_id, size)
def __getattr__(self, item):
if item.startswith('portrait_url_'):
size = item.strip('portrait_url_')
return self.portrait_url(size)
return self.__getattribute__(item)

View File

@@ -2,11 +2,9 @@
from django import forms
from django.utils.translation import ugettext_lazy as _
from allianceauth.optimer.models import OpTimer
class FatlinkForm(forms.Form):
fatname = forms.CharField(label=_('Name of fat-link'), required=True)
fleet = forms.CharField(label=_("Fleet Name"), max_length=50)
duration = forms.IntegerField(label=_("Duration of fat-link"), required=True, initial=30, min_value=1,
max_value=2147483647)
fleet = forms.ModelChoiceField(label=_("Fleet"), queryset=OpTimer.objects.all().order_by('operation_name'))
max_value=2147483647, help_text=_('minutes'))

View File

@@ -2,12 +2,10 @@
# Generated by Django 1.10.1 on 2016-09-05 21:39
from __future__ import unicode_literals
import datetime
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
from django.utils.timezone import utc
from django.utils import timezone
import allianceauth.fleetactivitytracking.models
@@ -36,9 +34,9 @@ class Migration(migrations.Migration):
name='Fatlink',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('fatdatetime', models.DateTimeField(default=datetime.datetime(2016, 9, 5, 21, 39, 17, 307954, tzinfo=utc))),
('fatdatetime', models.DateTimeField(default=timezone.now)),
('duration', models.PositiveIntegerField()),
('fleet', models.CharField(default=b'', max_length=254)),
('fleet', models.CharField(default='', max_length=254)),
('name', models.CharField(max_length=254)),
('hash', models.CharField(max_length=254, unique=True)),
('creator', models.ForeignKey(on_delete=models.SET(

View File

@@ -0,0 +1,22 @@
# Generated by Django 2.0.2 on 2018-02-28 18:00
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('fleetactivitytracking', '0004_make_strings_more_stringy'),
]
operations = [
migrations.RemoveField(
model_name='fatlink',
name='name',
),
migrations.AlterField(
model_name='fatlink',
name='fleet',
field=models.CharField(max_length=254),
),
]

View File

@@ -12,13 +12,12 @@ def get_sentinel_user():
class Fatlink(models.Model):
fatdatetime = models.DateTimeField(default=timezone.now)
duration = models.PositiveIntegerField()
fleet = models.CharField(max_length=254, default="")
name = models.CharField(max_length=254)
fleet = models.CharField(max_length=254)
hash = models.CharField(max_length=254, unique=True)
creator = models.ForeignKey(User, on_delete=models.SET(get_sentinel_user))
def __str__(self):
return self.name
return self.fleet
class Fat(models.Model):

View File

@@ -16,7 +16,7 @@
</div>
<div class="col-lg-10 col-sm-2">
<div class="alert alert-danger" role="alert">{% trans "Character not registered!" %}</div>
{% trans "This character is not part of any registered API-key. You must go to" %} <a href=" {% url 'auth_api_key_management' %}">{% trans "API key management</a> and add an API with the character on before being able to click fleet attendance links." %}
{% trans "This character is not associated with an auth account." %} <a href=" {% url 'authentication:add_character' %}">{% trans "Add it here" %}</a> {% trans "before attempting to click fleet attendance links." %}
</div>
</div>
</div>

View File

@@ -10,12 +10,12 @@
<h1 class="page-header text-center">{% blocktrans %}Participation data statistics for {{ month }}, {{ year }}{% endblocktrans %}
{% if char_id %}
<div class="text-right">
<a href="{% url 'fatlink:user_statistics_month' char_id previous_month|date:"Y" previous_month|date:"m" %}" class="btn btn-info">{% trans "Previous month" %}</a>
<a href="{% url 'fatlink:user_statistics_month' char_id next_month|date:"Y" next_month|date:"m" %}" class="btn btn-info">{% trans "Next month" %}</a>
<a href="{% url 'fatlink:user_statistics_month' char_id previous_month|date:'Y' previous_month|date:'m' %}" class="btn btn-info">{% trans "Previous month" %}</a>
<a href="{% url 'fatlink:user_statistics_month' char_id next_month|date:'Y' next_month|date:'m' %}" class="btn btn-info">{% trans "Next month" %}</a>
</div>
{% endif %}
</h1>
<h2>{% blocktrans %}{{ user }} has collected {{ n_fats }} links this month.{% endblocktrans %}</h2>
<h2>{% blocktrans %}{{ user }} has collected {{ n_fats }} link{{ n_fats|pluralize }} this month.{% endblocktrans %}</h2>
<table class="table table-responsive">
<tr>
<th class="col-md-2 text-center">{% trans "Ship" %}</th>
@@ -29,26 +29,24 @@
{% endfor %}
</table>
{% if created_fats %}
<h2>{% blocktrans %}{{ user }} has created {{ n_created_fats }} links this month.{% endblocktrans %}</h2>
<h2>{% blocktrans %}{{ user }} has created {{ n_created_fats }} link{{ n_created_fats|pluralize }} this month.{% endblocktrans %}</h2>
{% if created_fats %}
<table class="table">
<tr>
<th class="text-center">{% trans "Name" %}</th>
<th class="text-center">{% trans "Creator" %}</th>
<th class="text-center">{% trans "Fleet" %}</th>
<th class="text-center">{% trans "Creator" %}</th>
<th class="text-center">{% trans "Eve Time" %}</th>
<th class="text-center">{% trans "Duration" %}</th>
<th class="text-center">{% trans "Edit" %}</th>
</tr>
{% for link in created_fats %}
<tr>
<td class="text-center"><a href="{% url 'auth_click_fatlink_view' %}{{ link.hash }}/{{ link.name }}">{{ link.name }}</a></td>
<td class="text-center"><a href="{% url 'fatlink:click' link.hash %}" class="label label-primary">{{ link.fleet }}</a></td>
<td class="text-center">{{ link.creator.username }}</td>
<td class="text-center">{{ link.fleet }}</td>
<td class="text-center">{{ link.fatdatetime }}</td>
<td class="text-center">{{ link.duration }}</td>
<td class="text-center">
<a href="{% url 'auth_modify_fatlink_view' %}{{ link.hash }}/{{ link.name }}">
<a href="{% url 'fatlink:modify' link.hash %}">
<button type="button" class="btn btn-info"><span
class="glyphicon glyphicon-edit"></span></button>
</a>

View File

@@ -24,7 +24,7 @@
{% if fats %}
<table class="table table-responsive">
<tr>
<th class="text-center">{% trans "fatname" %}</th>
<th class="text-center">{% trans "Fleet" %}</th>
<th class="text-center">{% trans "Character" %}</th>
<th class="text-center">{% trans "System" %}</th>
<th class="text-center">{% trans "Ship" %}</th>
@@ -32,7 +32,7 @@
</tr>
{% for fat in fats %}
<tr>
<td class="text-center">{{ fat.fatlink.name }}</td>
<td class="text-center">{{ fat.fatlink.fleet }}</td>
<td class="text-center">{{ fat.character.character_name }}</td>
{% if fat.station != "No Station" %}
<td class="text-center">{% blocktrans %}Docked in {% endblocktrans %}{{ fat.system }}</td>
@@ -79,13 +79,13 @@
</tr>
{% for link in fatlinks %}
<tr>
<td class="text-center"><a href="{% url 'fatlink:click_fatlink' %}{{ link.hash }}/{{ link.name }}">{{ link.name }}</a></td>
<td class="text-center"><a href="{% url 'fatlink:click' link.hash %}" class="label label-primary">{{ link.fleet }}</a></td>
<td class="text-center">{{ link.creator.username }}</td>
<td class="text-center">{{ link.fleet }}</td>
<td class="text-center">{{ link.fatdatetime }}</td>
<td class="text-center">{{ link.duration }}</td>
<td class="text-center">
<a href="{% url 'fatlink:modify' %}{{ link.hash }}/{{ link.name }}" class="btn btn-info">
<a href="{% url 'fatlink:modify' link.hash %}" class="btn btn-info">
<span class="glyphicon glyphicon-edit"></span>
</a>
</td>

View File

@@ -25,10 +25,6 @@ urlpatterns = [
views.fatlink_monthly_personal_statistics_view,
name='user_statistics_month'),
url(r'^create/$', views.create_fatlink_view, name='create'),
url(r'^modify/$', views.modify_fatlink_view, name='modify'),
url(r'^modify/(?P<hash>[a-zA-Z0-9_-]+)/([a-z0-9_-]+)$',
views.modify_fatlink_view),
url(r'^link/$', views.fatlink_view, name='click_fatlink'),
url(r'^link/(?P<hash>[a-zA-Z0-9]+)/(?P<fatname>[a-z0-9_-]+)/$',
views.click_fatlink_view),
url(r'^modify/(?P<fat_hash>[a-zA-Z0-9_-]+)/$', views.modify_fatlink_view, name='modify'),
url(r'^link/(?P<fat_hash>[a-zA-Z0-9]+)/$', views.click_fatlink_view, name='click'),
]

View File

@@ -1,8 +1,6 @@
import datetime
import logging
import os
import random
import string
from allianceauth.authentication.models import CharacterOwnership
from django.contrib import messages
@@ -17,7 +15,7 @@ from esi.decorators import token_required
from allianceauth.eveonline.providers import provider
from .forms import FatlinkForm
from .models import Fatlink, Fat
from slugify import slugify
from django.utils.crypto import get_random_string
from allianceauth.eveonline.models import EveAllianceInfo
from allianceauth.eveonline.models import EveCharacter
@@ -181,7 +179,7 @@ def fatlink_personal_statistics_view(request, year=datetime.date.today().year):
personal_fats = Fat.objects.select_related('fatlink').filter(user=user).order_by('id')
monthlystats = [0 for month in range(1, 13)]
monthlystats = [0 for i in range(1, 13)]
for fat in personal_fats:
fatdate = fat.fatlink.fatdatetime
@@ -236,8 +234,8 @@ def fatlink_monthly_personal_statistics_view(request, year, month, char_id=None)
@login_required
@token_required(
scopes=['esi-location.read_location.v1', 'esi-location.read_ship_type.v1', 'esi-universe.read_structures.v1'])
def click_fatlink_view(request, token, hash, fatname):
fatlink = get_object_or_404(Fatlink, hash=hash, name=fatname)
def click_fatlink_view(request, token, fat_hash=None):
fatlink = get_object_or_404(Fatlink, hash=fat_hash)
if (timezone.now() - fatlink.fatdatetime) < datetime.timedelta(seconds=(fatlink.duration * 60)):
@@ -298,12 +296,11 @@ def create_fatlink_view(request):
logger.debug("Submitting fleetactivitytracking by user %s" % request.user)
if form.is_valid():
fatlink = Fatlink()
fatlink.name = slugify(form.cleaned_data["fatname"])
fatlink.fleet = form.cleaned_data["fleet"]
fatlink.duration = form.cleaned_data["duration"]
fatlink.fatdatetime = timezone.now()
fatlink.creator = request.user
fatlink.hash = ''.join(random.choice(string.ascii_letters + string.digits) for i in range(10))
fatlink.hash = get_random_string(length=15)
try:
fatlink.full_clean()
fatlink.save()
@@ -331,25 +328,19 @@ def create_fatlink_view(request):
@login_required
@permission_required('auth.fleetactivitytracking')
def modify_fatlink_view(request, hash=""):
def modify_fatlink_view(request, fat_hash=None):
logger.debug("modify_fatlink_view called by user %s" % request.user)
if not hash:
return redirect('fatlink:view')
try:
fatlink = Fatlink.objects.get(hash=hash)
except Fatlink.DoesNotExist:
raise Http404
fatlink = get_object_or_404(Fatlink, hash=fat_hash)
if request.GET.get('removechar', None):
character_id = request.GET.get('removechar')
character = EveCharacter.objects.get(character_id=character_id)
logger.debug("Removing character %s from fleetactivitytracking %s" % (character.character_name, fatlink.name))
logger.debug("Removing character %s from fleetactivitytracking %s" % (character.character_name, fatlink))
Fat.objects.filter(fatlink=fatlink).filter(character=character).delete()
if request.GET.get('deletefat', None):
logger.debug("Removing fleetactivitytracking %s" % fatlink.name)
logger.debug("Removing fleetactivitytracking %s" % fatlink)
fatlink.delete()
return redirect('fatlink:view')

View File

@@ -1,28 +1,68 @@
from django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.models import Group as BaseGroup
from django.db.models.signals import pre_save, post_save, pre_delete, post_delete, m2m_changed
from django.dispatch import receiver
from .models import AuthGroup
from .models import GroupRequest
class AuthGroupAdmin(admin.ModelAdmin):
"""
Admin model for AuthGroup
"""
class AuthGroupInlineAdmin(admin.StackedInline):
model = AuthGroup
filter_horizontal = ('group_leaders',)
fields = ('description', 'group_leaders', 'internal', 'hidden', 'open', 'public')
verbose_name_plural = 'Auth Settings'
verbose_name = ''
def has_add_permission(self, request):
return False
def has_delete_permission(self, request, obj=None):
return False
def has_change_permission(self, request, obj=None):
return request.user.has_perm('auth.change_group')
class ProxyGroup(Group):
class GroupAdmin(admin.ModelAdmin):
filter_horizontal = ('permissions',)
inlines = (AuthGroupInlineAdmin,)
class Group(BaseGroup):
class Meta:
proxy = True
verbose_name = Group._meta.verbose_name
verbose_name_plural = Group._meta.verbose_name_plural
verbose_name = BaseGroup._meta.verbose_name
verbose_name_plural = BaseGroup._meta.verbose_name_plural
try:
admin.site.unregister(Group)
admin.site.unregister(BaseGroup)
finally:
admin.site.register(ProxyGroup)
admin.site.register(Group, GroupAdmin)
admin.site.register(GroupRequest)
admin.site.register(AuthGroup, AuthGroupAdmin)
@receiver(pre_save, sender=Group)
def redirect_pre_save(sender, signal=None, *args, **kwargs):
pre_save.send(BaseGroup, *args, **kwargs)
@receiver(post_save, sender=Group)
def redirect_post_save(sender, signal=None, *args, **kwargs):
post_save.send(BaseGroup, *args, **kwargs)
@receiver(pre_delete, sender=Group)
def redirect_pre_delete(sender, signal=None, *args, **kwargs):
pre_delete.send(BaseGroup, *args, **kwargs)
@receiver(post_delete, sender=Group)
def redirect_post_delete(sender, signal=None, *args, **kwargs):
post_delete.send(BaseGroup, *args, **kwargs)
@receiver(m2m_changed, sender=Group.permissions.through)
def redirect_m2m_changed_permissions(sender, signal=None, *args, **kwargs):
m2m_changed.send(BaseGroup, *args, **kwargs)

View File

@@ -4,3 +4,4 @@ from django.apps import AppConfig
class GroupManagementConfig(AppConfig):
name = 'allianceauth.groupmanagement'
label = 'groupmanagement'
verbose_name = 'Group Management'

View File

@@ -15,7 +15,7 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
name='ProxyGroup',
name='Group',
fields=[
],
options={

View File

@@ -0,0 +1,38 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-02-23 23:09
from __future__ import unicode_literals
from django.db import migrations
def delete_permissions(apps, schema_editor):
AuthGroup = apps.get_model('groupmanagement', 'AuthGroup')
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
ct = ContentType.objects.get_for_model(AuthGroup)
Permission.objects.filter(content_type=ct).delete()
def recreate_permissions(apps, schema_editor):
AuthGroup = apps.get_model('groupmanagement', 'AuthGroup')
ContentType = apps.get_model('contenttypes', 'ContentType')
Permission = apps.get_model('auth', 'Permission')
ct = ContentType.objects.get_for_model(AuthGroup)
Permission.objects.create(content_type=ct, name='Can add auth group', codename='add_authgroup')
Permission.objects.create(content_type=ct, name='Can delete auth group', codename='delete_authgroup')
Permission.objects.create(content_type=ct, name='Can change auth group', codename='change_authgroup')
class Migration(migrations.Migration):
dependencies = [
('groupmanagement', '0007_on_delete'),
]
operations = [
migrations.AlterModelOptions(
name='authgroup',
options={'default_permissions': (), 'permissions': (('request_groups', 'Can request non-public groups'),)},
),
migrations.RunPython(delete_permissions, recreate_permissions)
]

View File

@@ -4,8 +4,6 @@ from django.db import models
from django.db.models.signals import post_save
from django.dispatch import receiver
from allianceauth.eveonline.models import EveCharacter
class GroupRequest(models.Model):
status = models.CharField(max_length=254)
@@ -76,6 +74,7 @@ class AuthGroup(models.Model):
permissions = (
("request_groups", u"Can request non-public groups"),
)
default_permissions = tuple()
@receiver(post_save, sender=Group)

View File

@@ -303,7 +303,6 @@ def group_request_add(request, group_id):
grouprequest.status = _('Pending')
grouprequest.group = group
grouprequest.user = request.user
grouprequest.main_char = request.user.profile.main_character
grouprequest.leave_request = False
grouprequest.save()
logger.info("Created group request for user %s to group %s" % (request.user, Group.objects.get(id=group_id)))
@@ -333,7 +332,6 @@ def group_request_leave(request, group_id):
grouprequest.status = _('Pending')
grouprequest.group = group
grouprequest.user = request.user
grouprequest.main_char = request.user.profile.main_character
grouprequest.leave_request = True
grouprequest.save()
logger.info("Created group leave request for user %s to group %s" % (request.user, Group.objects.get(id=group_id)))

View File

@@ -154,6 +154,12 @@
class="btn btn-primary">
<span class="glyphicon glyphicon-eye-open"></span>
</a>
{% if perms.hrapplications.delete_application %}
<a href="(% url 'hrapplications:remove' app.id %}"
class="btn btn-danger">
<span class="glyphicon glyphicon-remove"></span>
</a>
{% endif %}
</td>
</tr>
{% endfor %}

View File

@@ -67,7 +67,7 @@ def hr_application_create_view(request, form_id=None):
""))
response.save()
logger.info("%s created %s" % (request.user, application))
return redirect('hrapplications:view')
return redirect('hrapplications:personal_view', application.id)
else:
questions = app_form.questions.all()
return render(request, 'hrapplications/create.html',
@@ -95,7 +95,7 @@ def hr_application_personal_view(request, app_id):
return render(request, 'hrapplications/view.html', context=context)
else:
logger.warn("User %s not authorized to view %s" % (request.user, app))
return redirect('hrapplications:view')
return redirect('hrapplications:personal_view')
@login_required
@@ -110,7 +110,7 @@ def hr_application_personal_removal(request, app_id):
logger.warn("User %s attempting to delete reviewed app %s" % (request.user, app))
else:
logger.warn("User %s not authorized to delete %s" % (request.user, app))
return redirect('hrapplications:view')
return redirect('hrapplications:index')
@login_required
@@ -158,7 +158,7 @@ def hr_application_remove(request, app_id):
logger.info("User %s deleting %s" % (request.user, app))
app.delete()
notify(app.user, "Application Deleted", message="Your application to %s was deleted." % app.form.corp)
return redirect('hrapplications:view')
return redirect('hrapplications:index')
@login_required
@@ -175,7 +175,7 @@ def hr_application_approve(request, app_id):
level="success")
else:
logger.warn("User %s not authorized to approve %s" % (request.user, app))
return redirect('hrapplications:view')
return redirect('hrapplications:index')
@login_required
@@ -192,7 +192,7 @@ def hr_application_reject(request, app_id):
level="danger")
else:
logger.warn("User %s not authorized to reject %s" % (request.user, app))
return redirect('hrapplications:view')
return redirect('hrapplications:index')
@login_required

View File

@@ -8,6 +8,7 @@ from allianceauth.tests.auth_utils import AuthUtils
class PermissionsToolViewsTestCase(WebTest):
def setUp(self):
self.member = AuthUtils.create_member('auth_member')
AuthUtils.add_main_character(self.member, 'test character', '1234', '2345', 'test corp', 'testc')
self.member.email = 'auth_member@example.com'
self.member.save()
self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True)

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
from .celery import app as celery_app

View File

@@ -1,14 +1,10 @@
# -*- coding: UTF-8 -*-
"""
Django settings for alliance_auth project.
DO NOT EDIT THIS FILE
Generated by 'django-admin startproject' using Django 1.10.1.
For more information on this file, see
https://docs.djangoproject.com/en/1.10/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.10/ref/settings/
This settings file contains everything needed for Alliance Auth projects to function.
It gets overwritten by the 'allianceauth update' command.
If you wish to make changes, overload the setting in your project's settings file (local.py).
"""
import os
@@ -17,7 +13,6 @@ from django.contrib import messages
from celery.schedules import crontab
INSTALLED_APPS = [
# Core apps - required to function
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
@@ -38,6 +33,8 @@ INSTALLED_APPS = [
'allianceauth.thirdparty.navhelper',
]
SECRET_KEY = "wow I'm a really bad default secret key"
# Celery configuration
BROKER_URL = 'redis://localhost:6379/0'
CELERYBEAT_SCHEDULER = "django_celery_beat.schedulers.DatabaseScheduler"
@@ -90,7 +87,7 @@ LANGUAGES = (
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'DIRS': [os.path.join(PROJECT_DIR, 'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
@@ -148,8 +145,11 @@ USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/1.10/howto/static-files/
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(PROJECT_DIR, 'static'),
]
STATIC_ROOT = os.path.join(BASE_DIR, 'static')
# Bootstrap messaging css workaround
MESSAGE_TAGS = {
@@ -178,40 +178,22 @@ DATABASES = {
SITE_NAME = 'Alliance Auth'
#################
# Login Settings
#################
# LOGIN_REDIRECT_URL - default destination when logging in if no redirect specified
# LOGOUT_REDIRECT_URL - destination after logging out
LOGIN_URL = 'auth_login_user' # view that handles login logic
LOGIN_REDIRECT_URL = 'authentication:dashboard' # default destination when logging in if no redirect specified
LOGOUT_REDIRECT_URL = 'authentication:dashboard' # destination after logging out
# Both of these redirects accept values as per the django redirect shortcut
# https://docs.djangoproject.com/en/1.11/topics/http/shortcuts/#redirect
# - url names eg 'authentication:dashboard'
# - relative urls eg '/dashboard'
# - absolute urls eg 'http://example.com/dashboard'
# LOGIN_TOKEN_SCOPES - scopes required on new tokens when logging in. Cannot be blank.
# ACCOUNT_ACTIVATION_DAYS - number of days email verification tokens are valid for
##################
LOGIN_URL = 'auth_login_user'
LOGIN_REDIRECT_URL = 'authentication:dashboard'
LOGOUT_REDIRECT_URL = 'authentication:dashboard'
LOGIN_TOKEN_SCOPES = ['esi-characters.read_opportunities.v1']
# scopes required on new tokens when logging in. Cannot be blank.
LOGIN_TOKEN_SCOPES = ['publicData']
# number of days email verification links are valid for
ACCOUNT_ACTIVATION_DAYS = 1
#####################################################
##
## Logging Configuration
##
#####################################################
# Set log_file and console level to desired state:
# DEBUG - basically stack trace, explains every step
# INFO - model creation, deletion, updates, etc
# WARN - unexpected function outcomes that do not impact user
# ERROR - unexcpeted function outcomes which prevent user from achieving desired outcome
# EXCEPTION - something critical went wrong, unhandled
#####################################
# Recommended level for log_file is INFO, console is DEBUG
# Change log level of individual apps below to narrow your debugging
#####################################
LOGGING = {
'version': 1,
'disable_existing_loggers': False,

View File

@@ -1,68 +1,57 @@
# Every setting in base.py can be overloaded by redefining it here.
from .base import *
# These are required for Django to function properly
# These are required for Django to function properly. Don't touch.
ROOT_URLCONF = '{{ project_name }}.urls'
WSGI_APPLICATION = '{{ project_name }}.wsgi.application'
STATICFILES_DIRS = [
os.path.join(PROJECT_DIR, 'static'),
]
STATIC_ROOT = "/var/www/{{ project_name }}/static/"
TEMPLATES[0]['DIRS'] += [os.path.join(PROJECT_DIR, 'templates')]
SECRET_KEY = '{{ secret_key }}'
# Change this to change the name of the auth site
# This is where css/images will be placed for your webserver to read
STATIC_ROOT = "/var/www/{{ project_name }}/static/"
# Change this to change the name of the auth site displayed
# in page titles and the site header.
SITE_NAME = '{{ project_name }}'
# Change this to enable/disable debug mode
# Change this to enable/disable debug mode, which displays
# useful error messages but can leak sensitive data.
DEBUG = False
#######################################
# Database Settings #
#######################################
# Uncomment and change the database name
# and credentials to use MySQL/MariaDB.
# Leave commented to use sqlite3
#######################################
"""
# Add any additional apps to this list.
INSTALLED_APPS += [
]
# Enter credentials to use MySQL/MariaDB. Comment out to use sqlite3
DATABASES['default'] = {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'alliance_auth',
'USER': os.environ.get('AA_DB_DEFAULT_USER', ''),
'PASSWORD': os.environ.get('AA_DB_DEFAULT_PASSWORD', ''),
'HOST': os.environ.get('AA_DB_DEFAULT_HOST', '127.0.0.1'),
'PORT': os.environ.get('AA_DB_DEFAULT_PORT', '3306'),
'USER': '',
'PASSWORD': '',
'HOST': '127.0.0.1',
'PORT': '3306',
}
"""
######################################
# SSO Settings #
######################################
# Register an application at
# https://developers.eveonline.com
# and fill out these settings.
# Be sure to set the callback URL to
# https://example.com/sso/callback
# substituting your domain for example.com
######################################
# Register an application at https://developers.eveonline.com for Authentication
# & API Access and fill out these settings. Be sure to set the callback URL
# to https://example.com/sso/callback substituting your domain for example.com
# Logging in to auth requires the publicData scope (can be overridden through the
# LOGIN_TOKEN_SCOPES setting). Other apps may require more (see their docs).
ESI_SSO_CLIENT_ID = ''
ESI_SSO_CLIENT_SECRET = ''
ESI_SSO_CALLBACK_URL = ''
######################################
# Email Settings #
######################################
# Alliance Auth validates emails before
# new users can log in.
# It's recommended to use a free service
# like SparkPost or Mailgun to send email.
# Emails are validated before new users can log in.
# It's recommended to use a free service like SparkPost or Mailgun to send email.
# https://www.sparkpost.com/docs/integrations/django/
#################
# Set the default from email to something like 'noreply@example.com'
EMAIL_HOST = ''
EMAIL_PORT = 587
EMAIL_HOST_USER = ''
EMAIL_HOST_PASSWORD = ''
EMAIL_USE_TLS = True
DEFAULT_FROM_EMAIL = ''
######################################
# Add any custom settings below here #
######################################
#######################################
# Add any custom settings below here. #
#######################################

View File

@@ -9,12 +9,19 @@ class NameFormatConfigForm(forms.ModelForm):
super(NameFormatConfigForm, self).__init__(*args, **kwargs)
SERVICE_CHOICES = [(s.name, s.name) for h in hooks.get_hooks('services_hook') for s in [h()]]
if self.instance.id:
SERVICE_CHOICES.append((self.instance.field, self.instance.field))
current_choice = (self.instance.service_name, self.instance.service_name)
if current_choice not in SERVICE_CHOICES:
SERVICE_CHOICES.append(current_choice)
self.fields['service_name'] = forms.ChoiceField(choices=SERVICE_CHOICES)
class NameFormatConfigAdmin(admin.ModelAdmin):
form = NameFormatConfigForm
list_display = ('service_name', 'get_state_display_string')
def get_state_display_string(self, obj):
return ', '.join([state.name for state in obj.states.all()])
get_state_display_string.short_description = 'States'
admin.site.register(NameFormatConfig, NameFormatConfigAdmin)

View File

@@ -6,4 +6,4 @@ class ServicesConfig(AppConfig):
label = 'services'
def ready(self):
pass
from . import signals

View File

@@ -9,5 +9,5 @@ class Command(BaseCommand):
def handle(self, *args, **options):
for u in User.objects.all():
validate_services(u)
validate_services(u.pk)
self.stdout.write(self.style.SUCCESS('Verified all user service accounts.'))

View File

@@ -11,6 +11,7 @@ class Migration(migrations.Migration):
dependencies = [
('services', '0001_squashed_0003_delete_groupcache'),
('authentication', '0015_user_profiles'),
]
operations = [

View File

@@ -1,6 +1,4 @@
import requests
import json
import re
import math
from django.conf import settings
from requests_oauthlib import OAuth2Session
@@ -24,8 +22,8 @@ Previously all we asked for was permission to kick members, manage roles, and ma
Users have reported weird unauthorized errors we don't understand. So now we ask for full server admin.
It's almost fixed the problem.
"""
# kick members, manage roles, manage nicknames
# BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000
# kick members, manage roles, manage nicknames, create instant invite
# BOT_PERMISSIONS = 0x00000002 + 0x10000000 + 0x08000000 + 0x00000001
BOT_PERMISSIONS = 0x00000008
# get user ID, accept invite
@@ -109,7 +107,7 @@ def api_backoff(func):
backoff_timer = datetime.datetime.strptime(existing_backoff, cache_time_format)
if backoff_timer > datetime.datetime.utcnow():
backoff_seconds = (backoff_timer - datetime.datetime.utcnow()).total_seconds()
logger.debug("Still under backoff for {} seconds, backing off" % backoff_seconds)
logger.debug("Still under backoff for %s seconds, backing off" % backoff_seconds)
# Still under backoff
raise PerformBackoff(
retry_after=backoff_seconds,
@@ -117,8 +115,7 @@ def api_backoff(func):
global_ratelimit=bool(existing_global_backoff)
)
logger.debug("Calling API calling function")
func(*args, **kwargs)
break
return func(*args, **kwargs)
except requests.HTTPError as e:
if e.response.status_code == 429:
try:
@@ -163,12 +160,11 @@ class DiscordOAuthManager:
@staticmethod
def _sanitize_name(name):
return re.sub('[^\w.-]', '', name)[:32]
return name[:32]
@staticmethod
def _sanitize_groupname(name):
name = name.strip(' _')
return DiscordOAuthManager._sanitize_name(name)
def _sanitize_group_name(name):
return name[:100]
@staticmethod
def generate_bot_add_url():
@@ -187,23 +183,33 @@ class DiscordOAuthManager:
return token
@staticmethod
def add_user(code):
def add_user(code, groups, nickname=None):
try:
token = DiscordOAuthManager._process_callback_code(code)['access_token']
logger.debug("Received token from OAuth")
custom_headers = {'accept': 'application/json', 'authorization': 'Bearer ' + token}
path = DISCORD_URL + "/invites/" + str(settings.DISCORD_INVITE_CODE)
r = requests.post(path, headers=custom_headers)
logger.debug("Got status code %s after accepting Discord invite" % r.status_code)
r.raise_for_status()
path = DISCORD_URL + "/users/@me"
r = requests.get(path, headers=custom_headers)
logger.debug("Got status code %s after retrieving Discord profile" % r.status_code)
r.raise_for_status()
user_id = r.json()['id']
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_group_name(g)) for g in
groups]
data = {
'roles': group_ids,
'access_token': token,
}
if nickname:
data['nick'] = nickname
custom_headers['authorization'] = 'Bot ' + settings.DISCORD_BOT_TOKEN
r = requests.put(path, headers=custom_headers, json=data)
logger.debug("Got status code %s after joining Discord server" % r.status_code)
r.raise_for_status()
logger.info("Added Discord user ID %s to server." % user_id)
return user_id
except:
@@ -211,6 +217,7 @@ class DiscordOAuthManager:
return None
@staticmethod
@api_backoff
def update_nickname(user_id, nickname):
try:
nickname = DiscordOAuthManager._sanitize_name(nickname)
@@ -260,7 +267,7 @@ class DiscordOAuthManager:
@staticmethod
def _group_name_to_id(name):
name = DiscordOAuthManager._sanitize_groupname(name)
name = DiscordOAuthManager._sanitize_group_name(name)
def get_or_make_role():
groups = DiscordOAuthManager._get_groups()
@@ -271,42 +278,36 @@ class DiscordOAuthManager:
return cache.get_or_set(DiscordOAuthManager._generate_cache_role_key(name), get_or_make_role, GROUP_CACHE_MAX_AGE)
@staticmethod
def __generate_role():
def __generate_role(name, **kwargs):
custom_headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles"
r = requests.post(path, headers=custom_headers)
data = {'name': name}
data.update(kwargs)
r = requests.post(path, headers=custom_headers, json=data)
logger.debug("Received status code %s after generating new role." % r.status_code)
r.raise_for_status()
return r.json()
@staticmethod
def __edit_role(role_id, name, color=0, hoist=True, permissions=36785152):
def __edit_role(role_id, **kwargs):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
data = {
'color': color,
'hoist': hoist,
'name': name,
'permissions': permissions,
}
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/roles/" + str(role_id)
r = requests.patch(path, headers=custom_headers, data=json.dumps(data))
r = requests.patch(path, headers=custom_headers, json=kwargs)
logger.debug("Received status code %s after editing role id %s" % (r.status_code, role_id))
r.raise_for_status()
return r.json()
@staticmethod
def _create_group(name):
role = DiscordOAuthManager.__generate_role()
return DiscordOAuthManager.__edit_role(role['id'], name)
return DiscordOAuthManager.__generate_role(name)
@staticmethod
@api_backoff
def update_groups(user_id, groups):
custom_headers = {'content-type': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_groupname(g)) for g in groups]
group_ids = [DiscordOAuthManager._group_name_to_id(DiscordOAuthManager._sanitize_group_name(g)) for g in groups]
path = DISCORD_URL + "/guilds/" + str(settings.DISCORD_GUILD_ID) + "/members/" + str(user_id)
data = {'roles': group_ids}
r = requests.patch(path, headers=custom_headers, json=data)
logger.debug("Received status code %s after setting user roles" % r.status_code)
r.raise_for_status()

View File

@@ -5,7 +5,7 @@ from django.contrib.auth.models import User
from django.core.exceptions import ObjectDoesNotExist
from allianceauth.notifications import notify
from celery import shared_task
from requests.exceptions import HTTPError
from allianceauth.services.hooks import NameFormatter
from .manager import DiscordOAuthManager, DiscordApiBackoff
from .models import DiscordUser
@@ -19,15 +19,16 @@ class DiscordTasks:
@classmethod
def add_user(cls, user, code):
user_id = DiscordOAuthManager.add_user(code)
groups = DiscordTasks.get_groups(user)
nickname = None
if settings.DISCORD_SYNC_NAMES:
nickname = DiscordTasks.get_nickname(user)
user_id = DiscordOAuthManager.add_user(code, groups, nickname=nickname)
if user_id:
discord_user = DiscordUser()
discord_user.user = user
discord_user.uid = user_id
discord_user.save()
if settings.DISCORD_SYNC_NAMES:
cls.update_nickname.delay(user.pk)
cls.update_groups.delay(user.pk)
return True
return False
@@ -62,12 +63,7 @@ class DiscordTasks:
user = User.objects.get(pk=pk)
logger.debug("Updating discord groups for user %s" % user)
if DiscordTasks.has_account(user):
groups = []
for group in user.groups.all():
groups.append(str(group.name))
if len(groups) == 0:
logger.debug("No syncgroups found for user. Adding empty group.")
groups.append('empty')
groups = DiscordTasks.get_groups(user)
logger.debug("Updating user %s discord groups to %s" % (user, groups))
try:
DiscordOAuthManager.update_groups(user.discord.uid, groups)
@@ -75,6 +71,15 @@ class DiscordTasks:
logger.info("Discord group sync API back off for %s, "
"retrying in %s seconds" % (user, bo.retry_after_seconds))
raise task_self.retry(countdown=bo.retry_after_seconds)
except HTTPError as e:
if e.response.status_code == 404:
try:
if e.response.json()['code'] == 10007:
# user has left the server
DiscordTasks.delete_user(user)
return
finally:
raise e
except Exception as e:
if task_self:
logger.exception("Discord group sync failed for %s, retrying in 10 mins" % user)
@@ -95,7 +100,7 @@ class DiscordTasks:
@staticmethod
@shared_task(bind=True, name='discord.update_nickname')
def update_nickname(self, pk):
def update_nickname(task_self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating discord nickname for user %s" % user)
if DiscordTasks.has_account(user):
@@ -104,10 +109,14 @@ class DiscordTasks:
logger.debug("Updating user %s discord nickname to %s" % (user, character.character_name))
try:
DiscordOAuthManager.update_nickname(user.discord.uid, DiscordTasks.get_nickname(user))
except DiscordApiBackoff as bo:
logger.info("Discord nickname update API back off for %s, "
"retrying in %s seconds" % (user, bo.retry_after_seconds))
raise task_self.retry(countdown=bo.retry_after_seconds)
except Exception as e:
if self:
if task_self:
logger.exception("Discord nickname sync failed for %s, retrying in 10 mins" % user)
raise self.retry(countdown=60 * 10)
raise task_self.retry(countdown=60 * 10)
else:
# Rethrow
raise e
@@ -132,3 +141,7 @@ class DiscordTasks:
def get_nickname(user):
from .auth_hooks import DiscordService
return NameFormatter(DiscordService(), user).format_name()
@staticmethod
def get_groups(user):
return [g.name for g in user.groups.all()] + [user.profile.state.name]

View File

@@ -145,6 +145,7 @@ class DiscordHooksTestCase(TestCase):
class DiscordViewsTestCase(WebTest):
def setUp(self):
self.member = AuthUtils.create_member('auth_member')
AuthUtils.add_main_character(self.member, 'test character', '1234', '2345', 'test corp', 'testc')
add_permissions()
def login(self):
@@ -198,11 +199,11 @@ class DiscordManagerTestCase(TestCase):
def setUp(self):
pass
def test__sanitize_groupname(self):
test_group_name = ' Group Name_Test_'
group_name = DiscordOAuthManager._sanitize_groupname(test_group_name)
def test__sanitize_group_name(self):
test_group_name = str(10**103)
group_name = DiscordOAuthManager._sanitize_group_name(test_group_name)
self.assertEqual(group_name, 'GroupName_Test')
self.assertEqual(group_name, test_group_name[:100])
def test_generate_Bot_add_url(self):
bot_add_url = DiscordOAuthManager.generate_bot_add_url()
@@ -245,18 +246,20 @@ class DiscordManagerTestCase(TestCase):
headers = {'accept': 'application/json', 'authorization': 'Bearer accesstoken'}
m.register_uri('POST',
manager.DISCORD_URL + '/invites/' + str(settings.DISCORD_INVITE_CODE),
request_headers=headers,
text='{}')
m.register_uri('GET',
manager.DISCORD_URL + "/users/@me",
request_headers=headers,
text=json.dumps({'id': "123456"}))
headers = {'accept': 'application/json', 'authorization': 'Bot ' + settings.DISCORD_BOT_TOKEN}
m.register_uri('PUT',
manager.DISCORD_URL + '/guilds/' + str(settings.DISCORD_GUILD_ID) + '/members/123456',
request_headers=headers,
text='{}')
# Act
return_value = DiscordOAuthManager.add_user('abcdef')
return_value = DiscordOAuthManager.add_user('abcdef', [])
# Assert
self.assertEqual(return_value, '123456')
@@ -328,7 +331,7 @@ class DiscordManagerTestCase(TestCase):
@requests_mock.Mocker()
def test_update_groups(self, group_cache, m):
# Arrange
groups = ['Member', 'Blue', 'Special Group']
groups = ['Member', 'Blue', 'SpecialGroup']
group_cache.return_value = [{'id': 111, 'name': 'Member'},
{'id': 222, 'name': 'Blue'},

View File

@@ -345,7 +345,7 @@ class DiscourseManager:
@staticmethod
def update_groups(user):
groups = []
groups = [DiscourseManager._sanitize_groupname(user.profile.state.name)]
for g in user.groups.all():
groups.append(DiscourseManager._sanitize_groupname(str(g)))
logger.debug("Updating discourse user %s groups to %s" % (user, groups))

View File

@@ -4,16 +4,20 @@ import string
import re
from django.db import connections
from passlib.hash import bcrypt
from django.conf import settings
logger = logging.getLogger(__name__)
TABLE_PREFIX = getattr(settings, 'IPS4_TABLE_PREFIX', '')
class Ips4Manager:
SQL_ADD_USER = r"INSERT INTO core_members (name, email, members_pass_hash, members_pass_salt, " \
r"member_group_id) VALUES (%s, %s, %s, %s, %s)"
SQL_GET_ID = r"SELECT member_id FROM core_members WHERE name = %s"
SQL_UPDATE_PASSWORD = r"UPDATE core_members SET members_pass_hash = %s, members_pass_salt = %s WHERE name = %s"
SQL_DEL_USER = r"DELETE FROM core_members WHERE member_id = %s"
SQL_ADD_USER = r"INSERT INTO %score_members (name, email, members_pass_hash, members_pass_salt, " \
r"member_group_id) VALUES (%%s, %%s, %%s, %%s, %%s)" % TABLE_PREFIX
SQL_GET_ID = r"SELECT member_id FROM %score_members WHERE name = %%s" % TABLE_PREFIX
SQL_UPDATE_PASSWORD = r"UPDATE %score_members SET members_pass_hash = %%s, members_pass_salt = %%s WHERE name = %%s" % TABLE_PREFIX
SQL_DEL_USER = r"DELETE FROM %score_members WHERE member_id = %%s" % TABLE_PREFIX
MEMBER_GROUP_ID = 3

View File

@@ -5,26 +5,30 @@ import re
from django.db import connections
from passlib.hash import bcrypt
from django.conf import settings
# requires yum install libffi-devel and pip install bcrypt
logger = logging.getLogger(__name__)
TABLE_PREFIX = getattr(settings, 'MARKET_TABLE_PREFIX', 'fos_')
class MarketManager:
def __init__(self):
pass
SQL_ADD_USER = r"INSERT INTO fos_user (username, username_canonical, email, email_canonical, enabled, salt," \
SQL_ADD_USER = r"INSERT INTO %suser (username, username_canonical, email, email_canonical, enabled, salt," \
r"password, locked, expired, roles, credentials_expired, characterid, characterName)" \
r"VALUES (%s, %s, %s, %s, 1,%s, %s, 0, 0, 'a:0:{}', 0, %s, %s) "
SQL_GET_USER_ID = r"SELECT id FROM fos_user WHERE username = %s"
SQL_DISABLE_USER = r"UPDATE fos_user SET enabled = '0' WHERE username = %s"
SQL_ENABLE_USER = r"UPDATE fos_user SET enabled = '1' WHERE username = %s"
SQL_UPDATE_PASSWORD = r"UPDATE fos_user SET password = %s, salt = %s WHERE username = %s"
SQL_CHECK_EMAIL = r"SELECT email FROM fos_user WHERE email = %s"
SQL_CHECK_USERNAME = r"SELECT username FROM fos_user WHERE username = %s"
SQL_UPDATE_USER = r"UPDATE fos_user SET password = %s, salt = %s, enabled = '1' WHERE username = %s"
r"VALUES (%%s, %%s, %%s, %%s, 1,%%s, %%s, 0, 0, 'a:0:{}', 0, %%s, %%s) " % TABLE_PREFIX
SQL_GET_USER_ID = r"SELECT id FROM %suser WHERE username = %%s" % TABLE_PREFIX
SQL_DISABLE_USER = r"UPDATE %suser SET enabled = '0' WHERE username = %%s" % TABLE_PREFIX
SQL_ENABLE_USER = r"UPDATE %suser SET enabled = '1' WHERE username = %%s" % TABLE_PREFIX
SQL_UPDATE_PASSWORD = r"UPDATE %suser SET password = %%s, salt = %%s WHERE username = %%s" % TABLE_PREFIX
SQL_CHECK_EMAIL = r"SELECT email FROM %suser WHERE email = %%s" % TABLE_PREFIX
SQL_CHECK_USERNAME = r"SELECT username FROM %suser WHERE username = %%s" % TABLE_PREFIX
SQL_UPDATE_USER = r"UPDATE %suser SET password = %%s, salt = %%s, enabled = '1' WHERE username = %%s" % TABLE_PREFIX
@staticmethod
def __santatize_username(username):
@@ -47,31 +51,31 @@ class MarketManager:
@classmethod
def check_username(cls, username):
logger.debug("Checking alliance market username %s" % username)
logger.debug("Checking alliance market username %%s" % username)
cursor = connections['market'].cursor()
cursor.execute(cls.SQL_CHECK_USERNAME, [cls.__santatize_username(username)])
row = cursor.fetchone()
if row:
logger.debug("Found user %s on alliance market" % username)
logger.debug("Found user %%s on alliance market" % username)
return True
logger.debug("User %s not found on alliance market" % username)
logger.debug("User %%s not found on alliance market" % username)
return False
@classmethod
def check_user_email(cls, username, email):
logger.debug("Checking if alliance market email exists for user %s" % username)
logger.debug("Checking if alliance market email exists for user %%s" % username)
cursor = connections['market'].cursor()
cursor.execute(cls.SQL_CHECK_EMAIL, [email])
row = cursor.fetchone()
if row:
logger.debug("Found user %s email address on alliance market" % username)
logger.debug("Found user %%s email address on alliance market" % username)
return True
logger.debug("User %s email address not found on alliance market" % username)
logger.debug("User %%s email address not found on alliance market" % username)
return False
@classmethod
def add_user(cls, username, email, characterid, charactername):
logger.debug("Adding new market user %s" % username)
logger.debug("Adding new market user %%s" % username)
plain_password = cls.__generate_random_pass()
hash = cls._gen_pwhash(plain_password)
salt = cls._get_salt(hash)
@@ -79,33 +83,33 @@ class MarketManager:
if not cls.check_username(username):
if not cls.check_user_email(username, email):
try:
logger.debug("Adding user %s to alliance market" % username)
logger.debug("Adding user %%s to alliance market" % username)
cursor = connections['market'].cursor()
cursor.execute(cls.SQL_ADD_USER, [username_clean, username_clean, email, email, salt,
hash, characterid, charactername])
return username_clean, plain_password
except:
logger.debug("Unsuccessful attempt to add market user %s" % username)
logger.debug("Unsuccessful attempt to add market user %%s" % username)
return "", ""
else:
logger.debug("Alliance market email %s already exists Updating instead" % email)
logger.debug("Alliance market email %%s already exists Updating instead" % email)
username_clean, password = cls.update_user_info(username)
return username_clean, password
else:
logger.debug("Alliance market username %s already exists Updating instead" % username)
logger.debug("Alliance market username %%s already exists Updating instead" % username)
username_clean, password = cls.update_user_info(username)
return username_clean, password
@classmethod
def disable_user(cls, username):
logger.debug("Disabling alliance market user %s " % username)
logger.debug("Disabling alliance market user %%s " % username)
cursor = connections['market'].cursor()
cursor.execute(cls.SQL_DISABLE_USER, [username])
return True
@classmethod
def update_custom_password(cls, username, plain_password):
logger.debug("Updating alliance market user %s password" % username)
logger.debug("Updating alliance market user %%s password" % username)
if cls.check_username(username):
username_clean = cls.__santatize_username(username)
hash = cls._gen_pwhash(plain_password)
@@ -114,12 +118,12 @@ class MarketManager:
cursor.execute(cls.SQL_UPDATE_PASSWORD, [hash, salt, username_clean])
return plain_password
else:
logger.error("Unable to update alliance market user %s password" % username)
logger.error("Unable to update alliance market user %%s password" % username)
return ""
@classmethod
def update_user_password(cls, username):
logger.debug("Updating alliance market user %s password" % username)
logger.debug("Updating alliance market user %%s password" % username)
if cls.check_username(username):
username_clean = cls.__santatize_username(username)
plain_password = cls.__generate_random_pass()
@@ -129,12 +133,12 @@ class MarketManager:
cursor.execute(cls.SQL_UPDATE_PASSWORD, [hash, salt, username_clean])
return plain_password
else:
logger.error("Unable to update alliance market user %s password" % username)
logger.error("Unable to update alliance market user %%s password" % username)
return ""
@classmethod
def update_user_info(cls, username):
logger.debug("Updating alliance market user %s" % username)
logger.debug("Updating alliance market user %%s" % username)
try:
username_clean = cls.__santatize_username(username)
plain_password = cls.__generate_random_pass()
@@ -144,5 +148,5 @@ class MarketManager:
cursor.execute(cls.SQL_UPDATE_USER, [hash, salt, username_clean])
return username_clean, plain_password
except:
logger.debug("Alliance market update user failed for %s" % username)
logger.debug("Alliance market update user failed for %%s" % username)
return "", ""

View File

@@ -82,11 +82,9 @@ class MumbleUser(AbstractServiceModel):
def update_groups(self, groups: Group=None):
if groups is None:
groups = self.user.groups.all()
groups_str = []
groups_str = [self.user.profile.state.name]
for group in groups:
groups_str.append(str(group.name))
if len(groups) == 0:
groups_str.append('empty')
safe_groups = ','.join(set([g.replace(' ', '-') for g in groups_str]))
logger.info("Updating mumble user {} groups to {}".format(self.user, safe_groups))
self.groups = safe_groups

View File

@@ -136,7 +136,9 @@ class MumbleViewsTestCase(TestCase):
mumble_user = MumbleUser.objects.get(user=self.member)
self.assertEqual(mumble_user.username, expected_username)
self.assertTrue(mumble_user.pwhash)
self.assertEqual('Member', mumble_user.groups)
self.assertIn('Guest', mumble_user.groups)
self.assertIn('Member', mumble_user.groups)
self.assertIn(',', mumble_user.groups)
def test_deactivate_post(self):
self.login()

View File

@@ -45,11 +45,9 @@ class OpenfireTasks:
user = User.objects.get(pk=pk)
logger.debug("Updating jabber groups for user %s" % user)
if OpenfireTasks.has_account(user):
groups = []
groups = [user.profile.state.name]
for group in user.groups.all():
groups.append(str(group.name))
if len(groups) == 0:
groups.append('empty')
logger.debug("Updating user %s jabber groups to %s" % (user, groups))
try:
OpenfireManager.update_user_groups(user.openfire.username, groups)

View File

@@ -14,40 +14,43 @@ from django.conf import settings
logger = logging.getLogger(__name__)
TABLE_PREFIX = getattr(settings, 'PHPBB3_TABLE_PREFIX', 'phpbb_')
class Phpbb3Manager:
SQL_ADD_USER = r"INSERT INTO phpbb_users (username, username_clean, " \
SQL_ADD_USER = r"INSERT INTO %susers (username, username_clean, " \
r"user_password, user_email, group_id, user_regdate, user_permissions, " \
r"user_sig, user_lang) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, 'en')"
r"user_sig, user_lang) VALUES (%%s, %%s, %%s, %%s, %%s, %%s, %%s, %%s, 'en')" % TABLE_PREFIX
SQL_DEL_USER = r"DELETE FROM phpbb_users where username = %s"
SQL_DEL_USER = r"DELETE FROM %susers where username = %%s" % TABLE_PREFIX
SQL_DIS_USER = r"UPDATE phpbb_users SET user_email= %s, user_password=%s WHERE username = %s"
SQL_DIS_USER = r"UPDATE %susers SET user_email= %%s, user_password=%%s WHERE username = %%s" % TABLE_PREFIX
SQL_USER_ID_FROM_USERNAME = r"SELECT user_id from phpbb_users WHERE username = %s"
SQL_USER_ID_FROM_USERNAME = r"SELECT user_id from %susers WHERE username = %%s" % TABLE_PREFIX
SQL_ADD_USER_GROUP = r"INSERT INTO phpbb_user_group (group_id, user_id, user_pending) VALUES (%s, %s, %s)"
SQL_ADD_USER_GROUP = r"INSERT INTO %suser_group (group_id, user_id, user_pending) VALUES (%%s, %%s, %%s)" % TABLE_PREFIX
SQL_GET_GROUP_ID = r"SELECT group_id from phpbb_groups WHERE group_name = %s"
SQL_GET_GROUP_ID = r"SELECT group_id from %sgroups WHERE group_name = %%s" % TABLE_PREFIX
SQL_ADD_GROUP = r"INSERT INTO phpbb_groups (group_name,group_desc,group_legend) VALUES (%s,%s,0)"
SQL_ADD_GROUP = r"INSERT INTO %sgroups (group_name,group_desc,group_legend) VALUES (%%s,%%s,0)" % TABLE_PREFIX
SQL_UPDATE_USER_PASSWORD = r"UPDATE phpbb_users SET user_password = %s WHERE username = %s"
SQL_UPDATE_USER_PASSWORD = r"UPDATE %susers SET user_password = %%s WHERE username = %%s" % TABLE_PREFIX
SQL_REMOVE_USER_GROUP = r"DELETE FROM phpbb_user_group WHERE user_id=%s AND group_id=%s "
SQL_REMOVE_USER_GROUP = r"DELETE FROM %suser_group WHERE user_id=%%s AND group_id=%%s " % TABLE_PREFIX
SQL_GET_ALL_GROUPS = r"SELECT group_id, group_name FROM phpbb_groups"
SQL_GET_ALL_GROUPS = r"SELECT group_id, group_name FROM %sgroups" % TABLE_PREFIX
SQL_GET_USER_GROUPS = r"SELECT phpbb_groups.group_name FROM phpbb_groups , phpbb_user_group WHERE " \
r"phpbb_user_group.group_id = phpbb_groups.group_id AND user_id=%s"
SQL_GET_USER_GROUPS = r"SELECT %(prefix)sgroups.group_name FROM %(prefix)sgroups , %(prefix)suser_group WHERE " \
r"%(prefix)suser_group.group_id = %(prefix)sgroups.group_id AND user_id=%%s" % {'prefix': TABLE_PREFIX}
SQL_ADD_USER_AVATAR = r"UPDATE phpbb_users SET user_avatar_type=2, user_avatar_width=64, user_avatar_height=64, " \
"user_avatar=%s WHERE user_id = %s"
SQL_ADD_USER_AVATAR = r"UPDATE %susers SET user_avatar_type=2, user_avatar_width=64, user_avatar_height=64, " \
"user_avatar=%%s WHERE user_id = %%s" % TABLE_PREFIX
SQL_CLEAR_USER_PERMISSIONS = r"UPDATE phpbb_users SET user_permissions = '' WHERE user_Id = %s"
SQL_CLEAR_USER_PERMISSIONS = r"UPDATE %susers SET user_permissions = '' WHERE user_id = %%s" % TABLE_PREFIX
SQL_DEL_SESSION = r"DELETE FROM phpbb_sessions where session_user_id = %s"
SQL_DEL_SESSION = r"DELETE FROM %ssessions where session_user_id = %%s" % TABLE_PREFIX
SQL_DEL_AUTOLOGIN = r"DELETE FROM phpbb_sessions_keys where user_id = %s"
SQL_DEL_AUTOLOGIN = r"DELETE FROM %ssessions_keys where user_id = %%s" % TABLE_PREFIX
def __init__(self):
pass

View File

@@ -40,11 +40,9 @@ class Phpbb3Tasks:
user = User.objects.get(pk=pk)
logger.debug("Updating phpbb3 groups for user %s" % user)
if Phpbb3Tasks.has_account(user):
groups = []
groups = [user.profile.state.name]
for group in user.groups.all():
groups.append(str(group.name))
if len(groups) == 0:
groups.append('empty')
logger.debug("Updating user %s phpbb3 groups to %s" % (user, groups))
try:
Phpbb3Manager.update_groups(user.phpbb3.username, groups)

View File

@@ -38,13 +38,10 @@ class SeatTasks:
def update_roles(self, pk):
user = User.objects.get(pk=pk)
logger.debug("Updating SeAT roles for user %s" % user)
groups = []
if SeatTasks.has_account(user):
groups = [user.profile.state.name]
for group in user.groups.all():
groups.append(str(group.name))
if len(groups) == 0:
logger.debug("No syncgroups found for user. Adding empty group.")
groups.append('empty')
logger.debug("Updating user %s SeAT roles to %s" % (user, groups))
try:
SeatManager.update_roles(user.seat.username, groups)

View File

@@ -152,7 +152,6 @@ class SeatViewsTestCase(TestCase):
self.assertContains(response, expected_username)
seat_user = SeatUser.objects.get(user=self.member)
self.assertEqual(seat_user.username, expected_username)
self.assertTrue(manager.synchronize_eveapis.called)
@mock.patch(MODULE_PATH + '.tasks.SeatManager')
def test_deactivate(self, manager):

View File

@@ -39,7 +39,6 @@ def activate_seat(request):
logger.info("Successfully activated SeAT for user %s" % request.user)
messages.add_message(request, messages.SUCCESS, _('Successfully activated your %(service)s account.') %
SERVICE_NAME)
SeatManager.synchronize_eveapis(request.user)
credentials = {
'username': request.user.seat.username,
'password': result[1],

View File

@@ -12,35 +12,38 @@ from django.conf import settings
logger = logging.getLogger(__name__)
TABLE_PREFIX = getattr(settings, 'SMF_TABLE_PREFIX', 'smf_')
class SmfManager:
def __init__(self):
pass
SQL_ADD_USER = r"INSERT INTO smf_members (member_name, passwd, email_address, date_registered, real_name," \
SQL_ADD_USER = r"INSERT INTO %smembers (member_name, passwd, email_address, date_registered, real_name," \
r" buddy_list, message_labels, openid_uri, signature, ignore_boards) " \
r"VALUES (%s, %s, %s, %s, %s, 0, 0, 0, 0, 0)"
r"VALUES (%%s, %%s, %%s, %%s, %%s, 0, 0, 0, 0, 0)" % TABLE_PREFIX
SQL_DEL_USER = r"DELETE FROM smf_members where member_name = %s"
SQL_DEL_USER = r"DELETE FROM %smembers where member_name = %%s" % TABLE_PREFIX
SQL_DIS_USER = r"UPDATE smf_members SET email_address = %s, passwd = %s WHERE member_name = %s"
SQL_DIS_USER = r"UPDATE %smembers SET email_address = %%s, passwd = %%s WHERE member_name = %%s" % TABLE_PREFIX
SQL_USER_ID_FROM_USERNAME = r"SELECT id_member from smf_members WHERE member_name = %s"
SQL_USER_ID_FROM_USERNAME = r"SELECT id_member from %smembers WHERE member_name = %%s" % TABLE_PREFIX
SQL_ADD_USER_GROUP = r"UPDATE smf_members SET additional_groups = %s WHERE id_member = %s"
SQL_ADD_USER_GROUP = r"UPDATE %smembers SET additional_groups = %%s WHERE id_member = %%s" % TABLE_PREFIX
SQL_GET_GROUP_ID = r"SELECT id_group from smf_membergroups WHERE group_name = %s"
SQL_GET_GROUP_ID = r"SELECT id_group from %smembergroups WHERE group_name = %%s" % TABLE_PREFIX
SQL_ADD_GROUP = r"INSERT INTO smf_membergroups (group_name,description) VALUES (%s,%s)"
SQL_ADD_GROUP = r"INSERT INTO %smembergroups (group_name,description) VALUES (%%s,%%s)" % TABLE_PREFIX
SQL_UPDATE_USER_PASSWORD = r"UPDATE smf_members SET passwd = %s WHERE member_name = %s"
SQL_UPDATE_USER_PASSWORD = r"UPDATE %smembers SET passwd = %%s WHERE member_name = %%s" % TABLE_PREFIX
SQL_REMOVE_USER_GROUP = r"UPDATE smf_members SET additional_groups = %s WHERE id_member = %s"
SQL_REMOVE_USER_GROUP = r"UPDATE %smembers SET additional_groups = %%s WHERE id_member = %%s" % TABLE_PREFIX
SQL_GET_ALL_GROUPS = r"SELECT id_group, group_name FROM smf_membergroups"
SQL_GET_ALL_GROUPS = r"SELECT id_group, group_name FROM %smembergroups" % TABLE_PREFIX
SQL_GET_USER_GROUPS = r"SELECT additional_groups FROM smf_members WHERE id_member = %s"
SQL_GET_USER_GROUPS = r"SELECT additional_groups FROM %smembers WHERE id_member = %%s" % TABLE_PREFIX
SQL_ADD_USER_AVATAR = r"UPDATE smf_members SET avatar = %s WHERE id_member = %s"
SQL_ADD_USER_AVATAR = r"UPDATE %smembers SET avatar = %%s WHERE id_member = %%s" % TABLE_PREFIX
@staticmethod
def _sanitize_groupname(name):

View File

@@ -44,11 +44,9 @@ class SmfTasks:
user = User.objects.get(pk=pk)
logger.debug("Updating smf groups for user %s" % user)
if SmfTasks.has_account(user):
groups = []
groups = [user.profile.state.name]
for group in user.groups.all():
groups.append(str(group.name))
if len(groups) == 0:
groups.append('empty')
logger.debug("Updating user %s smf groups to %s" % (user, groups))
try:
SmfManager.update_groups(user.smf.username, groups)

View File

@@ -1,5 +1,5 @@
from django.contrib import admin
from .models import AuthTS, Teamspeak3User
from .models import AuthTS, Teamspeak3User, StateGroup
class Teamspeak3UserAdmin(admin.ModelAdmin):
@@ -12,5 +12,11 @@ class AuthTSgroupAdmin(admin.ModelAdmin):
filter_horizontal = ('ts_group',)
@admin.register(StateGroup)
class StateGroupAdmin(admin.ModelAdmin):
list_display = ('state', 'ts_group')
search_fields = ('state__name', 'ts_group__ts_group_name')
admin.site.register(AuthTS, AuthTSgroupAdmin)
admin.site.register(Teamspeak3User, Teamspeak3UserAdmin)

View File

@@ -244,10 +244,10 @@ class Teamspeak3Manager:
return False
def generate_new_permissionkey(self, uid, username, corpticker):
def generate_new_permissionkey(self, uid, username):
logger.debug("Re-issuing permission key for user id %s" % uid)
self.delete_user(uid)
return self.add_user(username, corpticker)
return self.add_user(username)
def update_groups(self, uid, ts_groups):
logger.debug("Updating uid %s TS3 groups %s" % (uid, ts_groups))

View File

@@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.10 on 2018-02-23 06:13
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('authentication', '0015_user_profiles'),
('teamspeak3', '0004_service_permissions'),
]
operations = [
migrations.CreateModel(
name='StateGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('state', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='authentication.State')),
('ts_group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='teamspeak3.TSgroup')),
],
),
]

View File

@@ -1,5 +1,6 @@
from django.db import models
from django.contrib.auth.models import User, Group
from allianceauth.authentication.models import State
class Teamspeak3User(models.Model):
@@ -50,3 +51,8 @@ class UserTSgroup(models.Model):
def __str__(self):
return self.user.name
class StateGroup(models.Model):
state = models.ForeignKey(State, on_delete=models.CASCADE)
ts_group = models.ForeignKey(TSgroup, on_delete=models.CASCADE)

View File

@@ -5,9 +5,9 @@ from django.db.models.signals import m2m_changed
from django.db.models.signals import post_delete
from django.db.models.signals import post_save
from django.dispatch import receiver
from allianceauth.authentication.signals import state_changed
from .tasks import Teamspeak3Tasks
from .models import AuthTS
from .models import AuthTS, StateGroup
logger = logging.getLogger(__name__)
@@ -34,3 +34,16 @@ def post_save_authts(sender, instance, *args, **kwargs):
def post_delete_authts(sender, instance, *args, **kwargs):
logger.debug("Received post_delete signal from %s" % instance)
transaction.on_commit(trigger_all_ts_update)
# it's literally the same logic so just recycle the receiver
post_save.connect(post_save_authts, sender=StateGroup)
post_delete.connect(post_delete_authts, sender=StateGroup)
@receiver(state_changed)
def check_groups_on_state_change(sender, user, state, **kwargs):
def trigger_update():
Teamspeak3Tasks.update_groups.delay(user.pk)
logger.debug("Received state_changed signal from {}".format(user))
transaction.on_commit(trigger_update)

View File

@@ -69,6 +69,8 @@ class Teamspeak3Tasks:
for filtered_group in filtered_groups:
for ts_group in filtered_group.ts_group.all():
groups[ts_group.ts_group_name] = ts_group.ts_group_id
for stategroup in user.profile.state.stategroup_set.all():
groups[stategroup.ts_group.ts_group_name] = stategroup.ts_group.ts_group_id
logger.debug("Updating user %s teamspeak3 groups to %s" % (user, groups))
try:
with Teamspeak3Manager() as ts3man:

View File

@@ -11,7 +11,7 @@
<h1 class="page-header text-center">{% trans "Verify Teamspeak Identity" %}</h1>
<div class="container-fluid">
<div class="col-md-4 col-md-offset-4">
<a href="ts3server://{{ TEAMSPEAK3_PUBLIC_URL }}?token={{ authinfo.teamspeak3_perm_key }}&nickname={{ authinfo.teamspeak3_uid }}" class="btn btn-primary btn-block btn-lg" title="Join">{% trans "Join Server" %}</a>
<a href="ts3server://{{ public_url }}?token={{ authinfo.teamspeak3_perm_key }}&nickname={{ authinfo.teamspeak3_uid }}" class="btn btn-primary btn-block btn-lg" title="Join">{% trans "Join Server" %}</a>
<br/>
<form class="form-signin" role="form" action="{% url 'teamspeak3:verify' %}" method="POST">
{% csrf_token %}

View File

@@ -7,9 +7,8 @@ from django.core.exceptions import ObjectDoesNotExist
from django.db.models import signals
from allianceauth.tests.auth_utils import AuthUtils
from .auth_hooks import Teamspeak3Service
from .models import Teamspeak3User, AuthTS, TSgroup
from .models import Teamspeak3User, AuthTS, TSgroup, StateGroup
from .tasks import Teamspeak3Tasks
from .signals import m2m_changed_authts_group, post_save_authts, post_delete_authts
@@ -31,13 +30,14 @@ class Teamspeak3HooksTestCase(TestCase):
member = AuthUtils.create_member(self.member)
Teamspeak3User.objects.create(user=member, uid=self.member, perm_key='123ABC')
self.none_user = 'none_user'
none_user = AuthUtils.create_user(self.none_user)
AuthUtils.create_user(self.none_user)
state = member.profile.state
ts_member_group = TSgroup.objects.create(ts_group_id=1, ts_group_name='Member')
ts_blue_group = TSgroup.objects.create(ts_group_id=2, ts_group_name='Blue')
ts_state_group = TSgroup.objects.create(ts_group_id=2, ts_group_name='State')
m2m_member_group = AuthTS.objects.create(auth_group=member.groups.all()[0])
m2m_member_group.ts_group.add(ts_member_group)
m2m_member_group.save()
StateGroup.objects.create(state=state, ts_group=ts_state_group)
self.service = Teamspeak3Service
add_permissions()
@@ -60,7 +60,7 @@ class Teamspeak3HooksTestCase(TestCase):
instance = manager.return_value.__enter__.return_value
service = self.service()
service.update_all_groups()
# Check member and blue user have groups updated
# Check user has groups updated
self.assertTrue(instance.update_groups.called)
self.assertEqual(instance.update_groups.call_count, 1)
@@ -74,7 +74,7 @@ class Teamspeak3HooksTestCase(TestCase):
self.assertTrue(instance.update_groups.called)
args, kwargs = instance.update_groups.call_args
# update_groups(user.teamspeak3.uid, groups)
self.assertEqual({'Member': 1}, args[1]) # Check groups
self.assertEqual({'Member': 1, 'State': 2}, args[1]) # Check groups
self.assertEqual(self.member, args[0]) # Check uid
# Check none user does not have groups updated
@@ -278,3 +278,15 @@ class Teamspeak3SignalsTestCase(TestCase):
self.m2m_member.delete() # Trigger delete signal
self.assertTrue(trigger_all_ts_update.called)
@mock.patch(MODULE_PATH + '.signals.transaction')
@mock.patch(MODULE_PATH + '.signals.Teamspeak3Tasks.update_groups.delay')
def test_state_changed(self, update_groups, transaction):
# Overload transaction.on_commit so everything happens synchronously
transaction.on_commit = lambda fn: fn()
state = AuthUtils.create_state('test', 1000, disconnect_signals=True)
self.member.profile.state = state
self.member.profile.save()
self.assertTrue(update_groups.called)

View File

@@ -3,7 +3,7 @@ import logging
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.shortcuts import render, redirect
from django.conf import settings
from .manager import Teamspeak3Manager
from .forms import TeamspeakJoinForm
from .models import Teamspeak3User
@@ -20,7 +20,6 @@ def activate_teamspeak3(request):
logger.debug("activate_teamspeak3 called by user %s" % request.user)
character = request.user.profile.main_character
ticker = character.corporation_ticker
with Teamspeak3Manager() as ts3man:
logger.debug("Adding TS3 user for user %s with main character %s" % (request.user, character))
result = ts3man.add_user(Teamspeak3Tasks.get_username(request.user))
@@ -56,6 +55,7 @@ def verify_teamspeak3(request):
'form': form,
'authinfo': {'teamspeak3_uid': request.user.teamspeak3.uid,
'teamspeak3_perm_key': request.user.teamspeak3.perm_key},
'public_url': settings.TEAMSPEAK3_PUBLIC_URL,
}
return render(request, 'services/teamspeak3/teamspeakjoin.html', context=context)
@@ -85,8 +85,7 @@ def reset_teamspeak3_perm(request):
ts3man.delete_user(request.user.teamspeak3.uid)
logger.debug("Generating new permission key for user %s with main character %s" % (request.user, character))
result = ts3man.generate_new_permissionkey(request.user.teamspeak3.uid, character.character_name,
character.corporation_ticker)
result = ts3man.generate_new_permissionkey(request.user.teamspeak3.uid, character.character_name)
# if blank we failed
if result[0] != "":

View File

@@ -2,7 +2,7 @@ import logging
import redis
from celery import shared_task
from django.contrib.auth.models import User
from .hooks import ServicesHook
REDIS_CLIENT = redis.Redis()
@@ -38,7 +38,8 @@ def only_one(function=None, key="", timeout=None):
@shared_task(bind=True)
def validate_services(self, user):
def validate_services(self, pk):
user = User.objects.get(pk=pk)
logger.debug('Ensuring user {} has permissions for active services'.format(user))
# Iterate through services hooks and have them check the validity of the user
for svc in ServicesHook.get_services():

View File

@@ -18,7 +18,7 @@ class ServicesTasksTestCase(TestCase):
services_hook.get_services.return_value = [svc]
validate_services.delay(user=self.member)
validate_services.delay(self.member.pk)
self.assertTrue(services_hook.get_services.called)
self.assertTrue(svc.validate_user.called)

View File

@@ -42,7 +42,6 @@
</ul>
<form id="f-lang-select" class="navbar-form navbar-right" action="{% url 'set_language' %}" method="post">
{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path|slice:'3:' }}"/>
<div class="form-group">
<select onchange="this.form.submit()" class="form-control" id="lang-select" name="language">
{% get_language_info_list for LANGUAGES as languages %}

View File

@@ -1,5 +1,6 @@
import requests
import logging
import amqp.exceptions
import semantic_version as semver
from django import template
from django.conf import settings
@@ -54,6 +55,9 @@ def get_celery_queue_length():
with app.connection_or_acquire() as conn:
return conn.default_channel.queue_declare(
queue=getattr(settings, 'CELERY_DEFAULT_QUEUE', 'celery'), passive=True).message_count
except amqp.exceptions.ChannelError:
# Queue doesn't exist, probably empty
return 0
except Exception:
logger.exception("Failed to get celery queue length")
return -1
@@ -82,7 +86,7 @@ def get_version_info():
}
try:
tags = cache.get_or_set('github_release_tags', get_github_tags, TAG_CACHE_TIME)
current_ver = semver.Version(__version__, partial=True)
current_ver = semver.Version.coerce(__version__)
# Set them all to the current version to start
# If the server has only earlier or the same version
@@ -102,7 +106,10 @@ def get_version_info():
if tag_name[0] == 'v':
# Strip 'v' off front of verison if it exists
tag_name = tag_name[1:]
tag_ver = semver.Version(tag_name, partial=True)
try:
tag_ver = semver.Version.coerce(tag_name)
except ValueError:
tag_ver = semver.Version('0.0.0', partial=True)
if tag_ver > current_ver:
if latest_major is None or tag_ver > latest_major:
latest_major = tag_ver

View File

@@ -32,14 +32,26 @@ class TimerForm(forms.ModelForm):
kwargs.update({'initial': initial})
super(TimerForm, self).__init__(*args, **kwargs)
structure_choices = [('POCO', 'POCO'), ('I-HUB', 'I-HUB'), ('POS[S]', 'POS[S]'),
('POS[M]', 'POS[M]'), ('POS[L]', 'POS[L]'), ('Citadel[M]', 'Citadel[M]'),
('Citadel[L]', 'Citadel[L]'), ('Citadel[XL]', 'Citadel[XL]'),
structure_choices = [('POCO', 'POCO'),
('I-HUB', 'I-HUB'),
('POS[S]', 'POS[S]'),
('POS[M]', 'POS[M]'),
('POS[L]', 'POS[L]'),
('Citadel[M]', 'Citadel[M]'),
('Citadel[L]', 'Citadel[L]'),
('Citadel[XL]', 'Citadel[XL]'),
('Engineering Complex[M]', 'Engineering Complex[M]'),
('Engineering Complex[L]', 'Engineering Complex[L]'),
('Engineering Complex[XL]', 'Engineering Complex[XL]'),
('Station', 'Station'), ('TCU', 'TCU'), (_('Other'), _('Other'))]
objective_choices = [('Friendly', _('Friendly')), ('Hostile', _('Hostile')), ('Neutral', _('Neutral'))]
('Refinery[M]', 'Refinery[M]'),
('Refinery[L]', 'Refinery[L]'),
('Station', 'Station'),
('TCU', 'TCU'),
('Moon Mining Cycle', 'Moon Mining Cycle'),
(_('Other'), _('Other'))]
objective_choices = [('Friendly', _('Friendly')),
('Hostile', _('Hostile')),
('Neutral', _('Neutral'))]
details = forms.CharField(max_length=254, required=True, label=_('Details'))
system = forms.CharField(max_length=254, required=True, label=_("System"))

View File

@@ -118,7 +118,6 @@
Engineering Complex [XL]
</div>
{% endifequal %}
{% ifequal timer.structure "Station" %}
<div class="label label-danger">
Station
@@ -129,6 +128,21 @@
TCU
</div>
{% endifequal %}
{% ifequal timer.structure "Refinery[M]" %}
<div class="label label-warning">
Refinery [M]
</div>
{% endifequal %}
{% ifequal timer.structure "Refinery[L]" %}
<div class="label label-warning">
Refinery [L]
</div>
{% endifequal %}
{% ifequal timer.structure "Moon Mining Cycle" %}
<div class="label label-success">
Moon Mining Cycle
</div>
{% endifequal %}
{% ifequal timer.structure "Other" %}
<div class="label label-default">
Other
@@ -263,6 +277,21 @@
TCU
</div>
{% endifequal %}
{% ifequal timer.structure "Refinery[M]" %}
<div class="label label-warning">
Refinery [M]
</div>
{% endifequal %}
{% ifequal timer.structure "Refinery[L]" %}
<div class="label label-warning">
Refinery [L]
</div>
{% endifequal %}
{% ifequal timer.structure "Moon Mining Cycle" %}
<div class="label label-success">
Moon Mining Cycle
</div>
{% endifequal %}
{% ifequal timer.structure "Other" %}
<div class="label label-default">
Other
@@ -399,6 +428,21 @@
TCU
</div>
{% endifequal %}
{% ifequal timer.structure "Refinery[M]" %}
<div class="label label-warning">
Refinery [M]
</div>
{% endifequal %}
{% ifequal timer.structure "Refinery[L]" %}
<div class="label label-warning">
Refinery [L]
</div>
{% endifequal %}
{% ifequal timer.structure "Moon Mining Cycle" %}
<div class="label label-success">
Moon Mining Cycle
</div>
{% endifequal %}
{% ifequal timer.structure "Other" %}
<div class="label label-default">
Other

View File

@@ -9,9 +9,9 @@ import allianceauth.authentication.urls
import allianceauth.notifications.urls
import allianceauth.groupmanagement.urls
import allianceauth.services.urls
from allianceauth.authentication.decorators import main_character_required, decorate_url_patterns
from allianceauth import NAME
from allianceauth import views
from allianceauth.authentication import hmac_urls
from allianceauth.hooks import get_hooks
@@ -42,13 +42,14 @@ urlpatterns = [
url(r'', include(allianceauth.groupmanagement.urls)),
# Services
url(r'', include(allianceauth.services.urls)),
url(r'', decorate_url_patterns(allianceauth.services.urls.urlpatterns, main_character_required)),
# Night mode
url(r'^night/', views.NightModeRedirectView.as_view(), name='nightmode')
]
# Append app urls
app_urls = get_hooks('url_hook')
for app in app_urls:
urlpatterns += [app().include_pattern]
urlpatterns += [url(r'', decorate_url_patterns([app().include_pattern], main_character_required))]

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 51 KiB

View File

@@ -1,10 +1,5 @@
# Menu Hooks
```eval_rst
.. note::
Currently most menu items are statically defined in the `base.html` template. Ideally this behaviour will change over time with each module of Alliance Auth providing all of its menu items via the hook. New modules should aim to use the hook over statically adding menu items to the base template.
```
The menu hooks allow you to dynamically specify menu items from your plugin app or service. To achieve this you should subclass or instantiate the `services.hooks.MenuItemHook` class and then register the menu item with one of the hooks.
To register a MenuItemHook class you would do the following:

View File

@@ -2,7 +2,7 @@
```eval_rst
.. note::
Currently most URL patterns are statically defined in the project's core urls.py file. Ideally this behaviour will change over time with each module of Alliance Auth providing all of its menu items via the hook. New modules should aim to use the hook over statically adding URL patterns to the project's patterns.
URLs added through URL Hooks are protected by a decorator which ensures the requesting user is logged in and has a main character set.
```
The URL hooks allow you to dynamically specify URL patterns from your plugin app or service. To achieve this you should subclass or instantiate the `services.hooks.UrlHook` class and then register the URL patterns with the hook.

View File

@@ -10,7 +10,7 @@ Auto groups allows you to automatically place users of certain states into Corp
## Installation
Add `allianceauth.eveonline.autogroups` to your `INSTALLED_APPS` and run migrations. All other settings are controlled via the admin panel under the `Eve_Autogroups` section.
Add `'allianceauth.eveonline.autogroups',` to your `INSTALLED_APPS` list and run migrations. All other settings are controlled via the admin panel under the `Eve_Autogroups` section.
## Configuring a group

View File

@@ -4,11 +4,9 @@ This module is used to check the registration status of corp members and to dete
## Installation
Add `allianceauth.corputils` to your `INSTALLED_APPS` setting and run migrations. In `myauth/settings/local.py`:
Corp Stats requires access to the `esi-corporations.read_corporation_membership.v1` SSO scope. Update your application on the [EVE Developers site](https://developers.eveonline.com) to ensure it is available.
INSTALLED_APPS += ['allianceauth.corputils']
Run migrations to complete installation.
Add `'allianceauth.corputils',` to your `INSTALLED_APPS` list in your auth project's settings file. Run migrations to complete installation.
## Creating a Corp Stats
@@ -43,9 +41,7 @@ On the right of this bar is a search field. Press enter to search. It checks all
![last update and update button](/_static/images/features/corpstats/last_update.png)
Corp Stats automatically update every 6 hours. An update can be performed immediately by pressing thi update button.
Only superusers and the creator of the Corp Stat can trigger an immediate update.
An update can be performed immediately by pressing the update button. Anyone who can view the Corp Stats can update it.
### Character Lists
@@ -114,6 +110,16 @@ 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.
## 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:
CELERYBEAT_SCHEDULE['update_all_corpstats'] = {
'task': 'allianceauth.corputils.tasks.update_all_corpstats',
'schedule': crontab(minute=0, hour="*/6"),
},
Adjust the crontab as desired.
## Troubleshooting
### Failure to create Corp Stats

View File

@@ -2,8 +2,6 @@
## Installation
Add `allianceauth.fleetactivitytracking` to your `INSTALLED_APPS` setting. In `myauth/settings/local.py`:
Fleet Activity Tracking requires access to the `esi-location.read_location.v1`, `esi-location.read_ship_type.v1`, and `esi-universe.read_structures.v1` SSO scopes. Update your application on the [EVE Developers site](https://developers.eveonline.com) to ensure these are available.
INSTALLED_APPS += ['allianceauth.fleetactivitytracking']
Run migrations to complete installation.
Add `'allianceauth.fleetactivitytracking',` to your `INSTALLED_APPS` list in your auth project's settings file. Run migrations to complete installation.

View File

@@ -2,6 +2,17 @@
## Installation
Add `allianceauth.fleetup` to your `INSTALLED_APPS` setting. In `myauth/settings/local.py`:
Add `'allianceauth.fleetup',` to your auth project's `INSTALLED_APPS` list.
INSTALLED_APPS += ['allianceauth.fleetup']
Additional settings are required. Append the following settings to the end of your auth project's settings file and fill them out.
FLEETUP_APP_KEY = '' # The app key from http://fleet-up.com/Api/MyApps
FLEETUP_USER_ID = '' # The user 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
Once filled out restart gunicorn and celery.
## Permissions
The Fleetup module is only visible to users with the `auth | user | view_fleeup` permission.

View File

@@ -1,35 +1,11 @@
# Groups
Group Management is one of the core tasks of Alliance Auth. Many of Alliance Auth's services allow for synchronising of group membership, allowing you to grant permissions or roles in services to access certain aspects of them.
## Automatic Groups
When a member registers in Alliance Auth and selects a main character, Auth will assign them some groups automatically based on some factors.
```eval_rst
.. important::
The ``Corp_`` and ``Alliance_`` group name prefixes are reserved for Alliance Auth internal group management. If you prefix a group with these you will find Alliance Auth automatically removes users from the group.
```
```eval_rst
+------------------------------+-----------------------------------------------------------------------------------+
| Group | Condition |
+------------------------------+-----------------------------------------------------------------------------------+
| ``Corp_<corp_name>`` | Users Main Character belongs to the Corporation |
+------------------------------+-----------------------------------------------------------------------------------+
| ``Alliance_<alliance_name>`` | Users Main Character belongs to the Alliance |
+------------------------------+-----------------------------------------------------------------------------------+
| ``Member`` | User is a member of one of the tenant Corps or Alliances |
+------------------------------+-----------------------------------------------------------------------------------+
| ``Blue`` | User is a member of a blue Corp or Alliance, be it via standings or static config |
+------------------------------+-----------------------------------------------------------------------------------+
```
When the user no longer has the condition required to be a member of that group they are automatically removed by Auth.
## User Organised Groups
Along with the automated groups, administrators can create custom groups for users to join. Examples might be groups like `Leadership`, `CEO` or `Scouts`.
Administrators can create custom groups for users to join. Examples might be groups like `Leadership`, `CEO` or `Scouts`.
When you create a Django `Group`, Auth automatically creates a corresponding `AuthGroup` model. The admin page looks like this:
When you create a `Group` additional settings are available beyond the normal Django group model. The admin page looks like this:
![AuthGroup Admin page](/_static/images/features/group-admin.png)

View File

@@ -2,11 +2,7 @@
## Installation
Add `allianceauth.hrapplications` to your `INSTALLED_APPS` setting. In `myauth/settings/local.py`:
INSTALLED_APPS += ['allianceauth.hrapplications']
Run migrations to complete installation.
Add `'allianceauth.hrapplications',` to your `INSTALLED_APPS` list in your auth project's settings file. Run migrations to complete installation.
## Management

View File

@@ -5,10 +5,11 @@
:maxdepth: 1
:caption: Features Contents
hrapplications
corpstats
states
groups
autogroups
hrapplications
corpstats
permissions_tool
nameformats
fleetup

View File

@@ -2,8 +2,4 @@
## Installation
Add `allianceauth.optimer` to your `INSTALLED_APPS` setting. In `myauth/settings/local.py`:
INSTALLED_APPS += ['allianceauth.optimer']
Run migrations to complete installation.
Add `'allianceauth.optimer',` to your `INSTALLED_APPS` list in your auth project's settings file. Run migrations to complete installation.

View File

@@ -9,9 +9,7 @@ Access to most of Alliance Auth's features are controlled by Django's permission
## Installation
Add `allianceauth.permissions_tool` to your `INSTALLED_APPS` setting. In `myauth/settings/local.py`:
INSTALLED_APPS += ['allianceauth.permissions_tool']
Add `'allianceauth.permissions_tool',` to your `INSTALLED_APPS` list in your auth project's settings file.
## Usage

View File

@@ -2,8 +2,4 @@
## Installation
Add `allianceauth.srp` to your `INSTALLED_APPS` setting. In `myauth/settings/local.py`:
INSTALLED_APPS += ['allianceauth.srp']
Run migrations to complete installation.
Add `'allianceauth.srp',` to your `INSTALLED_APPS` list in your auth project's settings file. Run migrations to complete installation.

49
docs/features/states.md Normal file
View File

@@ -0,0 +1,49 @@
# The State System
## 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.
## 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.
A number of fields are available and are described below.
### Name
This is the displayed name of a state. Should be self-explanatory.
### Permissions
This lets you select permissions to grant to the entire state, much like a group. Any user with this state will be granted these permissions.
A common use case would be granting service access to a state.
### Priority
This value determines the order in which states are applied to users. Higher numbers come first. So if a random user `Bob` could member of both the `Member` and `Blue` states, because `Member` has a higher priority `Bob` will be assigned to it.
### Public
Checking this box means this state is available to all users. There isn't much use for this outside the `Guest` state.
### Member Characters
This lets you select which characters the state is available to. Characters can be added by selecting the green plus icon.
### Member Corporations
This lets you select which corporations the state is available to. Corporations can be added by selecting the green plus icon.
### Member Alliances
This lets you select which alliances the state is available to. Alliances can be added by selecting the gree plus icon.
## Determining a User's State
States are mutually exclusive, meaning a user can only be in one at a time.
Membership is determined based on a user's main character. States are tested in order of descending priority - the first one which allows membership to the main character is assigned to the user.
States are automatically assigned when a user registers to the site, their main character changes, they are activated or deactivated, or states are edited. Note that editing states triggers lots of state checks so it can be a very slow process.
Assigned states are visible in the `Users` section of the `Authentication` admin site.
## The Guest State
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.

View File

@@ -2,8 +2,4 @@
## Installation
Add `allianceauth.timerboard` to your `INSTALLED_APPS` setting. In `myauth/settings/local.py`:
INSTALLED_APPS += ['allianceauth.timerboard']
Run migrations to complete installation.
Add `'allianceauth.timerboard',` to your `INSTALLED_APPS` list in your auth project's settings file. Run migrations to complete installation.

View File

@@ -1,7 +1,7 @@
# Alliance Auth
Alliance service auth to help large scale alliances manage services. Built for "The 99 Percent" open for anyone to use
An auth system for EVE Online to help in-game organizations manage online service access.
# Installing

View File

@@ -2,7 +2,7 @@
```eval_rst
.. tip::
Installation is easiest as the root user. Log in as root or a user with sudo powers.
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.
```
## Dependencies
@@ -66,16 +66,22 @@ CentOS:
Alliance Auth needs a MySQL user account and database. Open an SQL shell with `mysql -u root -p` and create them as follows, replacing `PASSWORD` with an actual secure password:
CREATE USER 'allianceserver'@'localhost' IDENTIFIED BY 'PASSWORD';
CREATE DATABASE alliance_auth;
CREATE DATABASE alliance_auth CHARACTER SET utf8;
GRANT ALL PRIVILEGES ON alliance_auth . * TO 'allianceserver'@'localhost';
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
### User Account
For security and permissions, its highly recommended you create a separate user to install under.
For security and permissions, its highly recommended you create a separate user to install auth under. Do not log in as this account.
Ubuntu:
@@ -91,6 +97,11 @@ Create a Python virtual environment and put it somewhere convenient (e.g. `/home
python3 -m venv /home/allianceserver/venv/auth/
```eval_rst
.. warning::
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
.. tip::
A virtual environment provides support for creating a lightweight "copy" of Python with their own site directories. Each virtual environment has its own Python binary (allowing creation of environments with various Python versions) and can have its own independent set of installed Python packages in its site directories. You can read more about virtual environments on the Python_ docs.
@@ -104,20 +115,15 @@ Activate the virtualenv using `source /home/allianceserver/venv/auth/bin/activat
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.
### Alliance Auth Project
You can install the library using `pip install allianceauth`. This will install Alliance Auth and all its python dependencies.
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`.
The `allianceauth start myauth` command will bootstrap a Django project which will run Alliance Auth. You can rename it from `myauth` to anything you'd like: this name is shown by default as the site name but that can be changed later.
```eval_rst
.. tip::
If you plan to use gunicorn as your WSGI server (recommended), ensure it is installed before starting your auth project to have an entry automatically created in the project's supervisor config file. ::
pip install gunicorn
```
The `allianceauth start myauth` command bootstraps a Django project which will run Alliance Auth. You can rename it from `myauth` to anything you'd like: this name is shown by default as the site name but that can be changed later.
The settings file needs configuring. Edit the template at `myauth/myauth/settings/local.py`. Be sure to configure the EVE SSO and Email settings.
@@ -129,7 +135,6 @@ Now we need to round up all the static files required to render templates. Make
mkdir -p /var/www/myauth/static
python /home/allianceserver/myauth/manage.py collectstatic
chown -R www-data:www-data /var/www/myauth/static
Check to ensure your settings are valid.
@@ -143,9 +148,9 @@ And finally ensure the allianceserver user has read/write permissions to this di
### 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. Installation is simple: `pip install gunicorn`. It can be manually called 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 with `gunicorn myauth.wsgi` or automatically run using supervisor.
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
@@ -163,8 +168,15 @@ CentOS:
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.
ln /home/allianceserver/myauth/supervisor.conf /etc/supervisor/conf.d/myauth.conf
supervisorctl reload
Ubuntu:
ln -s /home/allianceserver/myauth/supervisor.conf /etc/supervisor/conf.d/myauth.conf
CentOS:
ln -s /home/allianceserver/myauth/supervisor.conf /etc/supervisord.d/myauth.ini
And activate it with `supervisorctl reload`.
You can check the status of the processes with `supervisorctl status`. Logs from these processes are available in `/home/allianceserver/myauth/log` named by process.
@@ -185,10 +197,9 @@ Before using your auth site it is essential to create a superuser account. This
python /home/allianceserver/myauth/manage.py createsuperuser
```eval_rst
.. important::
Be sure to add a main character to this account before attempting to activate services with it.
```
The superuser account is accessed by logging in via the admin site at `https://example.com/admin`.
If you intend to use this account as your personal auth account you need to add a main character. Navigate to the normal user dashboard (at `https://example.com`) after logging in via the admin site and select `Change Main`. Once a main character has been added it is possible to use SSO to login to this account.
## Updating
@@ -198,4 +209,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`.
Always restart celery and gunicorn after updating.
Always restart celery and gunicorn after updating.

View File

@@ -1,108 +1,71 @@
# Apache Setup
### Overview
# Apache
AllianceAuth gets served using a Web Server Gateway Interface (WSGI) script. This script passes web requests to AllianceAuth which generates the content to be displayed and returns it. This means very little has to be configured in Apache to host AllianceAuth.
## Overview
In the interest of ~~laziness~~ time-efficiency, scroll down for example configs. Use these, changing the ServerName to your domain name.
Alliance Auth gets served using a Web Server Gateway Interface (WSGI) script. This script passes web requests to Alliance Auth which generates the content to be displayed and returns it. This means very little has to be configured in Apache to host Alliance Auth.
If you're using a small VPS to host services with very limited memory resources, consider using NGINX with [Gunicorn](gunicorn.md). Even if you would like to use Apache, Gunicorn may give you lower memory usage over mod_wsgi.
If you're using a small VPS to host services with very limited memory, consider using [NGINX](nginx.md).
### Required Parameters for AllianceAuth Core
## Installation
The AllianceAuth core requires the following parameters to be set:
Ubuntu:
WSGIDaemonProcess
WSGIProcessGroup
WSGIScriptAlias
apt-get install apache2
The following aliases are required:
CentOS:
Alias /static/ to point at the static folder
Alias /templates/ to point at the templates folder
yum install httpd
systemctl enable httpd
systemctl start httpd
## Description of Parameters
## Configuration
- `WSGIDaemonProcess` is the name of the process/application. It also needs to be passed the python-path parameter directing python to search the AllianceAuth directory for modules to load.
- `WSGIProcessGroup` is the group to run the process under. Typically the same as the name of the process/application.
- `WSGIScriptAlias` points to the WSGI script.
Apache needs to be able to read the folder containing your auth project's static files. On Ubuntu: `chown -R www-data:www-data /var/www/myauth/static`, and on CentOS: `chown -R apache:apache /var/www/myauth/static`
## Additional Parameters for Full Setup
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.
To pass additional services the following aliases and directories are required:
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`.
- `Alias /forums` to point at the forums folder
- `Alias /killboard` to point at the killboard
### Ubuntu
Each of these require directory permissions allowing all connections.
To proxy and modify headers a few mods need to be enabled.
For Apache 2.4 or greater:
a2enmod proxy
a2enmod proxy_http
a2enmod headers
<Directory "/path/to/alias/folder">
Require all granted
</Directory>
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`.
For Apache 2.3 or older:
### CentOS
<Directory "/path/to/alias/folder">
Order Deny,Allow
Allow from all
</Directory>
Place your virtual host configuration in the appropriate section within `/etc/httpd/conf.d/httpd.conf` and restart the httpd service with `systemctl restart httpd`.
## SSL
You can supply your own SSL certificates if you so desire. The alternative is running behind cloudflare for free SSL.
## Sample Config Files
### Minimally functional config
## Sample Config File
```
<VirtualHost *:80>
ServerName example.com
ServerAdmin webmaster@localhost
DocumentRoot /var/www
WSGIDaemonProcess allianceauth python-path=/home/allianceserver/allianceauth
WSGIProcessGroup allianceauth
WSGIScriptAlias / /home/allianceserver/allianceauth/alliance_auth/wsgi.py
Alias /static/ /home/allianceserver/allianceauth/static/
<Directory /home/allianceserver/allianceauth/>
Require all granted
ServerName auth.example.com
ProxyPassMatch ^/static !
ProxyPass / http://127.0.0.1:8000/
ProxyPassReverse / http://127.0.0.1:8000/
ProxyPreserveHost On
Alias "/static" "/var/www/myauth/static"
<Directory "/var/www/myauth/static">
Require all granted
</Directory>
<Directory /var/www/>
Require all granted
</Directory>
</VirtualHost>
```
### Own SSL Cert
- Apache 2.4 or newer:
- [000-default.conf](http://pastebin.com/3LLzyNmV)
- [default-ssl.conf](http://pastebin.com/HUPPEp0R)
- Apache 2.3 or older:
- [000-default](http://pastebin.com/HfyKpQNu)
- [default-ssl](http://pastebin.com/2WCS5jnb)
## SSL
### No SSL Cloudflare, or LetsEncrypt
- Apache 2.4 or newer:
- [000-default.conf](http://pastebin.com/j1Ps3ZK6)
- Apache 2.3 or older:
- [000-default](http://pastebin.com/BHQzf2pj)
It's 2018 - there's no reason to run a site without SSL. The EFF provides free, renewable SSL certificates with an automated installer. Visit their [website](https://certbot.eff.org/) for information.
To have LetsEncrypt automatically install SSL certs, comment out the three lines starting with `WSGI`, install certificates, then uncomment them in `000-default-ls-ssl.conf`
After acquiring SSL the config file needs to be adjusted. Add the following lines inside the `<VirtualHost>` block:
## Enabling and Disabling Sites
To instruct apache to serve traffic from a virtual host, enable it:
sudo a2ensite NAME
where NAME is the name of the configuration file (eg 000-default.conf)
To disable traffic from a site, disable the virtual host:
sudo a2dissite NAME
where NAME is the name of the configuration file (eg 000-default.conf)
```
RequestHeader set X-FORWARDED-PROTOCOL https
RequestHeader set X-FORWARDED-SSL On
```

View File

@@ -61,7 +61,7 @@ Change it by adding `--workers=2` to the command.
##### Running with a virtual environment
If you're running with a virtual environment, you'll need to add the path to the `command=` config line.
e.g. `command=/path/to/venv/bin/gunicorn alliance_auth.wsgi`
e.g. `command=/path/to/venv/bin/gunicorn myauth.wsgi`
### Starting via Supervisor
@@ -70,48 +70,6 @@ Once you have your configuration all sorted, you will need to reload your superv
## Configuring your webserver
### NGINX
To your server config add:
```
location / {
proxy_pass http://127.0.0.1:8000;
proxy_read_timeout 90;
proxy_redirect http://127.0.0.1:8000/ http://$host/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Proto $scheme;
}
```
Set `proxy_pass` and `proxy_redirect` to the address you set under `--bind=`. Set the second part of `proxy_redirect` to the URL you're hosting services on. Tell NGINX to reload your config, job done. Enjoy your lower memory usage and better performance!
If PHP is stopping you moving to NGINX, check out php-fpm as a way to run your PHP applications.
### Apache
If you were using mod_wsgi before, make a backup of your old config first and then strip out all of the mod_wsgi config from your Apache VirtualHost first config.
Your config will need something along these lines:
```
ProxyPreserveHost On
<Location />
SSLRequireSSL
ProxyPass http://127.0.0.1:8000/
ProxyPassReverse http://127.0.0.1:8000/
RequestHeader set X-FORWARDED-PROTOCOL ssl
RequestHeader set X-FORWARDED-SSL on
</Location>
```
Set `ProxyPass` and `ProxyPassReverse` addresses to your `--bind=` address set earlier.
You will need to enable some Apache mods. `sudo a2enmod http_proxy` should take care of the dependencies.
Restart Apache and you should be done.
### Other web servers
Any web server capable of proxy passing should be able to sit in front of Gunicorn. Consult their documentation armed with your `--bind=` address and you should be able to find how to do it relatively easy.

Some files were not shown because too many files have changed in this diff Show More