feat: sudo view
parent
2ccf3d9679
commit
627087cf0e
|
@ -0,0 +1,33 @@
|
|||
{% extends "common/components/base.html" %}
|
||||
{% block title %} Confirm Access | Hostea Dashbaord{% endblock %}
|
||||
{% block nav %} {% include "dash/common/components/primary-nav.html" %} {% endblock %}
|
||||
|
||||
{% block main %}
|
||||
<div class="dialogue-box__container">
|
||||
<h1>{{ Title }}</h1>
|
||||
<form
|
||||
action="{% url 'accounts.sudo' %}"
|
||||
method="POST"
|
||||
class="form"
|
||||
accept-charset="utf-8"
|
||||
>
|
||||
{% include "common/components/error.html" %} {% csrf_token %}
|
||||
<p>Please login to confirm access</p>
|
||||
|
||||
<input type="hidden" name="next" value="{{ next }}">
|
||||
<label class="form__label" for="password">
|
||||
Password
|
||||
<input
|
||||
class="form__input"
|
||||
name="password"
|
||||
required
|
||||
id="password"
|
||||
type="password"
|
||||
/>
|
||||
</label>
|
||||
<div class="form__action-container">
|
||||
<button class="form__submit" type="submit">Confirm access</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endblock %}
|
|
@ -19,6 +19,7 @@ import time
|
|||
from django.contrib.auth import get_user_model
|
||||
from django.urls import reverse
|
||||
from django.test import TestCase, Client, override_settings
|
||||
from django.utils.http import urlencode
|
||||
from django.contrib.auth import authenticate
|
||||
from django.conf import settings
|
||||
|
||||
|
@ -26,6 +27,8 @@ from .models import AccountConfirmChallenge
|
|||
from .management.commands.rm_unverified_users import (
|
||||
Command as CleanUnverifiedUsersCommand,
|
||||
)
|
||||
from .utils import ConfirmAccess
|
||||
from .decorators import confirm_access
|
||||
|
||||
|
||||
def register_util(t: TestCase, username: str):
|
||||
|
@ -245,3 +248,131 @@ class UnverifiedAccountCleanupTets(TestCase):
|
|||
self.assertEqual(
|
||||
get_user_model().objects.filter(username=username2).exists(), False
|
||||
)
|
||||
|
||||
|
||||
class SudoWorks(TestCase):
|
||||
def setUp(self):
|
||||
self.username = "sudo_useworks"
|
||||
register_util(t=self, username=self.username)
|
||||
|
||||
def test_sudo_renders(self):
|
||||
c = Client()
|
||||
|
||||
resp = c.get(reverse("accounts.sudo"))
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(
|
||||
resp.headers["location"],
|
||||
f"/accounts/login/?next={reverse('accounts.sudo')}",
|
||||
)
|
||||
|
||||
login_util(t=self, c=c, redirect_to="accounts.home")
|
||||
|
||||
# GET sudo page
|
||||
ctx = {"next": reverse("accounts.home")}
|
||||
sudo_path = f"{reverse('accounts.sudo')}?{urlencode(ctx)}"
|
||||
resp = c.get(sudo_path)
|
||||
self.assertEqual(resp.status_code, 200)
|
||||
self.assertEqual(b"Please login to confirm access" in resp.content, True)
|
||||
|
||||
# Success sudo validation
|
||||
payload = {"password": self.password, "next": ctx["next"]}
|
||||
resp = c.post(reverse("accounts.sudo"), payload)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
self.assertEqual(resp.headers["location"], ctx["next"])
|
||||
|
||||
# Fail sudo validation
|
||||
payload["password"] = self.username
|
||||
resp = c.post(reverse("accounts.sudo"), payload)
|
||||
self.assertEqual(resp.status_code, 401)
|
||||
self.assertEqual(b"Wrong Password" in resp.content, True)
|
||||
|
||||
|
||||
max_sudo_ttl = 5
|
||||
|
||||
|
||||
class MockRequest:
|
||||
def __init__(self, path, session={}):
|
||||
self.path = path
|
||||
self.session = session
|
||||
|
||||
|
||||
class ConfirmAccessDecorator(TestCase):
|
||||
|
||||
# TODO: override to test TTL
|
||||
def test_redirect_to_sudo(self):
|
||||
request = MockRequest(path="/")
|
||||
resp = ConfirmAccess.redirect_to_sudo(request)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
ctx = {"next": request.path}
|
||||
self.assertEqual(
|
||||
resp.headers["location"], f"{reverse('accounts.sudo')}?{urlencode(ctx)}"
|
||||
)
|
||||
|
||||
@override_settings(HOSTEA={"ACCOUNTS": {"SUDO_TTL": max_sudo_ttl}})
|
||||
def test_is_valid(self):
|
||||
request = MockRequest(path="/")
|
||||
|
||||
# request doesn't have sudo authorization data
|
||||
self.assertEqual(ConfirmAccess.is_valid(request), False)
|
||||
|
||||
# authorize sudo
|
||||
ConfirmAccess.set(request)
|
||||
|
||||
# request has sudo authorization data and is valid for this time duration
|
||||
self.assertEqual(ConfirmAccess.is_valid(request), True)
|
||||
|
||||
time.sleep(settings.HOSTEA["ACCOUNTS"]["SUDO_TTL"] + 2)
|
||||
|
||||
# request has sudo authorization data and is not valid for this time duration
|
||||
self.assertEqual(ConfirmAccess.is_valid(request), False)
|
||||
|
||||
def test_validate_decorator_cls_method(self):
|
||||
req = MockRequest(path="/")
|
||||
req.session = {}
|
||||
|
||||
def fn(req, *args, **kwargs):
|
||||
return True
|
||||
|
||||
args = {}
|
||||
kwargs = {}
|
||||
|
||||
# request doesn't have sudo authorization data and is not valid for this time duration
|
||||
resp = ConfirmAccess.validate_decorator(req, fn, *args, **kwargs)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
ctx = {"next": req.path}
|
||||
self.assertEqual(
|
||||
resp.headers["location"], f"{reverse('accounts.sudo')}?{urlencode(ctx)}"
|
||||
)
|
||||
|
||||
# authorize sudo
|
||||
ConfirmAccess.set(req)
|
||||
# request has sudo authorization data and is valid for this time duration
|
||||
self.assertEqual(
|
||||
ConfirmAccess.validate_decorator(req, fn, *args, **kwargs), True
|
||||
)
|
||||
|
||||
def test_validate_decorator(self):
|
||||
req = MockRequest(path="/")
|
||||
req.session = {}
|
||||
|
||||
@confirm_access
|
||||
def fn(req):
|
||||
return True
|
||||
|
||||
args = {}
|
||||
kwargs = {}
|
||||
|
||||
# request doesn't have sudo authorization data and is not valid for this time duration
|
||||
resp = fn(req)
|
||||
self.assertEqual(resp.status_code, 302)
|
||||
|
||||
ctx = {"next": req.path}
|
||||
self.assertEqual(
|
||||
resp.headers["location"], f"{reverse('accounts.sudo')}?{urlencode(ctx)}"
|
||||
)
|
||||
|
||||
# authorize sudo
|
||||
ConfirmAccess.set(req)
|
||||
# request has sudo authorization data and is valid for this time duration
|
||||
self.assertEqual(fn(req), True)
|
||||
|
|
|
@ -24,6 +24,7 @@ from .views import (
|
|||
verify_account,
|
||||
resend_verification_email_view,
|
||||
verification_pending_view,
|
||||
sudo,
|
||||
)
|
||||
|
||||
urlpatterns = [
|
||||
|
@ -42,5 +43,6 @@ urlpatterns = [
|
|||
name="accounts.verify.resend",
|
||||
),
|
||||
path("accounts/verify/<str:challenge>/", verify_account, name="accounts.verify"),
|
||||
path("accounts/sudo/", sudo, name="accounts.sudo"),
|
||||
path("", protected_view, name="accounts.home"),
|
||||
]
|
||||
|
|
|
@ -23,7 +23,7 @@ from django.urls import reverse
|
|||
|
||||
|
||||
from .models import AccountConfirmChallenge
|
||||
from .utils import send_verification_email
|
||||
from .utils import send_verification_email, ConfirmAccess
|
||||
|
||||
|
||||
@csrf_protect
|
||||
|
@ -185,3 +185,36 @@ def verify_account(request, challenge):
|
|||
challenge.owned_by.save()
|
||||
challenge.delete()
|
||||
return redirect("accounts.login")
|
||||
|
||||
|
||||
@login_required
|
||||
@csrf_protect
|
||||
def sudo(request):
|
||||
def default_login_ctx():
|
||||
return {
|
||||
"title": "Confirm Access",
|
||||
}
|
||||
|
||||
if request.method == "GET":
|
||||
ctx = default_login_ctx()
|
||||
ctx["next"] = request.GET["next"]
|
||||
return render(request, "accounts/auth/sudo.html", ctx)
|
||||
|
||||
password = request.POST["password"]
|
||||
user = request.user
|
||||
|
||||
user = authenticate(
|
||||
username=user.username,
|
||||
password=request.POST["password"],
|
||||
)
|
||||
if user is None:
|
||||
ctx = default_login_ctx()
|
||||
ctx["next"] = request.POST["next"]
|
||||
ctx["error"] = {
|
||||
"title": "Wrong Password",
|
||||
"reason": "Password is incorrect, please try again.",
|
||||
}
|
||||
return render(request, "accounts/auth/sudo.html", status=401, context=ctx)
|
||||
|
||||
ConfirmAccess.set(request=request)
|
||||
return redirect(request.POST["next"])
|
||||
|
|
Loading…
Reference in New Issue