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.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 from passbook.lib.views import CreateAssignPermView
@ -92,12 +92,12 @@ class UserPasswordResetView(LoginRequiredMixin, PermissionRequiredMixin, DetailV
permission_required = "passbook_core.reset_user_password" permission_required = "passbook_core.reset_user_password"
def get(self, request, *args, **kwargs): 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) super().get(request, *args, **kwargs)
# TODO: create plan for user, get token # 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( 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( messages.success(
request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link}) request, _("Password reset link: <pre>%(link)s</pre>" % {"link": link})

View File

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

View File

@ -2,14 +2,14 @@
from django.utils.timezone import now from django.utils.timezone import now
from structlog import get_logger from structlog import get_logger
from passbook.core.models import Nonce from passbook.core.models import Token
from passbook.root.celery import CELERY_APP from passbook.root.celery import CELERY_APP
LOGGER = get_logger() LOGGER = get_logger()
@CELERY_APP.task() @CELERY_APP.task()
def clean_nonces(): def clean_tokens():
"""Remove expired nonces""" """Remove expired tokens"""
amount, _ = Nonce.objects.filter(expires__lt=now(), expiring=True).delete() amount, _ = Token.objects.filter(expires__lt=now(), expiring=True).delete()
LOGGER.debug("Deleted expired nonces", amount=amount) 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 django.utils.translation import gettext as _
from structlog import get_logger 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 from passbook.lib.config import CONFIG
LOGGER = get_logger() LOGGER = get_logger()
class Command(BaseCommand): 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.") 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." "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""" """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}" return f"https://{CONFIG.y('domain')}{path}"
def handle(self, *args, **options): def handle(self, *args, **options):
"""Create Nonce used to recover access""" """Create Token used to recover access"""
duration = int(options.get("duration", 1)) duration = int(options.get("duration", 1))
delta = timedelta(days=duration * 365.2425) delta = timedelta(days=duration * 365.2425)
_now = now() _now = now()
expiry = _now + delta expiry = _now + delta
user = User.objects.get(username=options.get("user")) user = User.objects.get(username=options.get("user"))
nonce = Nonce.objects.create( token = Token.objects.create(
expires=expiry, expires=expiry,
user=user, user=user,
description=f"Recovery Nonce generated by {getuser()} on {_now}", description=f"Recovery Token generated by {getuser()} on {_now}",
) )
self.stdout.write( self.stdout.write(
( (
@ -53,4 +53,4 @@ class Command(BaseCommand):
f" anyone to access passbook as {user}." 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.shortcuts import reverse
from django.test import TestCase 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 from passbook.lib.config import CONFIG
@ -19,17 +19,17 @@ class TestRecovery(TestCase):
"""Test creation of a new key""" """Test creation of a new key"""
CONFIG.update_from_dict({"domain": "testserver"}) CONFIG.update_from_dict({"domain": "testserver"})
out = StringIO() 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) call_command("create_recovery_key", "1", self.user.username, stdout=out)
self.assertIn("https://testserver/recovery/use-nonce/", out.getvalue()) self.assertIn("https://testserver/recovery/use-token/", out.getvalue())
self.assertEqual(len(Nonce.objects.all()), 1) self.assertEqual(len(Token.objects.all()), 1)
def test_recovery_view(self): def test_recovery_view(self):
"""Test recovery view""" """Test recovery view"""
out = StringIO() out = StringIO()
call_command("create_recovery_key", "1", self.user.username, stdout=out) call_command("create_recovery_key", "1", self.user.username, stdout=out)
nonce = Nonce.objects.first() token = Token.objects.first()
self.client.get( 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 django.urls import path
from passbook.recovery.views import UseNonceView from passbook.recovery.views import UseTokenView
urlpatterns = [ 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.utils.translation import gettext as _
from django.views import View from django.views import View
from passbook.core.models import Nonce from passbook.core.models import Token
class UseNonceView(View): class UseTokenView(View):
"""Use nonce to login""" """Use token to login"""
def get(self, request: HttpRequest, uuid: str) -> HttpResponse: def get(self, request: HttpRequest, uuid: str) -> HttpResponse:
"""Check if nonce exists, log user in and delete nonce.""" """Check if token exists, log user in and delete token."""
nonce: Nonce = get_object_or_404(Nonce, pk=uuid) token: Token = get_object_or_404(Token, pk=uuid)
if nonce.is_expired: if token.is_expired:
nonce.delete() token.delete()
raise Http404 raise Http404
login(request, nonce.user, backend="django.contrib.auth.backends.ModelBackend") login(request, token.user, backend="django.contrib.auth.backends.ModelBackend")
nonce.delete() token.delete()
messages.warning(request, _("Used recovery-link to authenticate.")) messages.warning(request, _("Used recovery-link to authenticate."))
return redirect("passbook_core:overview") return redirect("passbook_core:overview")

View File

@ -228,8 +228,8 @@ USE_TZ = True
# Add a 10 minute timeout to all Celery tasks. # Add a 10 minute timeout to all Celery tasks.
CELERY_TASK_SOFT_TIME_LIMIT = 600 CELERY_TASK_SOFT_TIME_LIMIT = 600
CELERY_BEAT_SCHEDULE = { CELERY_BEAT_SCHEDULE = {
"clean_nonces": { "clean_tokens": {
"task": "passbook.core.tasks.clean_nonces", "task": "passbook.core.tasks.clean_tokens",
"schedule": crontab(minute="*/5"), # Run every 5 minutes "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 django.views.generic import FormView
from structlog import get_logger 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.planner import PLAN_CONTEXT_PENDING_USER
from passbook.flows.stage import AuthenticationStage from passbook.flows.stage import AuthenticationStage
from passbook.stages.email.forms import EmailStageSendForm from passbook.stages.email.forms import EmailStageSendForm
@ -38,9 +38,9 @@ class EmailStageView(FormView, AuthenticationStage):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
if QS_KEY_TOKEN in request.GET: if QS_KEY_TOKEN in request.GET:
nonce = get_object_or_404(Nonce, pk=request.GET[QS_KEY_TOKEN]) token = get_object_or_404(Token, pk=request.GET[QS_KEY_TOKEN])
self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = nonce.user self.executor.plan.context[PLAN_CONTEXT_PENDING_USER] = token.user
nonce.delete() token.delete()
messages.success(request, _("Successfully verified E-Mail.")) messages.success(request, _("Successfully verified E-Mail."))
return self.executor.stage_ok() return self.executor.stage_ok()
return super().get(request, *args, **kwargs) return super().get(request, *args, **kwargs)
@ -50,16 +50,16 @@ class EmailStageView(FormView, AuthenticationStage):
valid_delta = timedelta( valid_delta = timedelta(
minutes=self.executor.current_stage.token_expiry + 1 minutes=self.executor.current_stage.token_expiry + 1
) # + 1 because django timesince always rounds down ) # + 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 # Send mail to user
message = TemplateEmailMessage( message = TemplateEmailMessage(
subject=_("passbook - Password Recovery"), subject=_("passbook - Password Recovery"),
template_name=self.executor.current_stage.template, template_name=self.executor.current_stage.template,
to=[pending_user.email], to=[pending_user.email],
template_context={ 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, "user": pending_user,
"expires": nonce.expires, "expires": token.expires,
}, },
) )
send_mails(self.executor.current_stage, message) 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.shortcuts import reverse
from django.test import Client, TestCase 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.models import Flow, FlowDesignation, FlowStageBinding
from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan from passbook.flows.planner import PLAN_CONTEXT_PENDING_USER, FlowPlan
from passbook.flows.views import SESSION_KEY_PLAN from passbook.flows.views import SESSION_KEY_PLAN
@ -77,7 +77,7 @@ class TestEmailStage(TestCase):
url = reverse( url = reverse(
"passbook_flows:flow-executor", kwargs={"flow_slug": self.flow.slug} "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}" url += f"?{QS_KEY_TOKEN}={token.pk.hex}"
response = self.client.get(url) response = self.client.get(url)
self.assertEqual(response.status_code, 302) self.assertEqual(response.status_code, 302)