django-esi 8.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- django_esi-8.1.0.dist-info/METADATA +93 -0
- django_esi-8.1.0.dist-info/RECORD +100 -0
- django_esi-8.1.0.dist-info/WHEEL +4 -0
- django_esi-8.1.0.dist-info/licenses/LICENSE +674 -0
- esi/__init__.py +7 -0
- esi/admin.py +42 -0
- esi/aiopenapi3/client.py +79 -0
- esi/aiopenapi3/plugins.py +224 -0
- esi/app_settings.py +112 -0
- esi/apps.py +11 -0
- esi/checks.py +56 -0
- esi/clients.py +657 -0
- esi/decorators.py +271 -0
- esi/errors.py +22 -0
- esi/exceptions.py +51 -0
- esi/helpers.py +63 -0
- esi/locale/cs_CZ/LC_MESSAGES/django.mo +0 -0
- esi/locale/cs_CZ/LC_MESSAGES/django.po +53 -0
- esi/locale/de/LC_MESSAGES/django.mo +0 -0
- esi/locale/de/LC_MESSAGES/django.po +58 -0
- esi/locale/en/LC_MESSAGES/django.mo +0 -0
- esi/locale/en/LC_MESSAGES/django.po +54 -0
- esi/locale/es/LC_MESSAGES/django.mo +0 -0
- esi/locale/es/LC_MESSAGES/django.po +59 -0
- esi/locale/fr_FR/LC_MESSAGES/django.mo +0 -0
- esi/locale/fr_FR/LC_MESSAGES/django.po +59 -0
- esi/locale/it_IT/LC_MESSAGES/django.mo +0 -0
- esi/locale/it_IT/LC_MESSAGES/django.po +59 -0
- esi/locale/ja/LC_MESSAGES/django.mo +0 -0
- esi/locale/ja/LC_MESSAGES/django.po +58 -0
- esi/locale/ko_KR/LC_MESSAGES/django.mo +0 -0
- esi/locale/ko_KR/LC_MESSAGES/django.po +58 -0
- esi/locale/nl_NL/LC_MESSAGES/django.mo +0 -0
- esi/locale/nl_NL/LC_MESSAGES/django.po +53 -0
- esi/locale/pl_PL/LC_MESSAGES/django.mo +0 -0
- esi/locale/pl_PL/LC_MESSAGES/django.po +53 -0
- esi/locale/ru/LC_MESSAGES/django.mo +0 -0
- esi/locale/ru/LC_MESSAGES/django.po +61 -0
- esi/locale/sk/LC_MESSAGES/django.mo +0 -0
- esi/locale/sk/LC_MESSAGES/django.po +55 -0
- esi/locale/uk/LC_MESSAGES/django.mo +0 -0
- esi/locale/uk/LC_MESSAGES/django.po +57 -0
- esi/locale/zh_Hans/LC_MESSAGES/django.mo +0 -0
- esi/locale/zh_Hans/LC_MESSAGES/django.po +58 -0
- esi/management/commands/__init__.py +0 -0
- esi/management/commands/esi_clear_spec_cache.py +21 -0
- esi/management/commands/generate_esi_stubs.py +661 -0
- esi/management/commands/migrate_to_ssov2.py +188 -0
- esi/managers.py +303 -0
- esi/managers.pyi +85 -0
- esi/migrations/0001_initial.py +55 -0
- esi/migrations/0002_scopes_20161208.py +56 -0
- esi/migrations/0003_hide_tokens_from_admin_site.py +23 -0
- esi/migrations/0004_remove_unique_access_token.py +18 -0
- esi/migrations/0005_remove_token_length_limit.py +23 -0
- esi/migrations/0006_remove_url_length_limit.py +18 -0
- esi/migrations/0007_fix_mysql_8_migration.py +18 -0
- esi/migrations/0008_nullable_refresh_token.py +18 -0
- esi/migrations/0009_set_old_tokens_to_sso_v1.py +18 -0
- esi/migrations/0010_set_new_tokens_to_sso_v2.py +18 -0
- esi/migrations/0011_add_token_indices.py +28 -0
- esi/migrations/0012_fix_token_type_choices.py +18 -0
- esi/migrations/0013_squashed_0012_fix_token_type_choices.py +57 -0
- esi/migrations/__init__.py +0 -0
- esi/models.py +349 -0
- esi/openapi_clients.py +1225 -0
- esi/rate_limiting.py +107 -0
- esi/signals.py +21 -0
- esi/static/esi/img/EVE_SSO_Login_Buttons_Large_Black.png +0 -0
- esi/static/esi/img/EVE_SSO_Login_Buttons_Large_White.png +0 -0
- esi/static/esi/img/EVE_SSO_Login_Buttons_Small_Black.png +0 -0
- esi/static/esi/img/EVE_SSO_Login_Buttons_Small_White.png +0 -0
- esi/stubs.py +2 -0
- esi/stubs.pyi +6807 -0
- esi/tasks.py +78 -0
- esi/templates/esi/select_token.html +116 -0
- esi/templatetags/__init__.py +0 -0
- esi/templatetags/scope_tags.py +8 -0
- esi/tests/__init__.py +134 -0
- esi/tests/client_authed_pilot.py +63 -0
- esi/tests/client_public_pilot.py +53 -0
- esi/tests/factories.py +47 -0
- esi/tests/factories_2.py +60 -0
- esi/tests/jwt_factory.py +135 -0
- esi/tests/test_checks.py +48 -0
- esi/tests/test_clients.py +1019 -0
- esi/tests/test_decorators.py +578 -0
- esi/tests/test_management_command.py +307 -0
- esi/tests/test_managers.py +673 -0
- esi/tests/test_models.py +403 -0
- esi/tests/test_openapi.json +854 -0
- esi/tests/test_openapi.py +1017 -0
- esi/tests/test_swagger.json +489 -0
- esi/tests/test_swagger_full.json +51112 -0
- esi/tests/test_tasks.py +116 -0
- esi/tests/test_templatetags.py +22 -0
- esi/tests/test_views.py +331 -0
- esi/tests/threading_pilot.py +69 -0
- esi/urls.py +9 -0
- esi/views.py +129 -0
esi/tests/test_tasks.py
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from datetime import timedelta
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
from celery import current_app as celery_app
|
|
5
|
+
from django.utils.timezone import now
|
|
6
|
+
|
|
7
|
+
from esi.models import CallbackRedirect, Token
|
|
8
|
+
from esi.tasks import cleanup_callbackredirect, cleanup_token, cleanup_token_subset
|
|
9
|
+
|
|
10
|
+
from . import NoSocketsTestCase
|
|
11
|
+
from .factories_2 import TokenFactory, CallbackRedirectFactory
|
|
12
|
+
|
|
13
|
+
from math import ceil
|
|
14
|
+
|
|
15
|
+
MANAGERS_PATH = "esi.managers"
|
|
16
|
+
MODELS_PATH = "esi.models"
|
|
17
|
+
TASKS_PATH = "esi.tasks"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class CeleryTestCase(NoSocketsTestCase):
|
|
21
|
+
@classmethod
|
|
22
|
+
def setUpClass(cls):
|
|
23
|
+
super().setUpClass()
|
|
24
|
+
celery_app.conf.task_always_eager = True
|
|
25
|
+
celery_app.conf.task_eager_propagates = True
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class TestCleanupCallbackredirect(CeleryTestCase):
|
|
29
|
+
def test_should_remove_expired(self) -> None:
|
|
30
|
+
# given
|
|
31
|
+
cb_valid = CallbackRedirectFactory()
|
|
32
|
+
with patch("django.utils.timezone.now") as m:
|
|
33
|
+
m.return_value = now() - timedelta(minutes=5, seconds=1)
|
|
34
|
+
cb_expired = CallbackRedirectFactory()
|
|
35
|
+
# when
|
|
36
|
+
cleanup_callbackredirect.delay(max_age=300)
|
|
37
|
+
# then
|
|
38
|
+
self.assertTrue(CallbackRedirect.objects.filter(pk=cb_valid.pk).exists())
|
|
39
|
+
self.assertFalse(CallbackRedirect.objects.filter(pk=cb_expired.pk).exists())
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 120)
|
|
43
|
+
@patch(MODELS_PATH + '.Token.refresh', spec=True)
|
|
44
|
+
class TestCleanupToken(CeleryTestCase):
|
|
45
|
+
def test_should_delete_orphaned_tokens(self, mock_token_refresh) -> None:
|
|
46
|
+
# given
|
|
47
|
+
token_1 = TokenFactory(user=None)
|
|
48
|
+
token_2 = TokenFactory()
|
|
49
|
+
# when
|
|
50
|
+
cleanup_token.delay()
|
|
51
|
+
# then
|
|
52
|
+
self.assertFalse(Token.objects.filter(pk=token_1.pk).exists())
|
|
53
|
+
self.assertTrue(Token.objects.filter(pk=token_2.pk).exists())
|
|
54
|
+
|
|
55
|
+
def test_should_refresh_expired_tokens_only(self, mock_token_refresh) -> None:
|
|
56
|
+
# given
|
|
57
|
+
TokenFactory()
|
|
58
|
+
with patch("django.utils.timezone.now") as m:
|
|
59
|
+
m.return_value = now() - timedelta(minutes=3)
|
|
60
|
+
TokenFactory()
|
|
61
|
+
# when
|
|
62
|
+
cleanup_token.delay()
|
|
63
|
+
# then
|
|
64
|
+
self.assertEqual(mock_token_refresh.call_count, 1)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class TestCleanupTokenSubset(CeleryTestCase):
|
|
68
|
+
@patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 120)
|
|
69
|
+
@patch(TASKS_PATH + ".refresh_or_delete_token.apply_async")
|
|
70
|
+
def test_should_delete_orphaned_tokens(self, mock_token_refresh) -> None:
|
|
71
|
+
# given
|
|
72
|
+
orphaned = TokenFactory(user=None)
|
|
73
|
+
valid = TokenFactory()
|
|
74
|
+
# when
|
|
75
|
+
cleanup_token_subset.delay()
|
|
76
|
+
# then
|
|
77
|
+
self.assertFalse(Token.objects.filter(pk=orphaned.pk).exists())
|
|
78
|
+
self.assertTrue(Token.objects.filter(pk=valid.pk).exists())
|
|
79
|
+
|
|
80
|
+
@patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 1)
|
|
81
|
+
@patch(TASKS_PATH + ".refresh_or_delete_token.apply_async")
|
|
82
|
+
def test_should_refresh_fraction_of_expired_tokens(self, mock_token_refresh) -> None:
|
|
83
|
+
# given
|
|
84
|
+
num_expired = 100
|
|
85
|
+
for _ in range(num_expired):
|
|
86
|
+
TokenFactory(refresh_token="some_token")
|
|
87
|
+
# patch time so all tokens are expired
|
|
88
|
+
with patch("django.utils.timezone.now") as m:
|
|
89
|
+
m.return_value = now() + timedelta(minutes=5)
|
|
90
|
+
# when
|
|
91
|
+
cleanup_token_subset.delay(fraction=10)
|
|
92
|
+
# then
|
|
93
|
+
expected_count = ceil(num_expired / 10)
|
|
94
|
+
self.assertEqual(mock_token_refresh.call_count, expected_count)
|
|
95
|
+
|
|
96
|
+
@patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 1)
|
|
97
|
+
@patch("esi.tasks.refresh_or_delete_token.apply_async")
|
|
98
|
+
def test_should_refresh_expired_tokens_only(self, mock_token_refresh) -> None:
|
|
99
|
+
# given
|
|
100
|
+
TokenFactory()
|
|
101
|
+
with patch("django.utils.timezone.now") as m:
|
|
102
|
+
m.return_value = now() - timedelta(minutes=3)
|
|
103
|
+
TokenFactory()
|
|
104
|
+
# when
|
|
105
|
+
cleanup_token_subset.delay()
|
|
106
|
+
# then
|
|
107
|
+
self.assertEqual(mock_token_refresh.call_count, 1)
|
|
108
|
+
|
|
109
|
+
@patch(MANAGERS_PATH + '.app_settings.ESI_TOKEN_VALID_DURATION', 1)
|
|
110
|
+
@patch(TASKS_PATH + ".refresh_or_delete_token.apply_async")
|
|
111
|
+
def test_should_log_and_exit_gracefully_with_no_tokens(self, mock_token_refresh) -> None:
|
|
112
|
+
# when
|
|
113
|
+
cleanup_token_subset.delay()
|
|
114
|
+
# then
|
|
115
|
+
self.assertEqual(Token.objects.count(), 0)
|
|
116
|
+
mock_token_refresh.assert_not_called()
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""unit tests for esi"""
|
|
2
|
+
|
|
3
|
+
from django.test import TestCase
|
|
4
|
+
from ..templatetags.scope_tags import scope_friendly_name
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestScope(TestCase):
|
|
8
|
+
"""tests for template tag"""
|
|
9
|
+
|
|
10
|
+
def test_friendly_name_fail(self):
|
|
11
|
+
x = scope_friendly_name('dummy_scope')
|
|
12
|
+
self.assertEqual(
|
|
13
|
+
"dummy_scope",
|
|
14
|
+
x
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
def test_friendly_name_pass(self):
|
|
18
|
+
x = scope_friendly_name('test.dummy_scope.test')
|
|
19
|
+
self.assertEqual(
|
|
20
|
+
"dummy scope",
|
|
21
|
+
x
|
|
22
|
+
)
|
esi/tests/test_views.py
ADDED
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
from django.contrib.auth.models import User
|
|
4
|
+
from django.contrib.sessions.middleware import SessionMiddleware
|
|
5
|
+
from django.http import (
|
|
6
|
+
HttpResponse,
|
|
7
|
+
HttpResponseRedirect,
|
|
8
|
+
Http404,
|
|
9
|
+
HttpResponseBadRequest
|
|
10
|
+
)
|
|
11
|
+
from django.test import TestCase, RequestFactory
|
|
12
|
+
|
|
13
|
+
from . import _generate_token, _store_as_Token
|
|
14
|
+
from ..models import CallbackRedirect
|
|
15
|
+
from ..views import sso_redirect, receive_callback, select_token
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
ESI_SSO_CLIENT_ID = 'abc'
|
|
19
|
+
ESI_SSO_CALLBACK_URL = 'https://www.example.com/callback/'
|
|
20
|
+
ESI_OAUTH_LOGIN_URL = 'https://www.example.com/oauth/'
|
|
21
|
+
|
|
22
|
+
redirect_sub_url = '/%s/' % ('x' * (2048 - 25))
|
|
23
|
+
redirect_url = 'https://www.example.com' + redirect_sub_url
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class TestSsoCallbackView(TestCase):
|
|
27
|
+
|
|
28
|
+
def setUp(self):
|
|
29
|
+
self.user = User.objects.create_user(
|
|
30
|
+
'Bruce Wayne',
|
|
31
|
+
'abc@example.com',
|
|
32
|
+
'password'
|
|
33
|
+
)
|
|
34
|
+
self.token = _store_as_Token(
|
|
35
|
+
_generate_token(
|
|
36
|
+
character_id=99,
|
|
37
|
+
character_name=self.user.username,
|
|
38
|
+
scopes=['abc', 'xyz', '123']
|
|
39
|
+
),
|
|
40
|
+
self.user
|
|
41
|
+
)
|
|
42
|
+
self.factory = RequestFactory()
|
|
43
|
+
CallbackRedirect.objects.all().delete()
|
|
44
|
+
|
|
45
|
+
@patch('esi.views.app_settings.ESI_SSO_CLIENT_ID', ESI_SSO_CLIENT_ID)
|
|
46
|
+
@patch('esi.views.app_settings.ESI_SSO_CALLBACK_URL', ESI_SSO_CALLBACK_URL)
|
|
47
|
+
@patch('esi.views.app_settings.ESI_OAUTH_LOGIN_URL', ESI_OAUTH_LOGIN_URL)
|
|
48
|
+
@patch('esi.views.OAuth2Session', autospec=True)
|
|
49
|
+
def test_redirect_to_url_no_scopes(self, mock_OAuth2Session):
|
|
50
|
+
state = 'my_awesome_state'
|
|
51
|
+
mock_OAuth2Session.return_value.authorization_url.return_value = \
|
|
52
|
+
(redirect_url, state)
|
|
53
|
+
|
|
54
|
+
request = self.factory.get(redirect_url)
|
|
55
|
+
request.user = self.user
|
|
56
|
+
middleware = SessionMiddleware(HttpResponse)
|
|
57
|
+
middleware.process_request(request)
|
|
58
|
+
request.session.save()
|
|
59
|
+
|
|
60
|
+
http_response = sso_redirect(request)
|
|
61
|
+
|
|
62
|
+
self.assertTrue(mock_OAuth2Session.called)
|
|
63
|
+
args, kwargs = mock_OAuth2Session.call_args
|
|
64
|
+
self.assertEqual(kwargs['scope'], [])
|
|
65
|
+
|
|
66
|
+
self.assertEqual(http_response.url, redirect_url)
|
|
67
|
+
|
|
68
|
+
callback_redirects = (
|
|
69
|
+
CallbackRedirect.objects.filter(session_key=request.session.session_key)
|
|
70
|
+
)
|
|
71
|
+
self.assertEqual(len(callback_redirects), 1)
|
|
72
|
+
callback_redirect = callback_redirects.first()
|
|
73
|
+
self.assertEqual(callback_redirect.url, redirect_sub_url)
|
|
74
|
+
self.assertEqual(callback_redirect.session_key, request.session.session_key)
|
|
75
|
+
self.assertEqual(callback_redirect.state, state)
|
|
76
|
+
|
|
77
|
+
@patch('esi.views.app_settings.ESI_SSO_CLIENT_ID', ESI_SSO_CLIENT_ID)
|
|
78
|
+
@patch('esi.views.app_settings.ESI_SSO_CALLBACK_URL', ESI_SSO_CALLBACK_URL)
|
|
79
|
+
@patch('esi.views.app_settings.ESI_OAUTH_LOGIN_URL', ESI_OAUTH_LOGIN_URL)
|
|
80
|
+
@patch('esi.views.OAuth2Session', autospec=True)
|
|
81
|
+
def test_redirect_to_url_w_single_scope(self, mock_OAuth2Session):
|
|
82
|
+
state = 'my_awesome_state'
|
|
83
|
+
mock_OAuth2Session.return_value.authorization_url.return_value = \
|
|
84
|
+
(redirect_url, state)
|
|
85
|
+
|
|
86
|
+
request = self.factory.get(redirect_url)
|
|
87
|
+
request.user = self.user
|
|
88
|
+
middleware = SessionMiddleware(HttpResponse)
|
|
89
|
+
middleware.process_request(request)
|
|
90
|
+
request.session.save()
|
|
91
|
+
|
|
92
|
+
http_response = sso_redirect(request, scopes='abc')
|
|
93
|
+
self.assertTrue(mock_OAuth2Session.called)
|
|
94
|
+
args, kwargs = mock_OAuth2Session.call_args
|
|
95
|
+
self.assertEqual(kwargs['scope'], ['abc'])
|
|
96
|
+
|
|
97
|
+
self.assertEqual(http_response.url, redirect_url)
|
|
98
|
+
|
|
99
|
+
callback_redirects = (
|
|
100
|
+
CallbackRedirect.objects.filter(session_key=request.session.session_key)
|
|
101
|
+
)
|
|
102
|
+
self.assertEqual(len(callback_redirects), 1)
|
|
103
|
+
callback_redirect = callback_redirects.first()
|
|
104
|
+
self.assertEqual(callback_redirect.url, redirect_sub_url)
|
|
105
|
+
self.assertEqual(callback_redirect.session_key, request.session.session_key)
|
|
106
|
+
self.assertEqual(callback_redirect.state, state)
|
|
107
|
+
|
|
108
|
+
@patch('esi.views.app_settings.ESI_SSO_CLIENT_ID', ESI_SSO_CLIENT_ID)
|
|
109
|
+
@patch('esi.views.app_settings.ESI_SSO_CALLBACK_URL', ESI_SSO_CALLBACK_URL)
|
|
110
|
+
@patch('esi.views.app_settings.ESI_OAUTH_LOGIN_URL', ESI_OAUTH_LOGIN_URL)
|
|
111
|
+
@patch('esi.views.OAuth2Session', autospec=True)
|
|
112
|
+
def test_redirect_to_url_w_multiple_scopes(self, mock_OAuth2Session):
|
|
113
|
+
state = 'my_awesome_state'
|
|
114
|
+
mock_OAuth2Session.return_value.authorization_url.return_value = \
|
|
115
|
+
(redirect_url, state)
|
|
116
|
+
|
|
117
|
+
request = self.factory.get(redirect_url)
|
|
118
|
+
request.user = self.user
|
|
119
|
+
middleware = SessionMiddleware(HttpResponse)
|
|
120
|
+
middleware.process_request(request)
|
|
121
|
+
request.session.save()
|
|
122
|
+
|
|
123
|
+
http_response = sso_redirect(request, scopes=['abc', 'def'])
|
|
124
|
+
self.assertTrue(mock_OAuth2Session.called)
|
|
125
|
+
args, kwargs = mock_OAuth2Session.call_args
|
|
126
|
+
self.assertEqual(kwargs['scope'], ['abc', 'def'])
|
|
127
|
+
|
|
128
|
+
self.assertEqual(http_response.url, redirect_url)
|
|
129
|
+
|
|
130
|
+
callback_redirects = CallbackRedirect.objects\
|
|
131
|
+
.filter(session_key=request.session.session_key)
|
|
132
|
+
self.assertEqual(len(callback_redirects), 1)
|
|
133
|
+
callback_redirect = callback_redirects.first()
|
|
134
|
+
self.assertEqual(callback_redirect.url, redirect_sub_url)
|
|
135
|
+
self.assertEqual(callback_redirect.session_key, request.session.session_key)
|
|
136
|
+
self.assertEqual(callback_redirect.state, state)
|
|
137
|
+
|
|
138
|
+
@patch('esi.views.app_settings.ESI_SSO_CLIENT_ID', ESI_SSO_CLIENT_ID)
|
|
139
|
+
@patch('esi.views.app_settings.ESI_SSO_CALLBACK_URL', ESI_SSO_CALLBACK_URL)
|
|
140
|
+
@patch('esi.views.app_settings.ESI_OAUTH_LOGIN_URL', ESI_OAUTH_LOGIN_URL)
|
|
141
|
+
@patch('esi.views.reverse', autospec=True)
|
|
142
|
+
@patch('esi.views.OAuth2Session', autospec=True)
|
|
143
|
+
def test_redirect_to_view_no_scopes(self, mock_OAuth2Session, mock_reverse):
|
|
144
|
+
state = 'my_awesome_state'
|
|
145
|
+
mock_OAuth2Session.return_value.authorization_url.return_value = \
|
|
146
|
+
(redirect_url, state)
|
|
147
|
+
my_view_url = '/my_view/'
|
|
148
|
+
mock_reverse.return_value = my_view_url
|
|
149
|
+
|
|
150
|
+
request = self.factory.get('https://www.example.com/callback2/')
|
|
151
|
+
request.user = self.user
|
|
152
|
+
middleware = SessionMiddleware(HttpResponse)
|
|
153
|
+
middleware.process_request(request)
|
|
154
|
+
request.session.save()
|
|
155
|
+
|
|
156
|
+
http_response = sso_redirect(request, return_to='my_view')
|
|
157
|
+
|
|
158
|
+
self.assertTrue(mock_OAuth2Session.called)
|
|
159
|
+
args, kwargs = mock_OAuth2Session.call_args
|
|
160
|
+
self.assertEqual(kwargs['scope'], [])
|
|
161
|
+
|
|
162
|
+
self.assertEqual(http_response.url, redirect_url)
|
|
163
|
+
|
|
164
|
+
callback_redirects = (
|
|
165
|
+
CallbackRedirect.objects.filter(session_key=request.session.session_key)
|
|
166
|
+
)
|
|
167
|
+
self.assertEqual(len(callback_redirects), 1)
|
|
168
|
+
callback_redirect = callback_redirects.first()
|
|
169
|
+
self.assertEqual(callback_redirect.url, my_view_url)
|
|
170
|
+
self.assertEqual(callback_redirect.session_key, request.session.session_key)
|
|
171
|
+
self.assertEqual(callback_redirect.state, state)
|
|
172
|
+
|
|
173
|
+
@patch('esi.views.app_settings.ESI_SSO_CLIENT_ID', ESI_SSO_CLIENT_ID)
|
|
174
|
+
@patch('esi.views.app_settings.ESI_SSO_CALLBACK_URL', ESI_SSO_CALLBACK_URL)
|
|
175
|
+
@patch('esi.views.reverse')
|
|
176
|
+
def test_sso_redirect_return_to(self, mock_reverse):
|
|
177
|
+
mock_reverse.return_value = '/callback3/'
|
|
178
|
+
|
|
179
|
+
request = self.factory.get('https://www.example.com/callback2/')
|
|
180
|
+
request.user = self.user
|
|
181
|
+
middleware = SessionMiddleware(HttpResponse)
|
|
182
|
+
middleware.process_request(request)
|
|
183
|
+
request.session.save()
|
|
184
|
+
|
|
185
|
+
sso_redirect(request, return_to='callback3')
|
|
186
|
+
|
|
187
|
+
callback_redirects = (
|
|
188
|
+
CallbackRedirect.objects.filter(session_key=request.session.session_key)
|
|
189
|
+
)
|
|
190
|
+
self.assertEqual(len(callback_redirects), 1)
|
|
191
|
+
callback_redirect = callback_redirects.first()
|
|
192
|
+
self.assertEqual(callback_redirect.url, '/callback3/')
|
|
193
|
+
self.assertEqual(callback_redirect.session_key, request.session.session_key)
|
|
194
|
+
|
|
195
|
+
@patch('esi.views.app_settings.ESI_SSO_CLIENT_ID', ESI_SSO_CLIENT_ID)
|
|
196
|
+
@patch('esi.views.app_settings.ESI_SSO_CALLBACK_URL', ESI_SSO_CALLBACK_URL)
|
|
197
|
+
def test_sso_redirect_start_session(self):
|
|
198
|
+
request = self.factory.get('https://www.example.com/callback2/')
|
|
199
|
+
request.user = self.user
|
|
200
|
+
middleware = SessionMiddleware(HttpResponse)
|
|
201
|
+
middleware.process_request(request)
|
|
202
|
+
|
|
203
|
+
sso_redirect(request)
|
|
204
|
+
|
|
205
|
+
callback_redirects = (
|
|
206
|
+
CallbackRedirect.objects.filter(session_key=request.session.session_key)
|
|
207
|
+
)
|
|
208
|
+
self.assertEqual(len(callback_redirects), 1)
|
|
209
|
+
callback_redirect = callback_redirects.first()
|
|
210
|
+
self.assertEqual(callback_redirect.url, '/callback2/')
|
|
211
|
+
self.assertEqual(callback_redirect.session_key, request.session.session_key)
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
class TesReceiveCallbackView(TestCase):
|
|
215
|
+
|
|
216
|
+
def setUp(self):
|
|
217
|
+
self.user = User.objects.create_user(
|
|
218
|
+
'Bruce Wayne',
|
|
219
|
+
'abc@example.com',
|
|
220
|
+
'password'
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
self.token = _store_as_Token(
|
|
224
|
+
_generate_token(
|
|
225
|
+
character_id=99,
|
|
226
|
+
character_name=self.user.username,
|
|
227
|
+
scopes=['abc', 'xyz', '123']
|
|
228
|
+
),
|
|
229
|
+
self.user
|
|
230
|
+
)
|
|
231
|
+
self.factory = RequestFactory()
|
|
232
|
+
CallbackRedirect.objects.all().delete()
|
|
233
|
+
|
|
234
|
+
@patch('esi.managers.TokenManager.create_from_request')
|
|
235
|
+
def test_normal(self, mock_create_from_request):
|
|
236
|
+
mock_create_from_request.return_value = self.token
|
|
237
|
+
|
|
238
|
+
request = self.factory.get('https://www.example.com?code=abc&state=123')
|
|
239
|
+
request.user = self.user
|
|
240
|
+
middleware = SessionMiddleware(HttpResponse)
|
|
241
|
+
middleware.process_request(request)
|
|
242
|
+
request.session.save()
|
|
243
|
+
|
|
244
|
+
callback_redirect = CallbackRedirect.objects.create(
|
|
245
|
+
session_key=request.session.session_key,
|
|
246
|
+
state='123',
|
|
247
|
+
url=redirect_url
|
|
248
|
+
)
|
|
249
|
+
http_response = receive_callback(request)
|
|
250
|
+
callback_redirect.refresh_from_db()
|
|
251
|
+
self.assertIsInstance(http_response, HttpResponseRedirect)
|
|
252
|
+
self.assertEqual(http_response.url, redirect_url)
|
|
253
|
+
self.assertEqual(callback_redirect.token, self.token)
|
|
254
|
+
|
|
255
|
+
def test_missing_code(self):
|
|
256
|
+
request = self.factory.get('https://www.example.com?state=123')
|
|
257
|
+
request.user = self.user
|
|
258
|
+
middleware = SessionMiddleware(HttpResponse)
|
|
259
|
+
middleware.process_request(request)
|
|
260
|
+
request.session.save()
|
|
261
|
+
|
|
262
|
+
http_response = receive_callback(request)
|
|
263
|
+
self.assertIsInstance(http_response, HttpResponseBadRequest)
|
|
264
|
+
|
|
265
|
+
def test_missing_state(self):
|
|
266
|
+
request = self.factory.get('https://www.example.com?code=abc')
|
|
267
|
+
request.user = self.user
|
|
268
|
+
middleware = SessionMiddleware(HttpResponse)
|
|
269
|
+
middleware.process_request(request)
|
|
270
|
+
request.session.save()
|
|
271
|
+
|
|
272
|
+
http_response = receive_callback(request)
|
|
273
|
+
self.assertIsInstance(http_response, HttpResponseBadRequest)
|
|
274
|
+
|
|
275
|
+
def test_missing_callback(self):
|
|
276
|
+
request = self.factory.get('https://www.example.com?code=abc&state=123')
|
|
277
|
+
request.user = self.user
|
|
278
|
+
middleware = SessionMiddleware(HttpResponse)
|
|
279
|
+
middleware.process_request(request)
|
|
280
|
+
request.session.save()
|
|
281
|
+
|
|
282
|
+
with self.assertRaises(Http404):
|
|
283
|
+
receive_callback(request)
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
class TestSelectTokenView(TestCase):
|
|
287
|
+
def setUp(self):
|
|
288
|
+
self.user = User.objects.create_user(
|
|
289
|
+
'Bruce Wayne',
|
|
290
|
+
'abc@example.com',
|
|
291
|
+
'password'
|
|
292
|
+
)
|
|
293
|
+
self.token = _store_as_Token(
|
|
294
|
+
_generate_token(
|
|
295
|
+
character_id=99,
|
|
296
|
+
character_name=self.user.username,
|
|
297
|
+
scopes=['abc', 'xyz', '123']
|
|
298
|
+
),
|
|
299
|
+
self.user
|
|
300
|
+
)
|
|
301
|
+
self.factory = RequestFactory()
|
|
302
|
+
CallbackRedirect.objects.all().delete()
|
|
303
|
+
|
|
304
|
+
def test_should_render_select_page_on_first_call(self):
|
|
305
|
+
# given
|
|
306
|
+
request = self.factory.get('https://www.example.com/my_view/')
|
|
307
|
+
request.user = self.user
|
|
308
|
+
middleware = SessionMiddleware(HttpResponse)
|
|
309
|
+
middleware.process_request(request)
|
|
310
|
+
request.session.save()
|
|
311
|
+
# when
|
|
312
|
+
response = select_token(request, scopes='abc')
|
|
313
|
+
# then
|
|
314
|
+
self.assertIn("ESI Token Selection", response.content.decode("utf-8"))
|
|
315
|
+
|
|
316
|
+
def test_should_render_select_page_on_second_call(self):
|
|
317
|
+
# given
|
|
318
|
+
request = self.factory.get('https://www.example.com/my_view/')
|
|
319
|
+
request.user = self.user
|
|
320
|
+
middleware = SessionMiddleware(HttpResponse)
|
|
321
|
+
middleware.process_request(request)
|
|
322
|
+
request.session.save()
|
|
323
|
+
CallbackRedirect.objects.create(
|
|
324
|
+
session_key=request.session.session_key,
|
|
325
|
+
state='state123',
|
|
326
|
+
token=self.token
|
|
327
|
+
)
|
|
328
|
+
# when
|
|
329
|
+
response = select_token(request, scopes='abc', new=True)
|
|
330
|
+
# then
|
|
331
|
+
self.assertIn("ESI Token Selection", response.content.decode("utf-8"))
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# flake8: noqa
|
|
2
|
+
"""script for testing connection pool size with live requests to ESI
|
|
3
|
+
|
|
4
|
+
Run this script directly. Make sure to also set the environment variable
|
|
5
|
+
DJANGO_PROJECT_PATH and DJANGO_SETTINGS_MODULE to match your setup:
|
|
6
|
+
|
|
7
|
+
You can see the result in your main log file of your Django installation.
|
|
8
|
+
If your connection pool size is too small you will see the following warning in your
|
|
9
|
+
log file: "Connection pool is full, discarding connection: esi.evetech.net"
|
|
10
|
+
To pass this test successfully ESI_CONNECTION_POOL_MAXSIZE must equal to number
|
|
11
|
+
of MAX_WORKER (e.g. 20)
|
|
12
|
+
|
|
13
|
+
Example:
|
|
14
|
+
export DJANGO_PROJECT_PATH="/home/erik997/dev/python/django-dev/mysite"
|
|
15
|
+
export DJANGO_SETTINGS_MODULE="mysite.settings"
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
# start django project
|
|
20
|
+
import os
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
if not 'DJANGO_PROJECT_PATH' in os.environ:
|
|
24
|
+
print('DJANGO_PROJECT_PATH is not set')
|
|
25
|
+
exit(1)
|
|
26
|
+
|
|
27
|
+
if not 'DJANGO_SETTINGS_MODULE' in os.environ:
|
|
28
|
+
print('DJANGO_SETTINGS_MODULE is not set')
|
|
29
|
+
exit(1)
|
|
30
|
+
|
|
31
|
+
sys.path.insert(0, os.environ['DJANGO_PROJECT_PATH'])
|
|
32
|
+
import django
|
|
33
|
+
django.setup()
|
|
34
|
+
|
|
35
|
+
# normal imports
|
|
36
|
+
import concurrent.futures
|
|
37
|
+
import logging
|
|
38
|
+
|
|
39
|
+
from django.core.cache import cache
|
|
40
|
+
from esi.clients import EsiClientProvider
|
|
41
|
+
|
|
42
|
+
MAX_WORKER = 20
|
|
43
|
+
|
|
44
|
+
cache.clear()
|
|
45
|
+
esi = EsiClientProvider()
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger('__name__')
|
|
48
|
+
logger.level = logging.DEBUG
|
|
49
|
+
logger.propagate = True
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def thread_func(type_id):
|
|
53
|
+
logger.info('Fetching type with ID %d from ESI', type_id)
|
|
54
|
+
esi.client.Universe.get_universe_types_type_id(type_id=type_id).result()
|
|
55
|
+
|
|
56
|
+
def main():
|
|
57
|
+
print('Script started...')
|
|
58
|
+
entity_ids = esi.client.Universe.get_universe_types().results()[1000:2000]
|
|
59
|
+
logger.info('Start fetching %d types' , len(entity_ids))
|
|
60
|
+
|
|
61
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_WORKER) as executor:
|
|
62
|
+
executor.map(thread_func, entity_ids)
|
|
63
|
+
|
|
64
|
+
logger.info('Finished fetching %d types', len(entity_ids))
|
|
65
|
+
print('DONE')
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == '__main__':
|
|
69
|
+
main()
|
esi/urls.py
ADDED
esi/views.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from requests_oauthlib import OAuth2Session
|
|
4
|
+
|
|
5
|
+
from django.http.response import HttpResponseBadRequest
|
|
6
|
+
from django.shortcuts import get_object_or_404, redirect, render
|
|
7
|
+
from django.urls import reverse
|
|
8
|
+
|
|
9
|
+
from esi import app_settings
|
|
10
|
+
from esi.models import CallbackRedirect, Token
|
|
11
|
+
|
|
12
|
+
from .decorators import tokens_required
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def sso_redirect(request, scopes=None, return_to=None):
|
|
18
|
+
"""
|
|
19
|
+
Generates a :model:`esi.CallbackRedirect` for the specified request.
|
|
20
|
+
Redirects to EVE for login.
|
|
21
|
+
Accepts a view or URL name as a redirect after SSO.
|
|
22
|
+
"""
|
|
23
|
+
logger.debug(
|
|
24
|
+
"Initiating redirect of %s session %s",
|
|
25
|
+
request.user,
|
|
26
|
+
request.session.session_key[:5] if request.session.session_key else '[no key]'
|
|
27
|
+
)
|
|
28
|
+
if scopes is None:
|
|
29
|
+
scopes = list()
|
|
30
|
+
elif isinstance(scopes, str):
|
|
31
|
+
scopes = list([scopes])
|
|
32
|
+
|
|
33
|
+
# ensure only one callback redirect model per session
|
|
34
|
+
if request.session.session_key:
|
|
35
|
+
CallbackRedirect.objects\
|
|
36
|
+
.filter(session_key=request.session.session_key).delete()
|
|
37
|
+
|
|
38
|
+
# ensure we have a session
|
|
39
|
+
if not request.session.exists(request.session.session_key):
|
|
40
|
+
logger.debug("Creating new session before redirect.")
|
|
41
|
+
request.session.create()
|
|
42
|
+
|
|
43
|
+
if return_to:
|
|
44
|
+
url = reverse(return_to)
|
|
45
|
+
else:
|
|
46
|
+
url = request.get_full_path()
|
|
47
|
+
|
|
48
|
+
oauth = OAuth2Session(
|
|
49
|
+
app_settings.ESI_SSO_CLIENT_ID,
|
|
50
|
+
redirect_uri=app_settings.ESI_SSO_CALLBACK_URL,
|
|
51
|
+
scope=scopes
|
|
52
|
+
)
|
|
53
|
+
redirect_url, state = oauth.authorization_url(app_settings.ESI_OAUTH_LOGIN_URL)
|
|
54
|
+
|
|
55
|
+
CallbackRedirect.objects.create(
|
|
56
|
+
session_key=request.session.session_key, state=state, url=url
|
|
57
|
+
)
|
|
58
|
+
logger.debug(
|
|
59
|
+
"Redirecting %s session %s to SSO. Callback will be redirected to %s",
|
|
60
|
+
request.user,
|
|
61
|
+
request.session.session_key[:5],
|
|
62
|
+
url
|
|
63
|
+
)
|
|
64
|
+
return redirect(redirect_url)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def receive_callback(request):
|
|
68
|
+
"""
|
|
69
|
+
Parses SSO callback, validates, retrieves :model:`esi.Token`,
|
|
70
|
+
and internally redirects to the target url.
|
|
71
|
+
"""
|
|
72
|
+
logger.debug(
|
|
73
|
+
"Received callback for %s session %s",
|
|
74
|
+
request.user,
|
|
75
|
+
request.session.session_key[:5]
|
|
76
|
+
)
|
|
77
|
+
# make sure request has required parameters
|
|
78
|
+
code = request.GET.get('code', None)
|
|
79
|
+
state = request.GET.get('state', None)
|
|
80
|
+
if not code or not state:
|
|
81
|
+
logger.warning("Missing parameters for code exchange.")
|
|
82
|
+
return HttpResponseBadRequest()
|
|
83
|
+
|
|
84
|
+
callback = get_object_or_404(
|
|
85
|
+
CallbackRedirect, state=state, session_key=request.session.session_key
|
|
86
|
+
)
|
|
87
|
+
token = Token.objects.create_from_request(request)
|
|
88
|
+
callback.token = token
|
|
89
|
+
callback.save()
|
|
90
|
+
logger.debug(
|
|
91
|
+
"Processed callback for %s session %s. Redirecting to %s",
|
|
92
|
+
request.user,
|
|
93
|
+
request.session.session_key[:5],
|
|
94
|
+
callback.url
|
|
95
|
+
)
|
|
96
|
+
return redirect(callback.url)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def select_token(request, scopes='', new=False):
|
|
100
|
+
"""
|
|
101
|
+
Presents the user with a selection of applicable tokens for the requested view.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
@tokens_required(scopes=scopes, new=new)
|
|
105
|
+
def _token_list(r, tokens):
|
|
106
|
+
# Single Scope as string will break the Parser in the template
|
|
107
|
+
if isinstance(scopes, str):
|
|
108
|
+
scopes_output = [scopes]
|
|
109
|
+
else:
|
|
110
|
+
scopes_output = scopes
|
|
111
|
+
|
|
112
|
+
# fake distint on character_name to not show duplicates
|
|
113
|
+
# MySQL doesn't support distint on field.
|
|
114
|
+
token_output = []
|
|
115
|
+
_characters = set()
|
|
116
|
+
for t in tokens:
|
|
117
|
+
if t.character_name in _characters:
|
|
118
|
+
continue
|
|
119
|
+
token_output.append(t)
|
|
120
|
+
_characters.add(t.character_name)
|
|
121
|
+
|
|
122
|
+
context = {
|
|
123
|
+
'tokens': token_output,
|
|
124
|
+
'scopes': scopes_output
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return render(r, 'esi/select_token.html', context=context)
|
|
128
|
+
|
|
129
|
+
return _token_list(request)
|