You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

201 lines
6.0 KiB

"""
Registrace uživatelů k existujícím osobám
V tomto souboru bude asi všechno, co je relevantní (kromě template), protože to
je dostatečně malá a jednorázová věc.
Importovat prosím jen ty dva Views, ve výjimečných případech invite, nic dalšího.
"""
#TODO: Logování (tohle logovat chce skoro určitě)
#TODO: Omezení počtu pokusů (per token -- města / údaje se bruteforcit dají, na rozdíl od tokenů)
from datetime import datetime
from enum import Enum
from django import forms
from django.contrib.auth.models import User, Permission
from django.forms import Form
from django.views import View
from django.views.generic.base import TemplateResponseMixin
import hmac
from typing import Optional
from django.conf.settings import SECRET_KEY
from seminar.models import Osoba
# Složitější class-based views mi neumožňují vracet chyby.
class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View):
template_name = ...
form = RegistraceUzivateleForm
def get(self, request, url_token):
# Ověřit token
tok_data = verify_token(UseCase.email, url_token)
if tok_data is None:
return render_to_response(
context={
'error': 'Token není platný',
},
status_code=400,
)
osoba_id, tok_timestamp = tok_data # Token prostě vypadá takhle, je to zafixované. Pokud tak nevypadá, je něco moc špatně.
# Zkontrolovat, že to není moc staré poslání
now = datetime.now()
token_generation = datetime.fromisoformat(tok_timestamp)
delta = now - token_generation
if delta >= timedelta(weeks=10):
return render_to_response(
context={
'error': 'Vypršela časová platnost tokenu',
},
status_code=400,
)
# Najít osobu
osoba = m.Osoba.objects.get(id=osoba_id)
if osoba.user is not None:
return render_to_response(
context={
'error': 'Už máte uživatele',
},
status_code=400,
)
# Vrátit view s formulářem a formulářovým tokenem
form_token = gen_token(UseCase.form, osoba=osoba_id, timestamp=datetime.now())
return render_to_response(
context={
'form': self.form(initial={
'token': form_token,
}),
}
)
def post(self, request, url_token):
# Zkontrolovat formulář
form = self.form(self.request.POST)
if not form.is_valid():
return render_to_response(
context={
'error': 'Chyba ve formuláři', # TODO: Umíme dostat konkrétní detaily?
# TODO: Formulář pro zkusení znovu?
},
status_code=400,
)
form_data = form.cleaned_data
# Zkontrolovat token
token_data = verify_token(UseCase.form, form_data['token'])
if token_data is None:
return render_to_response(
context={
'error': 'Neplatný token',
},
status_code=400,
)
osoba_id = int(token_data['osoba'])
osoba = m.Osoba.objects.get(id=osoba_id)
# Zkontrolovat verifikační field
kanonicke_psc = lambda x : ''.join(filter(str.isdigit, x))
psc = kanonicke_psc(form_data['PSC_verifikace'])
realne_psc = kanonicke_psc(osoba.psc)
# Much crypto, very secure :-)
if not hmac.compare_digest(psc, realne_psc):
return render_to_response(
context={
'error': 'Nesedí verifikační pole',
},
status_code=400,
)
# Vyrobit uživatele
u = User.objects.create_user(
username=form_data['username'],
password=form_data['password'],
email=osoba.email,
#first_name=o.jmeno,
#last_name=o.prijmeni,
)
u.user_permissions.add(Permission.objects.get(codename__exact='resitel'))
# Přesměrovat na kontrolu údajů
return ...
class KontrolaUdajuASouhlasyView(FormView):
...
class RegistraceUzivateleForm(Form):
#model = User
# Zkopírováno z přihlášky :-)
username = forms.CharField(label='Přihlašovací jméno',
max_length=256,
required=True,
help_text='Tímto jménem se následně budeš přihlašovat pro odevzdání řešení a další činnosti v semináři')
password = forms.CharField(
label='Heslo',
max_length=256,
required=True,
widget=forms.PasswordInput())
password_check = forms.CharField(
label='Ověření hesla',
max_length=256,
required=True,
widget=forms.PasswordInput())
# Dodatečné fieldy + token…
token = forms.CharField(widget=forms.HiddenInput(), required=True)
# TODO: Tohle asi blízký útočník (e.g. zapomenuté přihlášení na školním počítači) umí odhalit.
PSC_verifikace = forms.CharField(max_length=8, required=True, label="PSČ tvého bydliště",
help_text="Chceme si jen ověřit, že se někdo cizí nezmocnil tvého odkazu")
# TODO: clean_username, verifikace …
def invite(osoba: Osoba):
"""
Pošle dané osobě e-mail s odkazem na registraci.
"""
...
# TODO: Testy na tokeny?
# Pozor, tokeny existují dva: jeden do URL do mailu, druhý do hidden položky ve formuláři.
class UseCase(Enum):
email = 'email'
form = 'form'
# Token kóduje všechno v sobě
# Tokenové metody zároveň řeší konzistentní zakódování dat do tokenu, takže už to nemusí řešit nikdo jiný.
HMAC_FUNCTION='sha-256'
def gen_token(usecase: _UseCase, *, osoba_id: int, timestamp: datetime) -> str:
strmsg = '@'.join([usecase.value, str(osoba_id), timestamp.isoformat()])
msg = bytes(strmsg, 'utf-8')
key = bytes(SECRET_KEY, 'utf-8')
mac = hmac.mac(key, msg, HMAC_FUNCTION).hexdigest()
return mac+'@'+msg
def verify_token(usecase: _UseCase, token: str) -> Optional[Tuple[int, datetime]]:
"""
Vrací dvojici dat, pokud je token validní, jinak None.
Inspirováno OSMO, je díky tomu jednoduché zároveň předat dekódovaná data a
výsledek ověření.
"""
try:
tok_mac, uc, tok_osoba, tok_ts = token.split('@')
except ValueError:
# Nepodařilo se rozbít na právě čtyři části, takže je token špatně.
return None
strmsg = '@'.join([usecase.value, tok_osoba, tok_ts])
msg = bytes(strmsg, 'utf-8')
key = bytes(SECRET_KEY, 'utf-8')
valid_mac = hmac.mac(key, msg, HMAC_FUNCTION).hexdigest()
if hmac.compare_digest(tok_mac, valid_mac):
# HMAC sedí, takže data jsou v pořádku
osoba_id = int(tok_osoba)
ts = datetime.fromisoformat(tok_ts)
return (osoba_id, ts)
else:
return None