core: rename nonce to token

This commit is contained in:
Jens Langhammer 2020-05-16 16:11:53 +02:00
parent 406f69080b
commit 227966e727
12 changed files with 108 additions and 56 deletions

View file

@ -16,7 +16,7 @@ from guardian.mixins import (
)
from passbook.admin.forms.users import UserForm
from passbook.core.models import Nonce, User
from passbook.core.models import Token, User
from passbook.lib.views import CreateAssignPermView
@ -92,12 +92,12 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
permission_required = "passbook_core.reset_user_password"
def get(self, request, *args, **kwargs):
"""Create nonce for user and return link"""
"""Create token for user and return link"""
super().get(request, *args, **kwargs)
# TODO: create plan for user, get token
nonce = Nonce.objects.create(user=self.object)
token = Token.objects.create(user=self.object)
link = request.build_absolute_uri(
reverse("passbook_flows:default-recovery", kwargs={"nonce": nonce.uuid})
reverse("passbook_flows:default-recovery", kwargs={"token": token.uuid})
)
messages.success(
request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link})

View file

@ -284,7 +284,7 @@ class Migration(migrations.Migration):
(
"expires",
models.DateTimeField(
default=passbook.core.models.default_nonce_duration
default=passbook.core.models.default_token_duration
),
),
("expiring", models.BooleanField(default=True)),

View file

@ -0,0 +1,52 @@
# Generated by Django 3.0.5 on 2020-05-16 14:07
import uuid
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
import passbook.core.models
class Migration(migrations.Migration):
dependencies = [
("passbook_core", "0014_delete_invitation"),
]
operations = [
migrations.CreateModel(
name="Token",
fields=[
(
"uuid",
models.UUIDField(
default=uuid.uuid4,
editable=False,
primary_key=True,
serialize=False,
),
),
(
"expires",
models.DateTimeField(
default=passbook.core.models.default_token_duration
),
),
("expiring", models.BooleanField(default=True)),
("description", models.TextField(blank=True, default="")),
(
"user",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="+",
to=settings.AUTH_USER_MODEL,
),
),
],
options={"verbose_name": "Token", "verbose_name_plural": "Tokens",},
bases=(models.Model,),
),
migrations.DeleteModel(name="Nonce",),
]

View file

@ -29,8 +29,8 @@ LOGGER = get_logger()
NATIVE_ENVIRONMENT = NativeEnvironment()
def default_nonce_duration():
"""Default duration a Nonce is valid"""
def default_token_duration():
"""Default duration a Token is valid"""
return now() + timedelta(minutes=30)
@ -195,26 +195,26 @@ class Policy(ExportModelOperationsMixin("policy"), UUIDModel, CreatedUpdatedMode
raise PolicyException()
class Nonce(ExportModelOperationsMixin("nonce"), UUIDModel):
class Token(ExportModelOperationsMixin("token"), UUIDModel):
"""One-time link for password resets/sign-up-confirmations"""
expires = models.DateTimeField(default=default_nonce_duration)
user = models.ForeignKey("User", on_delete=models.CASCADE)
expires = models.DateTimeField(default=default_token_duration)
user = models.ForeignKey("User", on_delete=models.CASCADE, related_name="+")
expiring = models.BooleanField(default=True)
description = models.TextField(default="", blank=True)
@property
def is_expired(self) -> bool:
"""Check if nonce is expired yet."""
"""Check if token is expired yet."""
return now() > self.expires
def __str__(self):
return f"Nonce f{self.uuid.hex} {self.description} (expires={self.expires})"
return f"Token f{self.uuid.hex} {self.description} (expires={self.expires})"
class Meta:
verbose_name = _("Nonce")
verbose_name_plural = _("Nonces")
verbose_name = _("Token")
verbose_name_plural = _("Tokens")
class PropertyMapping(UUIDModel):

View file

@ -2,14 +2,14 @@
from django.utils.timezone import now
from structlog import get_logger
from passbook.core.models import Nonce
from passbook.core.models import Token
from passbook.root.celery import CELERY_APP
LOGGER = get_logger()
@CELERY_APP.task()
def clean_nonces():
"""Remove expired nonces"""
amount, _ = Nonce.objects.filter(expires__lt=now(), expiring=True).delete()
LOGGER.debug("Deleted expired nonces", amount=amount)
def clean_tokens():
"""Remove expired tokens"""
amount, _ = Token.objects.filter(expires__lt=now(), expiring=True).delete()
LOGGER.debug("Deleted expired tokens", amount=amount)

View file

@ -8,14 +8,14 @@ from django.utils.timezone import now
from django.utils.translation import gettext as _
from structlog import get_logger
from passbook.core.models import Nonce, User
from passbook.core.models import Token, User
from passbook.lib.config import CONFIG
LOGGER = get_logger()
class Command(BaseCommand):
"""Create Nonce used to recover access"""
"""Create Token used to recover access"""
help = _("Create a Key which can be used to restore access to passbook.")
@ -30,22 +30,22 @@ class Command(BaseCommand):
"user", action="store", help="Which user the Token gives access to."
)
def get_url(self, nonce: Nonce) -> str:
def get_url(self, token: Token) -> str:
"""Get full recovery link"""
path = reverse("passbook_recovery:use-nonce", kwargs={"uuid": str(nonce.uuid)})
path = reverse("passbook_recovery:use-token", kwargs={"uuid": str(token.uuid)})
return f"https://{CONFIG.y('domain')}{path}"
def handle(self, *args, **options):
"""Create Nonce used to recover access"""
"""Create Token used to recover access"""
duration = int(options.get("duration", 1))
delta = timedelta(days=duration * 365.2425)
_now = now()
expiry = _now + delta
user = User.objects.get(username=options.get("user"))
nonce = Nonce.objects.create(
token = Token.objects.create(
expires=expiry,
user=user,
description=f"Recovery Nonce generated by {getuser()} on {_now}",
description=f"Recovery Token generated by {getuser()} on {_now}",
)
self.stdout.write(
(
@ -53,4 +53,4 @@ class Command(BaseCommand):
f" anyone to access passbook as {user}."
)
)
self.stdout.write(self.get_url(nonce))
self.stdout.write(self.get_url(token))

View file

@ -5,7 +5,7 @@ from django.core.management import call_command
from django.shortcuts import reverse
from django.test import TestCase
from passbook.core.models import Nonce, User
from passbook.core.models import Token, User
from passbook.lib.config import CONFIG
@ -19,17 +19,17 @@ class TestRecovery(TestCase):
"""Test creation of a new key"""
CONFIG.update_from_dict({"domain": "testserver"})
out = StringIO()
self.assertEqual(len(Nonce.objects.all()), 0)
self.assertEqual(len(Token.objects.all()), 0)
call_command("create_recovery_key", "1", self.user.username, stdout=out)
self.assertIn("https://testserver/recovery/use-nonce/", out.getvalue())
self.assertEqual(len(Nonce.objects.all()), 1)
self.assertIn("https://testserver/recovery/use-token/", out.getvalue())
self.assertEqual(len(Token.objects.all()), 1)
def test_recovery_view(self):
"""Test recovery view"""
out = StringIO()
call_command("create_recovery_key", "1", self.user.username, stdout=out)
nonce = Nonce.objects.first()
token = Token.objects.first()
self.client.get(
reverse("passbook_recovery:use-nonce", kwargs={"uuid": str(nonce.uuid)})
reverse("passbook_recovery:use-token", kwargs={"uuid": str(token.uuid)})
)
self.assertEqual(int(self.client.session["_auth_user_id"]), nonce.user.pk)
self.assertEqual(int(self.client.session["_auth_user_id"]), token.user.pk)

View file

@ -2,8 +2,8 @@
from django.urls import path
from passbook.recovery.views import UseNonceView
from passbook.recovery.views import UseTokenView
urlpatterns = [
path("use-nonce/<uuid:uuid>/", UseNonceView.as_view(), name="use-nonce"),
path("use-token/<uuid:uuid>/", UseTokenView.as_view(), name="use-token"),
]

View file

@ -6,19 +6,19 @@ from django.shortcuts import get_object_or_404, redirect
from django.utils.translation import gettext as _
from django.views import View
from passbook.core.models import Nonce
from passbook.core.models import Token
class UseNonceView(View):
"""Use nonce to login"""
class UseTokenView(View):
"""Use token to login"""
def get(self, request: HttpRequest, uuid: str) -> HttpResponse:
"""Check if nonce exists, log user in and delete nonce."""
nonce: Nonce = get_object_or_404(Nonce, pk=uuid)
if nonce.is_expired:
nonce.delete()
"""Check if token exists, log user in and delete token."""
token: Token = get_object_or_404(Token, pk=uuid)
if token.is_expired:
token.delete()
raise Http404
login(request, nonce.user, backend="django.contrib.auth.backends.ModelBackend")
nonce.delete()
login(request, token.user, backend="django.contrib.auth.backends.ModelBackend")
token.delete()
messages.warning(request, _("Used recovery-link to authenticate."))
return redirect("passbook_core:overview")

View file

@ -228,8 +228,8 @@ USE_TZ = True
# Add a 10 minute timeout to all Celery tasks.
CELERY_TASK_SOFT_TIME_LIMIT = 600
CELERY_BEAT_SCHEDULE = {
"clean_nonces": {
"task": "passbook.core.tasks.clean_nonces",
"clean_tokens": {
"task": "passbook.core.tasks.clean_tokens",
"schedule": crontab(minute="*/5"), # Run every 5 minutes
}
}

View file

@ -10,7 +10,7 @@ from django.utils.translation import gettext as _
from django.views.generic import FormView
from structlog import get_logger
from passbook.core.models import Nonce
from passbook.core.models import Token
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER
from passbook.flows.stage import AuthenticationStage
from passbook.stages.email.forms import EmailStageSendForm
@ -38,9 +38,9 @@ class EmailStageView(FormView, AuthenticationStage):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
if QS_KEY_TOKEN in request.GET:
nonce = get_object_or_404(Nonce, pk=request.GET[QS_KEY_TOKEN])
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = nonce.user
nonce.delete()
token = get_object_or_404(Token, pk=request.GET[QS_KEY_TOKEN])
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = token.user
token.delete()
messages.success(request, _("Successfully verified E-Mail."))
return self.executor.stage_ok()
return super().get(request, *args, **kwargs)
@ -50,16 +50,16 @@ class EmailStageView(FormView, AuthenticationStage):
valid_delta = timedelta(
minutes=self.executor.current_stage.token_expiry + 1
) # + 1 because django timesince always rounds down
nonce = Nonce.objects.create(user=pending_user, expires=now() + valid_delta)
token = Token.objects.create(user=pending_user, expires=now() + valid_delta)
# Send mail to user
message = TemplateEmailMessage(
subject=_("passbook - Password Recovery"),
template_name=self.executor.current_stage.template,
to=[pending_user.email],
template_context={
"url": self.get_full_url(**{QS_KEY_TOKEN: nonce.pk.hex}),
"url": self.get_full_url(**{QS_KEY_TOKEN: token.pk.hex}),
"user": pending_user,
"expires": nonce.expires,
"expires": token.expires,
},
)
send_mails(self.executor.current_stage, message)

View file

@ -5,7 +5,7 @@ from django.core import mail
from django.shortcuts import reverse
from django.test import Client, TestCase
from passbook.core.models import Nonce, User
from passbook.core.models import Token, User
from passbook.flows.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from passbook.flows.views import SESSION_KEY_PLAN
@ -77,7 +77,7 @@ class TestEmailStage(TestCase):
url = reverse(
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug}
)
token = Nonce.objects.get(user=self.user)
token = Token.objects.get(user=self.user)
url += f"?{QS_KEY_TOKEN}={token.pk.hex}"
response = self.client.get(url)
self.assertEqual(response.status_code, 302)