From a0edc1e0a1b2808a86d1878a795816edb47e865a Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Tue, 11 May 2021 20:22:49 +0200 Subject: [PATCH 1/7] =?UTF-8?q?Kostra=20formul=C3=A1=C5=99e=20na=20dodate?= =?UTF-8?q?=C4=8Dn=C3=A9=20vyroben=C3=AD=20u=C5=BEivatele?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/views/registrace.py | 67 +++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 seminar/views/registrace.py diff --git a/seminar/views/registrace.py b/seminar/views/registrace.py new file mode 100644 index 00000000..26e9056e --- /dev/null +++ b/seminar/views/registrace.py @@ -0,0 +1,67 @@ +""" +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. + +Proto všechno kromě view začíná podtržítkem, aby se to nenatáhlo jako součást +seminar.views +""" + +#TODO: Logování (tohle logovat chce skoro určitě) + +from django.forms import Form +from django.views.generic.edit import FormView +import hmac + +from django.conf.settings import AUTH_USER_MODEL, SECRET_KEY +from seminar.models import Osoba + +class DodatecnaRegistraceUzivateleView(FormView): + form = _RegistraceUzivateleForm + template_name = ... + success_url_pattern = ... + + def form_valid(self, form): + pass + +class KontrolaUdajuASouhlasyView(FormView): + ... + +class _RegistraceUzivateleForm(Form): + #model = AUTH_USER_MODEL + # 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 = ... + verifikace_TODO = ... # TODO: Co verifikovat + + # TODO: clean_username, verifikace … + + +# Pozor, tokeny existují dva: jeden do URL do mailu, druhý do hidden položky ve formuláři. + +def _gen_token(usecase: str, data: dict[str, object]): + ... + +def _verify_token(usecase: str, data: dict[str, object]): + ... + +def _invite(osoba: Osoba): + """ + Pošle dané osobě e-mail s odkazem na registraci. + """ From c9f0d60e7cd0e54f65317a7f483594eb7939dd21 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Wed, 12 May 2021 01:48:43 +0200 Subject: [PATCH 2/7] =?UTF-8?q?J=C3=A1dro=20implementace=20view=20na=20dor?= =?UTF-8?q?egistraci?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/views/registrace.py | 136 +++++++++++++++++++++++++++++++----- 1 file changed, 117 insertions(+), 19 deletions(-) diff --git a/seminar/views/registrace.py b/seminar/views/registrace.py index 26e9056e..6e1472fb 100644 --- a/seminar/views/registrace.py +++ b/seminar/views/registrace.py @@ -4,32 +4,119 @@ 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. -Proto všechno kromě view začíná podtržítkem, aby se to nenatáhlo jako součást -seminar.views +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 enum import Enum +from django import forms +from django.contrib.auth.models import User, Permission from django.forms import Form -from django.views.generic.edit import FormView +from django.views import View +from django.views.generic.base import TemplateResponseMixin import hmac +from typing import Optional -from django.conf.settings import AUTH_USER_MODEL, SECRET_KEY +from django.conf.settings import SECRET_KEY from seminar.models import Osoba -class DodatecnaRegistraceUzivateleView(FormView): - form = _RegistraceUzivateleForm +# Složitější class-based views mi neumožňují vracet chyby. +class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View): template_name = ... - success_url_pattern = ... - - def form_valid(self, form): - pass + 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, + ) + + # Zkontrolovat, že to není moc staré poslání + now = datetime.now() + token_generation = datetime.fromisoformat(tok_data['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_id = int(tok_data['osoba']) # Pokud tam není, tak jsme vygenerovali špatný token my (byl validní). + 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, data={ + 'osoba': str(osoba_id), + 'timestamp': datetime.now().isoformat(), + }) + 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 + ... + # 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 = AUTH_USER_MODEL +class RegistraceUzivateleForm(Form): + #model = User # Zkopírováno z přihlášky :-) username = forms.CharField(label='Přihlašovací jméno', max_length=256, @@ -47,21 +134,32 @@ class _RegistraceUzivateleForm(Form): widget=forms.PasswordInput()) # Dodatečné fieldy + token… - token = ... + token = forms.CharField(widget=forms.HiddenInput(), required=True) verifikace_TODO = ... # TODO: Co verifikovat # TODO: clean_username, verifikace … +def invite(osoba: Osoba): + """ + Pošle dané osobě e-mail s odkazem na registraci. + """ + ... # Pozor, tokeny existují dva: jeden do URL do mailu, druhý do hidden položky ve formuláři. +class UseCase(Enum): + email = 'email' + form = 'form' -def _gen_token(usecase: str, data: dict[str, object]): +# Token kóduje všechno v sobě +def gen_token(usecase: _UseCase, data: dict[str, str]) -> str: ... -def _verify_token(usecase: str, data: dict[str, object]): - ... - -def _invite(osoba: Osoba): +def verify_token(usecase: _UseCase, token: str) -> Optional[dict[str, str]]: """ - Pošle dané osobě e-mail s odkazem na registraci. + Vrací slovník 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í. """ + ... + From 1d19f48ac7aa2d9d13649e657fccd93be5cb442a Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Wed, 12 May 2021 01:49:34 +0200 Subject: [PATCH 3/7] =?UTF-8?q?Rename=20souboru=20s=20doregistrac=C3=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/views/{registrace.py => doregistrace.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename seminar/views/{registrace.py => doregistrace.py} (100%) diff --git a/seminar/views/registrace.py b/seminar/views/doregistrace.py similarity index 100% rename from seminar/views/registrace.py rename to seminar/views/doregistrace.py From a11d7d011cf4049e55f98e7ffc9d6448a35abd30 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Tue, 18 May 2021 21:45:20 +0200 Subject: [PATCH 4/7] =?UTF-8?q?Implementov=C3=A1na=20validace=20token?= =?UTF-8?q?=C5=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implementoval jsem to od stolu, není to vyzkoušené. --- seminar/views/doregistrace.py | 46 +++++++++++++++++++++++++---------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/seminar/views/doregistrace.py b/seminar/views/doregistrace.py index 6e1472fb..39fbff74 100644 --- a/seminar/views/doregistrace.py +++ b/seminar/views/doregistrace.py @@ -10,6 +10,7 @@ Importovat prosím jen ty dva Views, ve výjimečných případech invite, nic d #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 @@ -38,9 +39,11 @@ class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View): 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_data['timestamp']) + token_generation = datetime.fromisoformat(tok_timestamp) delta = now - token_generation if delta >= timedelta(weeks=10): return render_to_response( @@ -52,7 +55,6 @@ class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View): # Najít osobu - osoba_id = int(tok_data['osoba']) # Pokud tam není, tak jsme vygenerovali špatný token my (byl validní). osoba = m.Osoba.objects.get(id=osoba_id) if osoba.user is not None: return render_to_response( @@ -62,10 +64,7 @@ class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View): status_code=400, ) # Vrátit view s formulářem a formulářovým tokenem - form_token = gen_token(UseCase.form, data={ - 'osoba': str(osoba_id), - 'timestamp': datetime.now().isoformat(), - }) + form_token = gen_token(UseCase.form, osoba=osoba_id, timestamp=datetime.now()) return render_to_response( context={ 'form': self.form(initial={ @@ -151,15 +150,36 @@ class UseCase(Enum): form = 'form' # Token kóduje všechno v sobě -def gen_token(usecase: _UseCase, data: dict[str, str]) -> str: - ... - -def verify_token(usecase: _UseCase, token: str) -> Optional[dict[str, str]]: +# 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í slovník dat, pokud je token validní, jinak None. + 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 From b42d18f0a47bfc84fa902cc7ec7de925f878667e Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Tue, 18 May 2021 21:46:49 +0200 Subject: [PATCH 5/7] + TODO :-) --- seminar/views/doregistrace.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/seminar/views/doregistrace.py b/seminar/views/doregistrace.py index 39fbff74..d33aae53 100644 --- a/seminar/views/doregistrace.py +++ b/seminar/views/doregistrace.py @@ -144,6 +144,8 @@ def invite(osoba: Osoba): """ ... +# 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' From 0df3cb8509314d1aac099d2b47aa3731321bc115 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Tue, 18 May 2021 22:44:26 +0200 Subject: [PATCH 6/7] =?UTF-8?q?Kontrola=20PS=C4=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/views/doregistrace.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/seminar/views/doregistrace.py b/seminar/views/doregistrace.py index d33aae53..9331349f 100644 --- a/seminar/views/doregistrace.py +++ b/seminar/views/doregistrace.py @@ -97,8 +97,20 @@ class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View): ) 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'], @@ -134,7 +146,9 @@ class RegistraceUzivateleForm(Form): # Dodatečné fieldy + token… token = forms.CharField(widget=forms.HiddenInput(), required=True) - verifikace_TODO = ... # TODO: Co verifikovat + # 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 … From 4028ceb5b90afb637b9ac3f88ef0618aa427e1c1 Mon Sep 17 00:00:00 2001 From: "Pavel \"LEdoian\" Turinsky" Date: Tue, 25 May 2021 19:28:13 +0200 Subject: [PATCH 7/7] =?UTF-8?q?Dal=C5=A1=C3=AD=20TODO?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- seminar/views/doregistrace.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/seminar/views/doregistrace.py b/seminar/views/doregistrace.py index 9331349f..c495c1ba 100644 --- a/seminar/views/doregistrace.py +++ b/seminar/views/doregistrace.py @@ -120,6 +120,9 @@ class DodatecnaRegistraceUzivateleView(TemplateResponseMixin, View): #last_name=o.prijmeni, ) u.user_permissions.add(Permission.objects.get(codename__exact='resitel')) + + # Poslat kontrolní e-mail, že registrace proběhla (a že pokud se to nemělo stát, tak ať napíšou) + ... # Přesměrovat na kontrolu údajů return ...