mirror of
https://gitlab.com/allianceauth/allianceauth.git
synced 2025-07-14 06:50:15 +02:00
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
This commit is contained in:
parent
552c795041
commit
cc8a7a18d2
37
allianceauth/authentication/decorators.py
Normal file
37
allianceauth/authentication/decorators.py
Normal 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)
|
@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
{% else %}
|
{% 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 %}
|
{% endif %}
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
<div class="col-xs-6">
|
<div class="col-xs-6">
|
||||||
|
@ -8,6 +8,61 @@ from .backends import StateBackend
|
|||||||
from .tasks import check_character_ownership
|
from .tasks import check_character_ownership
|
||||||
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
|
from allianceauth.eveonline.models import EveCharacter, EveCorporationInfo, EveAllianceInfo
|
||||||
from esi.models import Token
|
from esi.models import Token
|
||||||
|
from 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):
|
class BackendTestCase(TestCase):
|
||||||
|
@ -8,6 +8,7 @@ from allianceauth.tests.auth_utils import AuthUtils
|
|||||||
class PermissionsToolViewsTestCase(WebTest):
|
class PermissionsToolViewsTestCase(WebTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.member = AuthUtils.create_member('auth_member')
|
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.email = 'auth_member@example.com'
|
||||||
self.member.save()
|
self.member.save()
|
||||||
self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True)
|
self.none_user = AuthUtils.create_user('none_user', disconnect_signals=True)
|
||||||
|
@ -145,6 +145,7 @@ class DiscordHooksTestCase(TestCase):
|
|||||||
class DiscordViewsTestCase(WebTest):
|
class DiscordViewsTestCase(WebTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.member = AuthUtils.create_member('auth_member')
|
self.member = AuthUtils.create_member('auth_member')
|
||||||
|
AuthUtils.add_main_character(self.member, 'test character', '1234', '2345', 'test corp', 'testc')
|
||||||
add_permissions()
|
add_permissions()
|
||||||
|
|
||||||
def login(self):
|
def login(self):
|
||||||
|
@ -9,9 +9,9 @@ import allianceauth.authentication.urls
|
|||||||
import allianceauth.notifications.urls
|
import allianceauth.notifications.urls
|
||||||
import allianceauth.groupmanagement.urls
|
import allianceauth.groupmanagement.urls
|
||||||
import allianceauth.services.urls
|
import allianceauth.services.urls
|
||||||
|
from allianceauth.authentication.decorators import main_character_required, decorate_url_patterns
|
||||||
from allianceauth import NAME
|
from allianceauth import NAME
|
||||||
from allianceauth import views
|
from allianceauth import views
|
||||||
|
|
||||||
from allianceauth.authentication import hmac_urls
|
from allianceauth.authentication import hmac_urls
|
||||||
from allianceauth.hooks import get_hooks
|
from allianceauth.hooks import get_hooks
|
||||||
|
|
||||||
@ -42,13 +42,14 @@ urlpatterns = [
|
|||||||
url(r'', include(allianceauth.groupmanagement.urls)),
|
url(r'', include(allianceauth.groupmanagement.urls)),
|
||||||
|
|
||||||
# Services
|
# Services
|
||||||
url(r'', include(allianceauth.services.urls)),
|
url(r'', decorate_url_patterns(allianceauth.services.urls.urlpatterns, main_character_required)),
|
||||||
|
|
||||||
# Night mode
|
# Night mode
|
||||||
url(r'^night/', views.NightModeRedirectView.as_view(), name='nightmode')
|
url(r'^night/', views.NightModeRedirectView.as_view(), name='nightmode')
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
# Append app urls
|
# Append app urls
|
||||||
app_urls = get_hooks('url_hook')
|
app_urls = get_hooks('url_hook')
|
||||||
for app in app_urls:
|
for app in app_urls:
|
||||||
urlpatterns += [app().include_pattern]
|
urlpatterns += [url(r'', decorate_url_patterns([app().include_pattern], main_character_required))]
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
```eval_rst
|
```eval_rst
|
||||||
.. note::
|
.. 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.
|
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.
|
||||||
|
Loading…
x
Reference in New Issue
Block a user