Add token management and restrict logins to mains only

This commit is contained in:
Aaron Kable 2022-10-12 17:49:28 +08:00
parent dd1313a2a9
commit f17c94a9e1
5 changed files with 152 additions and 5 deletions

View File

@ -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}')
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,11 +64,18 @@ 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
# set this as their main by default as they have none
user.profile.main_character = co.character
user.profile.save()
return user

View File

@ -0,0 +1,61 @@
{% extends "allianceauth/base.html" %}
{% load i18n %}
{% block page_title %}{% translate "Dashboard" %}{% endblock %}
{% block content %}
<h1 class="page-header text-center">{% translate "Token Management" %}</h1>
<div class="col-sm-12">
<table class="table table-aa" id="table_tokens" style="width:100%">
<thead>
<tr>
<th>Scopes</th>
<th class="text-right">Actions</th>
<th>Character</th>
</tr>
</thead>
<tbody>
{% for t in tokens %}
<tr>
<td styl="white-space:initial;">{% for s in t.scopes.all %}<span class="label label-default">{{s.name}}</span> {% endfor %}</td>
<td class="text-right"><a href="{% url 'authentication:token_revoke' t.id %}" class="btn btn-danger"><i class="fas fa-trash"></i></a> <a href="{% url 'authentication:token_refresh' t.id %}" class="btn btn-success"><i class="fas fa-sync-alt"></i></a></td>
<td>{{t.character_name}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% 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('<tr class="info"><td colspan="3">' + group + '</td></tr>');
last = group;
}
});
}
});
});
{% endblock %}

View File

@ -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,

View File

@ -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/<int:token_id>',
views.token_revoke,
name='token_revoke'
),
path(
'account/tokens/refresh/<int:token_id>',
views.token_refresh,
name='token_refresh'
),
path('dashboard/', views.dashboard, name='dashboard'),
]

View File

@ -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)