diff --git a/ordr3/resources.py b/ordr3/resources.py index ac77bb9..7ea69b5 100644 --- a/ordr3/resources.py +++ b/ordr3/resources.py @@ -56,7 +56,12 @@ class Root(BaseResource): def __acl__(self): """ access controll list """ - return [(Allow, Everyone, "view")] + return [ + (Allow, Everyone, "login"), + (Allow, Everyone, "logout"), + (Allow, Everyone, "registration"), + (Allow, Everyone, "view"), + ] def includeme(config): diff --git a/ordr3/static/style.css b/ordr3/static/style.css index f459932..46a2ef6 100644 --- a/ordr3/static/style.css +++ b/ordr3/static/style.css @@ -1,3 +1,40 @@ +.alert a { + color:inherit; + font-weight:bold; +} + .o3-login-card { margin-top:7em; } + + +.o3-registration-card { + margin-top:1em; + margin-bottom:2em; + } + + +.o3-registration-card .alert-danger { + font-size: 80%; + text-align: center; + } + +.o3-registration-card .help-block { + color: #6c757d!important; + font-size: 80%; + font-weight: 400; + display: block; + margin-top: .25rem; + } + +.o3-pwd-dots .bi-eye-slash, .o3-pwd-text .bi-eye { + display:inline; + } + +.o3-pwd-dots .bi-eye, .o3-pwd-text .bi-eye-slash { + display:none; + } + +.form-group.deform-form-buttons { + margin-top:2em; + } diff --git a/ordr3/templates/account/breached_password.jinja2 b/ordr3/templates/account/breached_password.jinja2 new file mode 100644 index 0000000..04cae9b --- /dev/null +++ b/ordr3/templates/account/breached_password.jinja2 @@ -0,0 +1,51 @@ +{% extends "ordr3:templates/layout_small.jinja2" %} + +{% block subtitle %} Log In {% endblock subtitle %} + +{% block content %} + +
+ Ordr itself has not suffered a breach. This is a protective measure to + reduce the risk of credential stuffing + attacks against Ordr and its users. +
++ Each time a user supplies a password while registering or updating their + password — Ordr securely checks whether that password has appeared in + public data breaches. +
++ During each of these processes, Ordr generates a SHA-1 hash of the + supplied password and uses the first five (5) characters of the hash to + check the Have I Been Pwned API + and determine if the password has been previously compromised. + The plaintext password is never stored by Ordr or submitted to the + Have I Been Pwned API. +
++ If you receive an error message saying that + "This password appears in a breach or has been compromised.", + you should change it all other places that you use it as soon as possible. +
++ This text originally appeared on the PyPI FAQ. +
+
{{message[0]}}
+{{message[1]|safe}}
+The registration is completed.
+
The account needs to be activated by an administrator.
+You should receive an email as soon as the account is activated.
+No content
+{% endblock content %} + + + diff --git a/ordr3/views/__init__.py b/ordr3/views/__init__.py index 37fe4e6..b987ab8 100644 --- a/ordr3/views/__init__.py +++ b/ordr3/views/__init__.py @@ -3,15 +3,13 @@ some view helpers are defined here """ -from collections import namedtuple -# a message for session.flash() -FlashMessage = namedtuple("FlashMessage", ["message", "description"]) +from .. import models def flash(request, channel, message, description=""): """ small wrapper around request.session.flash """ - msg = FlashMessage(message, description) + msg = models.FlashMessage(message, description) request.session.flash(msg, channel, allow_duplicate=False) diff --git a/ordr3/views/account.py b/ordr3/views/account.py new file mode 100644 index 0000000..cfdb2fd --- /dev/null +++ b/ordr3/views/account.py @@ -0,0 +1,123 @@ +""" static and login pages """ + + +import deform +from pyramid.view import view_config +from pyramid.security import forget, remember +from pyramid.httpexceptions import HTTPFound + +from .. import models, security, services +from ..schemas.account import RegistrationSchema + + +@view_config( + context="ordr3:resources.Root", + name="login", + permission="login", + request_method="GET", + renderer="ordr3:templates/account/login.jinja2", +) +def login(context, request): + return {"error": False} + + +@view_config( + context="ordr3:resources.Root", + name="login", + permission="login", + request_method="POST", + require_csrf=False, + renderer="ordr3:templates/account/login.jinja2", +) +def check_credentials(context, request): + username = request.POST.get("username", "") + password = request.POST.get("password", "") + + crypt_context = security.get_passlib_context() + user = services.verify_credentials( + request.repo, crypt_context, username, password + ) + if user is not None and user.is_active: + headers = remember(request, user.id) + return HTTPFound( + request.resource_path(request.root, "orders"), headers=headers + ) + return {"error": True} + + +@view_config( + context="ordr3:resources.Root", name="logout", permission="logout" +) +def logout(context, request): + """ logout of a user """ + return HTTPFound( + request.resource_path(request.root, "login"), headers=forget(request) + ) + + +@view_config( + context="ordr3:resources.Root", + name="registration", + permission="registration", + request_method="GET", + renderer="ordr3:templates/account/registration.jinja2", +) +def registration(context, request): + form = RegistrationSchema.as_form(request) + return {"form": form} + + +@view_config( + context="ordr3:resources.Root", + name="registration", + permission="registration", + request_method="POST", + renderer="ordr3:templates/account/registration.jinja2", +) +def register_new_user(context, request): + if "Cancel" in request.POST: + return HTTPFound(request.resource_path(request.root)) + + form = RegistrationSchema.as_form(request) + data = request.POST.items() + try: + appstruct = form.validate(data) + except deform.ValidationFailure: + return {"form": form} + + account = models.User( + id=None, + password=None, + username=appstruct["user_name"], + first_name=appstruct["first_name"], + last_name=appstruct["last_name"], + email=appstruct["email"], + role=models.UserRole.NEW, + ) + warnings = services.set_new_password(account, appstruct["password"]) + request.repo.add_user(account) + + for message in warnings: + request.flash("warning", message.message, message.description) + + return HTTPFound(request.resource_path(request.root, "registered")) + + +@view_config( + context="ordr3:resources.Root", + name="registered", + permission="view", + renderer="ordr3:templates/account/registration_complete.jinja2", +) +def registration_complete(context, request): + return {} + + +@view_config( + context="ordr3:resources.Root", + name="breached", + permission="view", + renderer="ordr3:templates/account/breached_password.jinja2", +) +def breached_password(context, request): + return {} diff --git a/ordr3/views/root.py b/ordr3/views/root.py index e41f46c..e51e0da 100644 --- a/ordr3/views/root.py +++ b/ordr3/views/root.py @@ -2,11 +2,8 @@ from pyramid.view import view_config -from pyramid.security import forget, remember from pyramid.httpexceptions import HTTPFound -from .. import security, services - @view_config( context="ordr3:resources.Root", permission="view", @@ -16,47 +13,3 @@ def root(context, request): return HTTPFound(request.resource_path(request.root, "orders")) else: return HTTPFound(request.resource_path(request.root, "login")) - - -@view_config( - context="ordr3:resources.Root", - name="login", - permission="view", - request_method="GET", - renderer="ordr3:templates/root/login.jinja2", -) -def login(context, request): - return {"error": False} - - -@view_config( - context="ordr3:resources.Root", - name="login", - permission="view", - request_method="POST", - require_csrf=False, - renderer="ordr3:templates/root/login.jinja2", -) -def check_credentials(context, request): - username = request.POST.get("username", "") - password = request.POST.get("password", "") - - crypt_context = security.get_passlib_context() - user = services.verify_credentials( - request.repo, crypt_context, username, password - ) - if user is not None and user.is_active: - headers = remember(request, user.id) - return HTTPFound( - request.resource_path(request.root, "orders"), headers=headers - ) - return {"error": True} - - -@view_config(context="ordr3:resources.Root", name="logout", permission="view") -def logout(context, request): - """ logout of a user """ - headers = forget(request) - return HTTPFound( - request.resource_path(request.root, "login"), headers=headers - )