From f17c94a9e11f4ed9dca55769a4efa67b9709a61b Mon Sep 17 00:00:00 2001 From: Aaron Kable Date: Wed, 12 Oct 2022 17:49:28 +0800 Subject: [PATCH] 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)