From f17c94a9e11f4ed9dca55769a4efa67b9709a61b Mon Sep 17 00:00:00 2001 From: Aaron Kable Date: Wed, 12 Oct 2022 17:49:28 +0800 Subject: [PATCH 1/9] Add token management and restrict logins to mains only --- allianceauth/authentication/backends.py | 24 ++++++-- .../templates/authentication/tokens.html | 61 +++++++++++++++++++ .../authentication/tests/test_backend.py | 17 ++++++ allianceauth/authentication/urls.py | 15 +++++ allianceauth/authentication/views.py | 40 ++++++++++++ 5 files changed, 152 insertions(+), 5 deletions(-) create mode 100644 allianceauth/authentication/templates/authentication/tokens.html diff --git a/allianceauth/authentication/backends.py b/allianceauth/authentication/backends.py index 526950d9..94ac066b 100644 --- a/allianceauth/authentication/backends.py +++ b/allianceauth/authentication/backends.py @@ -2,6 +2,7 @@ import logging from django.contrib.auth.backends import ModelBackend from django.contrib.auth.models import User, Permission +from django.contrib import messages from .models import UserProfile, CharacterOwnership, OwnershipRecord @@ -37,7 +38,13 @@ class StateBackend(ModelBackend): ownership = CharacterOwnership.objects.get(character__character_id=token.character_id) if ownership.owner_hash == token.character_owner_hash: logger.debug(f'Authenticating {ownership.user} by ownership of character {token.character_name}') - return ownership.user + if ownership.user.profile.main_character: + if ownership.user.profile.main_character.character_id == token.character_id: + return ownership.user + else: ## this is an alt, enforce main only. + if request: + messages.error("Unable to authenticate with this Character, Please log in with the main character associated with this account.") + return None else: logger.debug(f'{token.character_name} has changed ownership. Creating new user account.') ownership.delete() @@ -57,13 +64,20 @@ class StateBackend(ModelBackend): if records.exists(): # we've seen this character owner before. Re-attach to their old user account user = records[0].user + if user.profile.main_character: + if ownership.user.profile.main_character.character_id != token.character_id: + ## this is an alt, enforce main only due to trust issues in SSO. + if request: + messages.error("Unable to authenticate with this Character, Please log in with the main character associated with this account. Then add this character from the dashboard.") + return None + token.user = user co = CharacterOwnership.objects.create_by_token(token) logger.debug(f'Authenticating {user} by matching owner hash record of character {co.character}') - if not user.profile.main_character: - # set this as their main by default if they have none - user.profile.main_character = co.character - user.profile.save() + + # set this as their main by default as they have none + user.profile.main_character = co.character + user.profile.save() return user logger.debug(f'Unable to authenticate character {token.character_name}. Creating new user.') return self.create_user(token) diff --git a/allianceauth/authentication/templates/authentication/tokens.html b/allianceauth/authentication/templates/authentication/tokens.html new file mode 100644 index 00000000..e90be72b --- /dev/null +++ b/allianceauth/authentication/templates/authentication/tokens.html @@ -0,0 +1,61 @@ +{% extends "allianceauth/base.html" %} +{% load i18n %} + +{% block page_title %}{% translate "Dashboard" %}{% endblock %} + +{% block content %} +

{% translate "Token Management" %}

+
+ + + + + + + + + + + {% for t in tokens %} + + + + + + {% endfor %} + +
ScopesActionsCharacter
{% for s in t.scopes.all %}{{s.name}} {% endfor %} {{t.character_name}}
+
+{% endblock %} + +{% block extra_javascript %} + {% include 'bundles/datatables-js.html' %} +{% endblock %} + +{% block extra_css %} + {% include 'bundles/datatables-css.html' %} +{% endblock %} + +{% block extra_script %} + $(document).ready(function(){ + let grp = 2; + var table = $('#table_tokens').DataTable({ + "columnDefs": [{ orderable: false, targets: [0,1] },{ "visible": false, "targets": grp }], + "order": [[grp, 'asc']], + "drawCallback": function (settings) { + var api = this.api(); + var rows = api.rows({ page: 'current' }).nodes(); + var last = null; + api.column(grp, { page: 'current' }) + .data() + .each(function (group, i) { + if (last !== group) { + $(rows).eq(i).before('' + group + ''); + last = group; + } + }); + } + }); + + }); +{% endblock %} diff --git a/allianceauth/authentication/tests/test_backend.py b/allianceauth/authentication/tests/test_backend.py index ed984521..92936355 100644 --- a/allianceauth/authentication/tests/test_backend.py +++ b/allianceauth/authentication/tests/test_backend.py @@ -116,10 +116,17 @@ class TestAuthenticate(TestCase): user = StateBackend().authenticate(token=t) self.assertEqual(user, self.user) + """ Alt Login disabled def test_authenticate_alt_character(self): t = Token(character_id=self.alt_character.character_id, character_owner_hash='2') user = StateBackend().authenticate(token=t) self.assertEqual(user, self.user) + """ + + def test_authenticate_alt_character_fail(self): + t = Token(character_id=self.alt_character.character_id, character_owner_hash='2') + user = StateBackend().authenticate(token=t) + self.assertEqual(user, None) def test_authenticate_unclaimed_character(self): t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3') @@ -128,6 +135,7 @@ class TestAuthenticate(TestCase): self.assertEqual(user.username, 'Unclaimed_Character') self.assertEqual(user.profile.main_character, self.unclaimed_character) + """ Alt Login disabled def test_authenticate_character_record(self): t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4') OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4') @@ -135,6 +143,15 @@ class TestAuthenticate(TestCase): self.assertEqual(user, self.old_user) self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists()) self.assertTrue(user.profile.main_character) + """ + + def test_authenticate_character_record_fails(self): + t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4') + OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4') + user = StateBackend().authenticate(token=t) + self.assertEqual(user, self.old_user) + self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists()) + self.assertTrue(user.profile.main_character) def test_iterate_username(self): t = Token(character_id=self.unclaimed_character.character_id, diff --git a/allianceauth/authentication/urls.py b/allianceauth/authentication/urls.py index adef3e5f..03a0da18 100644 --- a/allianceauth/authentication/urls.py +++ b/allianceauth/authentication/urls.py @@ -22,5 +22,20 @@ urlpatterns = [ views.add_character, name='add_character' ), + path( + 'account/tokens/manage/', + views.token_management, + name='token_management' + ), + path( + 'account/tokens/revoke/', + views.token_revoke, + name='token_revoke' + ), + path( + 'account/tokens/refresh/', + views.token_refresh, + name='token_refresh' + ), path('dashboard/', views.dashboard, name='dashboard'), ] diff --git a/allianceauth/authentication/views.py b/allianceauth/authentication/views.py index 779f9a1c..b8c18a64 100644 --- a/allianceauth/authentication/views.py +++ b/allianceauth/authentication/views.py @@ -1,4 +1,6 @@ +from glob import escape import logging +from symbol import except_clause from django.conf import settings from django.contrib import messages @@ -61,6 +63,44 @@ def dashboard(request): } return render(request, 'authentication/dashboard.html', context) +@login_required +def token_management(request): + tokens = request.user.token_set.all() + + context = { + 'tokens': tokens + } + return render(request, 'authentication/tokens.html', context) + +@login_required +def token_revoke(request, token_id=None): + try: + token = Token.objects.get(id=token_id) + if request.user == token.user: + token.delete() + messages.success(request, "Token Deleted.") + else: + messages.error(request, "This token does not belong to you.") + except Token.DoesNotExist: + messages.warning(request, "Token does not exist") + return redirect('authentication:token_management') + +@login_required +def token_refresh(request, token_id=None): + try: + token = Token.objects.get(id=token_id) + if request.user == token.user: + try: + token.refresh() + messages.success(request, "Token refreshed.") + except Exception as e: + messages.warning(request, f"Failed to refresh token. {e}") + else: + messages.error(request, "This token does not belong to you.") + except Token.DoesNotExist: + messages.warning(request, "Token does not exist") + return redirect('authentication:token_management') + @login_required @token_required(scopes=settings.LOGIN_TOKEN_SCOPES) From 36ff0af99386b72b11ec17ddf7ab577d498e4f21 Mon Sep 17 00:00:00 2001 From: Aaron Kable Date: Wed, 12 Oct 2022 17:49:28 +0800 Subject: [PATCH 2/9] Add token management and restrict logins to mains only --- allianceauth/authentication/backends.py | 24 ++++++-- .../templates/authentication/tokens.html | 61 +++++++++++++++++++ .../authentication/tests/test_backend.py | 17 ++++++ allianceauth/authentication/urls.py | 15 +++++ allianceauth/authentication/views.py | 38 ++++++++++++ 5 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 allianceauth/authentication/templates/authentication/tokens.html diff --git a/allianceauth/authentication/backends.py b/allianceauth/authentication/backends.py index 526950d9..94ac066b 100644 --- a/allianceauth/authentication/backends.py +++ b/allianceauth/authentication/backends.py @@ -2,6 +2,7 @@ import logging from django.contrib.auth.backends import ModelBackend from django.contrib.auth.models import User, Permission +from django.contrib import messages from .models import UserProfile, CharacterOwnership, OwnershipRecord @@ -37,7 +38,13 @@ class StateBackend(ModelBackend): ownership = CharacterOwnership.objects.get(character__character_id=token.character_id) if ownership.owner_hash == token.character_owner_hash: logger.debug(f'Authenticating {ownership.user} by ownership of character {token.character_name}') - return ownership.user + if ownership.user.profile.main_character: + if ownership.user.profile.main_character.character_id == token.character_id: + return ownership.user + else: ## this is an alt, enforce main only. + if request: + messages.error("Unable to authenticate with this Character, Please log in with the main character associated with this account.") + return None else: logger.debug(f'{token.character_name} has changed ownership. Creating new user account.') ownership.delete() @@ -57,13 +64,20 @@ class StateBackend(ModelBackend): if records.exists(): # we've seen this character owner before. Re-attach to their old user account user = records[0].user + if user.profile.main_character: + if ownership.user.profile.main_character.character_id != token.character_id: + ## this is an alt, enforce main only due to trust issues in SSO. + if request: + messages.error("Unable to authenticate with this Character, Please log in with the main character associated with this account. Then add this character from the dashboard.") + return None + token.user = user co = CharacterOwnership.objects.create_by_token(token) logger.debug(f'Authenticating {user} by matching owner hash record of character {co.character}') - if not user.profile.main_character: - # set this as their main by default if they have none - user.profile.main_character = co.character - user.profile.save() + + # set this as their main by default as they have none + user.profile.main_character = co.character + user.profile.save() return user logger.debug(f'Unable to authenticate character {token.character_name}. Creating new user.') return self.create_user(token) diff --git a/allianceauth/authentication/templates/authentication/tokens.html b/allianceauth/authentication/templates/authentication/tokens.html new file mode 100644 index 00000000..e90be72b --- /dev/null +++ b/allianceauth/authentication/templates/authentication/tokens.html @@ -0,0 +1,61 @@ +{% extends "allianceauth/base.html" %} +{% load i18n %} + +{% block page_title %}{% translate "Dashboard" %}{% endblock %} + +{% block content %} +

{% translate "Token Management" %}

+
+ + + + + + + + + + + {% for t in tokens %} + + + + + + {% endfor %} + +
ScopesActionsCharacter
{% for s in t.scopes.all %}{{s.name}} {% endfor %} {{t.character_name}}
+
+{% endblock %} + +{% block extra_javascript %} + {% include 'bundles/datatables-js.html' %} +{% endblock %} + +{% block extra_css %} + {% include 'bundles/datatables-css.html' %} +{% endblock %} + +{% block extra_script %} + $(document).ready(function(){ + let grp = 2; + var table = $('#table_tokens').DataTable({ + "columnDefs": [{ orderable: false, targets: [0,1] },{ "visible": false, "targets": grp }], + "order": [[grp, 'asc']], + "drawCallback": function (settings) { + var api = this.api(); + var rows = api.rows({ page: 'current' }).nodes(); + var last = null; + api.column(grp, { page: 'current' }) + .data() + .each(function (group, i) { + if (last !== group) { + $(rows).eq(i).before('' + group + ''); + last = group; + } + }); + } + }); + + }); +{% endblock %} diff --git a/allianceauth/authentication/tests/test_backend.py b/allianceauth/authentication/tests/test_backend.py index ed984521..92936355 100644 --- a/allianceauth/authentication/tests/test_backend.py +++ b/allianceauth/authentication/tests/test_backend.py @@ -116,10 +116,17 @@ class TestAuthenticate(TestCase): user = StateBackend().authenticate(token=t) self.assertEqual(user, self.user) + """ Alt Login disabled def test_authenticate_alt_character(self): t = Token(character_id=self.alt_character.character_id, character_owner_hash='2') user = StateBackend().authenticate(token=t) self.assertEqual(user, self.user) + """ + + def test_authenticate_alt_character_fail(self): + t = Token(character_id=self.alt_character.character_id, character_owner_hash='2') + user = StateBackend().authenticate(token=t) + self.assertEqual(user, None) def test_authenticate_unclaimed_character(self): t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='3') @@ -128,6 +135,7 @@ class TestAuthenticate(TestCase): self.assertEqual(user.username, 'Unclaimed_Character') self.assertEqual(user.profile.main_character, self.unclaimed_character) + """ Alt Login disabled def test_authenticate_character_record(self): t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4') OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4') @@ -135,6 +143,15 @@ class TestAuthenticate(TestCase): self.assertEqual(user, self.old_user) self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists()) self.assertTrue(user.profile.main_character) + """ + + def test_authenticate_character_record_fails(self): + t = Token(character_id=self.unclaimed_character.character_id, character_name=self.unclaimed_character.character_name, character_owner_hash='4') + OwnershipRecord.objects.create(user=self.old_user, character=self.unclaimed_character, owner_hash='4') + user = StateBackend().authenticate(token=t) + self.assertEqual(user, self.old_user) + self.assertTrue(CharacterOwnership.objects.filter(owner_hash='4', user=self.old_user).exists()) + self.assertTrue(user.profile.main_character) def test_iterate_username(self): t = Token(character_id=self.unclaimed_character.character_id, diff --git a/allianceauth/authentication/urls.py b/allianceauth/authentication/urls.py index adef3e5f..03a0da18 100644 --- a/allianceauth/authentication/urls.py +++ b/allianceauth/authentication/urls.py @@ -22,5 +22,20 @@ urlpatterns = [ views.add_character, name='add_character' ), + path( + 'account/tokens/manage/', + views.token_management, + name='token_management' + ), + path( + 'account/tokens/revoke/', + views.token_revoke, + name='token_revoke' + ), + path( + 'account/tokens/refresh/', + views.token_refresh, + name='token_refresh' + ), path('dashboard/', views.dashboard, name='dashboard'), ] diff --git a/allianceauth/authentication/views.py b/allianceauth/authentication/views.py index 779f9a1c..6c008a06 100644 --- a/allianceauth/authentication/views.py +++ b/allianceauth/authentication/views.py @@ -61,6 +61,44 @@ def dashboard(request): } return render(request, 'authentication/dashboard.html', context) +@login_required +def token_management(request): + tokens = request.user.token_set.all() + + context = { + 'tokens': tokens + } + return render(request, 'authentication/tokens.html', context) + +@login_required +def token_revoke(request, token_id=None): + try: + token = Token.objects.get(id=token_id) + if request.user == token.user: + token.delete() + messages.success(request, "Token Deleted.") + else: + messages.error(request, "This token does not belong to you.") + except Token.DoesNotExist: + messages.warning(request, "Token does not exist") + return redirect('authentication:token_management') + +@login_required +def token_refresh(request, token_id=None): + try: + token = Token.objects.get(id=token_id) + if request.user == token.user: + try: + token.refresh() + messages.success(request, "Token refreshed.") + except Exception as e: + messages.warning(request, f"Failed to refresh token. {e}") + else: + messages.error(request, "This token does not belong to you.") + except Token.DoesNotExist: + messages.warning(request, "Token does not exist") + return redirect('authentication:token_management') + @login_required @token_required(scopes=settings.LOGIN_TOKEN_SCOPES) From cc60b26f5ac9ced250820d758929815335c0e1df Mon Sep 17 00:00:00 2001 From: Aaron Kable Date: Wed, 12 Oct 2022 17:57:12 +0800 Subject: [PATCH 3/9] add token management link to user dropdown --- .../templates/allianceauth/top-menu-user-dropdown.html | 3 +++ 1 file changed, 3 insertions(+) diff --git a/allianceauth/templates/allianceauth/top-menu-user-dropdown.html b/allianceauth/templates/allianceauth/top-menu-user-dropdown.html index 61c6ee39..7ae46150 100644 --- a/allianceauth/templates/allianceauth/top-menu-user-dropdown.html +++ b/allianceauth/templates/allianceauth/top-menu-user-dropdown.html @@ -53,6 +53,9 @@ {% if user.is_authenticated %} +
  • {% translate "Token Management" %}
  • + +
  • {% translate "Logout" %}
  • {% else %}
  • {% translate "Login" %}
  • From 6b8341ab5a6b892dabe40aa4c28c047b9b41fe19 Mon Sep 17 00:00:00 2001 From: Ariel Rin Date: Wed, 12 Oct 2022 20:21:27 +1000 Subject: [PATCH 4/9] Add FA icon to user dropdown --- .../templates/allianceauth/top-menu-user-dropdown.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/allianceauth/templates/allianceauth/top-menu-user-dropdown.html b/allianceauth/templates/allianceauth/top-menu-user-dropdown.html index 7ae46150..2c9760be 100644 --- a/allianceauth/templates/allianceauth/top-menu-user-dropdown.html +++ b/allianceauth/templates/allianceauth/top-menu-user-dropdown.html @@ -53,7 +53,12 @@ {% if user.is_authenticated %} -
  • {% translate "Token Management" %}
  • +
  • + + + {% translate "Token Management" %} + +
  • {% translate "Logout" %}
  • From 90ad7790e1712431a36b2370acb1a3257ca53544 Mon Sep 17 00:00:00 2001 From: Ariel Rin Date: Wed, 12 Oct 2022 20:22:20 +1000 Subject: [PATCH 5/9] rename revoke to delete to be clearer --- .../authentication/templates/authentication/tokens.html | 8 ++++---- allianceauth/authentication/urls.py | 6 +++--- allianceauth/authentication/views.py | 4 +--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/allianceauth/authentication/templates/authentication/tokens.html b/allianceauth/authentication/templates/authentication/tokens.html index e90be72b..afb81828 100644 --- a/allianceauth/authentication/templates/authentication/tokens.html +++ b/allianceauth/authentication/templates/authentication/tokens.html @@ -9,9 +9,9 @@ - - - + + + @@ -19,7 +19,7 @@ {% for t in tokens %} - + {% endfor %} diff --git a/allianceauth/authentication/urls.py b/allianceauth/authentication/urls.py index 03a0da18..6b1e9a5e 100644 --- a/allianceauth/authentication/urls.py +++ b/allianceauth/authentication/urls.py @@ -28,9 +28,9 @@ urlpatterns = [ name='token_management' ), path( - 'account/tokens/revoke/', - views.token_revoke, - name='token_revoke' + 'account/tokens/delete/', + views.token_delete, + name='token_delete' ), path( 'account/tokens/refresh/', diff --git a/allianceauth/authentication/views.py b/allianceauth/authentication/views.py index b8c18a64..74e05c28 100644 --- a/allianceauth/authentication/views.py +++ b/allianceauth/authentication/views.py @@ -1,6 +1,4 @@ -from glob import escape import logging -from symbol import except_clause from django.conf import settings from django.contrib import messages @@ -73,7 +71,7 @@ def token_management(request): return render(request, 'authentication/tokens.html', context) @login_required -def token_revoke(request, token_id=None): +def token_delete(request, token_id=None): try: token = Token.objects.get(id=token_id) if request.user == token.user: From 13a05606fb6b63b0403a37b5564f7279a4a38dc1 Mon Sep 17 00:00:00 2001 From: Ariel Rin Date: Wed, 12 Oct 2022 20:29:42 +1000 Subject: [PATCH 6/9] Scopes Typo --- .../authentication/templates/authentication/tokens.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allianceauth/authentication/templates/authentication/tokens.html b/allianceauth/authentication/templates/authentication/tokens.html index afb81828..5b234d00 100644 --- a/allianceauth/authentication/templates/authentication/tokens.html +++ b/allianceauth/authentication/templates/authentication/tokens.html @@ -9,7 +9,7 @@
    ScopesActionsCharacter{% translate "Scoes" %}{% translate "Actions" %}{% translate "Character" %}
    {% for s in t.scopes.all %}{{s.name}} {% endfor %} {{t.character_name}}
    - + From 36dedfcbd2ba14b8dac0f7f21565a33b2408b461 Mon Sep 17 00:00:00 2001 From: Ariel Rin Date: Wed, 12 Oct 2022 20:51:36 +1000 Subject: [PATCH 7/9] add disclaimer --- allianceauth/authentication/templates/authentication/tokens.html | 1 + 1 file changed, 1 insertion(+) diff --git a/allianceauth/authentication/templates/authentication/tokens.html b/allianceauth/authentication/templates/authentication/tokens.html index 5b234d00..92468e2f 100644 --- a/allianceauth/authentication/templates/authentication/tokens.html +++ b/allianceauth/authentication/templates/authentication/tokens.html @@ -25,6 +25,7 @@ {% endfor %}
    {% translate "Scoes" %}{% translate "Scopes" %} {% translate "Actions" %} {% translate "Character" %}
    + {% translate "This page is a best attempt, but backups or database logs can still contain your tokens. Always revoke tokens on https://community.eveonline.com/support/third-party-applications/ where possible."|urlize %} {% endblock %} From 1f781c50371e314e054a8c28f75b7aff4ebb23f2 Mon Sep 17 00:00:00 2001 From: Ariel Rin Date: Wed, 12 Oct 2022 20:52:59 +1000 Subject: [PATCH 8/9] datatables statesave --- .../authentication/templates/authentication/tokens.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/allianceauth/authentication/templates/authentication/tokens.html b/allianceauth/authentication/templates/authentication/tokens.html index 92468e2f..7670d433 100644 --- a/allianceauth/authentication/templates/authentication/tokens.html +++ b/allianceauth/authentication/templates/authentication/tokens.html @@ -55,8 +55,8 @@ last = group; } }); - } + }, + "stateSave": true, }); - }); {% endblock %} From 0f2f5ea0ba6668e69344f2c295855aaec7eb41c6 Mon Sep 17 00:00:00 2001 From: Ariel Rin Date: Fri, 14 Oct 2022 20:21:07 +1000 Subject: [PATCH 9/9] nowrap to stop buttons moving around --- .../authentication/templates/authentication/tokens.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/allianceauth/authentication/templates/authentication/tokens.html b/allianceauth/authentication/templates/authentication/tokens.html index 7670d433..11bb91ed 100644 --- a/allianceauth/authentication/templates/authentication/tokens.html +++ b/allianceauth/authentication/templates/authentication/tokens.html @@ -19,7 +19,7 @@ {% for t in tokens %} {% for s in t.scopes.all %}{{s.name}} {% endfor %} - + {{t.character_name}} {% endfor %}