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.contrib.auth import get_user_model
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.test import TestCase, Client, override_settings
|
from django.test import TestCase, Client, override_settings
|
||||||
|
from django.utils.http import urlencode
|
||||||
from django.contrib.auth import authenticate
|
from django.contrib.auth import authenticate
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
@ -26,6 +27,8 @@ from .models import AccountConfirmChallenge
|
||||||
from .management.commands.rm_unverified_users import (
|
from .management.commands.rm_unverified_users import (
|
||||||
Command as CleanUnverifiedUsersCommand,
|
Command as CleanUnverifiedUsersCommand,
|
||||||
)
|
)
|
||||||
|
from .utils import ConfirmAccess
|
||||||
|
from .decorators import confirm_access
|
||||||
|
|
||||||
|
|
||||||
def register_util(t: TestCase, username: str):
|
def register_util(t: TestCase, username: str):
|
||||||
|
@ -245,3 +248,131 @@ class UnverifiedAccountCleanupTets(TestCase):
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
get_user_model().objects.filter(username=username2).exists(), False
|
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,
|
verify_account,
|
||||||
resend_verification_email_view,
|
resend_verification_email_view,
|
||||||
verification_pending_view,
|
verification_pending_view,
|
||||||
|
sudo,
|
||||||
)
|
)
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
|
@ -42,5 +43,6 @@ urlpatterns = [
|
||||||
name="accounts.verify.resend",
|
name="accounts.verify.resend",
|
||||||
),
|
),
|
||||||
path("accounts/verify/<str:challenge>/", verify_account, name="accounts.verify"),
|
path("accounts/verify/<str:challenge>/", verify_account, name="accounts.verify"),
|
||||||
|
path("accounts/sudo/", sudo, name="accounts.sudo"),
|
||||||
path("", protected_view, name="accounts.home"),
|
path("", protected_view, name="accounts.home"),
|
||||||
]
|
]
|
||||||
|
|
|
@ -23,7 +23,7 @@ from django.urls import reverse
|
||||||
|
|
||||||
|
|
||||||
from .models import AccountConfirmChallenge
|
from .models import AccountConfirmChallenge
|
||||||
from .utils import send_verification_email
|
from .utils import send_verification_email, ConfirmAccess
|
||||||
|
|
||||||
|
|
||||||
@csrf_protect
|
@csrf_protect
|
||||||
|
@ -185,3 +185,36 @@ def verify_account(request, challenge):
|
||||||
challenge.owned_by.save()
|
challenge.owned_by.save()
|
||||||
challenge.delete()
|
challenge.delete()
|
||||||
return redirect("accounts.login")
|
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