From 7f58da298bd5aea3725a421b58ed07441c4e07fd Mon Sep 17 00:00:00 2001 From: s3lph Date: Tue, 14 Aug 2018 22:53:39 +0200 Subject: [PATCH 1/4] Basic user bootstrapping, still needs documentation & testing. --- matemat/db/facade.py | 18 ++++++++++-- matemat/webserver/pagelets/__init__.py | 1 + matemat/webserver/pagelets/main.py | 6 ++-- matemat/webserver/pagelets/userbootstrap.py | 32 +++++++++++++++++++++ templates/userbootstrap.html | 27 +++++++++++++++++ 5 files changed, 80 insertions(+), 4 deletions(-) create mode 100644 matemat/webserver/pagelets/userbootstrap.py create mode 100644 templates/userbootstrap.html diff --git a/matemat/db/facade.py b/matemat/db/facade.py index 1c954db..99aef1d 100644 --- a/matemat/db/facade.py +++ b/matemat/db/facade.py @@ -62,7 +62,18 @@ class MatematDatabase(object): """ return self.db.transaction(exclusive=exclusive) - def list_users(self) -> List[User]: + def has_admin_users(self) -> bool: + """ + Check whether the instance has any admin users configured. + + :return: True if there are admin users, false otherwise. + """ + with self.db.transaction(exclusive=False) as c: + c.execute('SELECT COUNT(user_id) FROM users WHERE is_admin = 1') + n, = c.fetchone() + return n > 0 + + def list_users(self, with_touchkey: bool = False) -> List[User]: """ Return a list of users in the database. :return: A list of users. @@ -72,7 +83,10 @@ class MatematDatabase(object): for row in c.execute(''' SELECT user_id, username, email, is_admin, is_member, balance FROM users - '''): + WHERE touchkey IS NOT NULL OR NOT :must_have_touchkey + ''', { + 'must_have_touchkey': with_touchkey + }): # Decompose each row and put the values into a User object user_id, username, email, is_admin, is_member, balance = row users.append(User(user_id, username, balance, email, is_admin, is_member)) diff --git a/matemat/webserver/pagelets/__init__.py b/matemat/webserver/pagelets/__init__.py index c336f46..4ef6fd5 100644 --- a/matemat/webserver/pagelets/__init__.py +++ b/matemat/webserver/pagelets/__init__.py @@ -13,3 +13,4 @@ from .deposit import deposit from .admin import admin from .moduser import moduser from .modproduct import modproduct +from .userbootstrap import userbootstrap diff --git a/matemat/webserver/pagelets/main.py b/matemat/webserver/pagelets/main.py index 81f2d7d..8524e4a 100644 --- a/matemat/webserver/pagelets/main.py +++ b/matemat/webserver/pagelets/main.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Union -from matemat.webserver import pagelet, RequestArguments, PageletResponse, TemplateResponse +from matemat.webserver import pagelet, RequestArguments, PageletResponse, TemplateResponse, RedirectResponse from matemat.db import MatematDatabase @@ -30,7 +30,9 @@ def main_page(method: str, authuser=user, products=products, authlevel=authlevel, setupname=config['InstanceName']) else: + if not db.has_admin_users(): + return RedirectResponse('/userbootstrap') # If no user is logged in, fetch the list of users and render the userlist template - users = db.list_users() + users = db.list_users(with_touchkey=True) return TemplateResponse('userlist.html', users=users, setupname=config['InstanceName']) diff --git a/matemat/webserver/pagelets/userbootstrap.py b/matemat/webserver/pagelets/userbootstrap.py new file mode 100644 index 0000000..b9ac067 --- /dev/null +++ b/matemat/webserver/pagelets/userbootstrap.py @@ -0,0 +1,32 @@ + +from typing import Any, Dict, Union + +from matemat.db import MatematDatabase +from matemat.webserver import pagelet, RequestArguments, PageletResponse, RedirectResponse, TemplateResponse +from matemat.exceptions import HttpException + + +@pagelet('/userbootstrap') +def userbootstrap(method: str, + path: str, + args: RequestArguments, + session_vars: Dict[str, Any], + headers: Dict[str, str], + config: Dict[str, str]) \ + -> Union[bytes, str, PageletResponse]: + with MatematDatabase(config['DatabaseFile']) as db: + if db.has_admin_users(): + return RedirectResponse('/') + if method == 'POST': + if 'username' not in args or 'password' not in args or 'password2' not in args: + raise HttpException(400, 'Some arguments are missing') + username: str = str(args.username) + password: str = str(args.password) + password2: str = str(args.password2) + if password != password2: + return RedirectResponse('/userbootstrap') + db.create_user(username, password, None, True, False) + return RedirectResponse('/') + else: + return TemplateResponse('userbootstrap.html', + setupname=config['InstanceName']) diff --git a/templates/userbootstrap.html b/templates/userbootstrap.html new file mode 100644 index 0000000..57c8a45 --- /dev/null +++ b/templates/userbootstrap.html @@ -0,0 +1,27 @@ +{% extends "base.html" %} + +{% block header %} +

Setup

+ {{ super() }} +{% endblock %} + +{% block main %} + + {# Show a user creation form #} + Please create an admin user account +
+ +
+ + +
+ + +
+ + +
+ + {{ super() }} + +{% endblock %} From 707883b1c42b3e66257e67098b05e5fc2d746b3c Mon Sep 17 00:00:00 2001 From: s3lph Date: Tue, 14 Aug 2018 22:57:52 +0200 Subject: [PATCH 2/4] Removed trailing whitespace. --- matemat/db/facade.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/matemat/db/facade.py b/matemat/db/facade.py index 99aef1d..29562c8 100644 --- a/matemat/db/facade.py +++ b/matemat/db/facade.py @@ -83,7 +83,7 @@ class MatematDatabase(object): for row in c.execute(''' SELECT user_id, username, email, is_admin, is_member, balance FROM users - WHERE touchkey IS NOT NULL OR NOT :must_have_touchkey + WHERE touchkey IS NOT NULL OR NOT :must_have_touchkey ''', { 'must_have_touchkey': with_touchkey }): From 0b34f5ec7f664a062ec3095b21828b21b0fc9145 Mon Sep 17 00:00:00 2001 From: s3lph Date: Tue, 14 Aug 2018 23:58:54 +0200 Subject: [PATCH 3/4] Added unit tests for admin user test and touchkey-only userlist. --- matemat/db/test/test_facade.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/matemat/db/test/test_facade.py b/matemat/db/test/test_facade.py index d82b62c..5843a29 100644 --- a/matemat/db/test/test_facade.py +++ b/matemat/db/test/test_facade.py @@ -4,6 +4,7 @@ import unittest import crypt from matemat.db import MatematDatabase +from matemat.db.primitives import User from matemat.exceptions import AuthenticationError, DatabaseConsistencyError @@ -43,11 +44,15 @@ class DatabaseTest(unittest.TestCase): with self.db as db: users = db.list_users() self.assertEqual(0, len(users)) - db.create_user('testuser', 'supersecurepassword', 'testuser@example.com', True, True) + users = db.list_users(with_touchkey=True) + self.assertEqual(0, len(users)) + testuser: User = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com', True, True) + db.change_touchkey(testuser, '', 'touchkey', verify_password=False) db.create_user('anothertestuser', 'otherpassword', 'anothertestuser@example.com', False, True) db.create_user('yatu', 'igotapasswordtoo', 'yatu@example.com', False, False) users = db.list_users() - self.assertEqual(3, len(users)) + users_with_touchkey = db.list_users(with_touchkey=True) + self.assertEqual(3, len(users)) usercheck = {} for user in users: if user.name == 'testuser': @@ -64,6 +69,16 @@ class DatabaseTest(unittest.TestCase): self.assertFalse(user.is_admin) usercheck[user.id] = 1 self.assertEqual(3, len(usercheck)) + self.assertEqual(1, len(users_with_touchkey)) + self.assertEqual('testuser', users_with_touchkey[0].name) + + def test_has_admin_users(self): + with self.db as db: + self.assertFalse(db.has_admin_users()) + testuser: User = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com', True, True) + self.assertTrue(db.has_admin_users()) + db.change_user(testuser, agent=testuser, is_admin=False) + self.assertFalse(db.has_admin_users()) def test_login(self) -> None: with self.db as db: From f6901f7f9eb588c6065d18ef5f42e9d1ccbc3716 Mon Sep 17 00:00:00 2001 From: s3lph Date: Wed, 15 Aug 2018 16:18:52 +0200 Subject: [PATCH 4/4] User bootstrapping documentation --- doc | 2 +- matemat/db/facade.py | 2 ++ matemat/webserver/pagelets/main.py | 1 + matemat/webserver/pagelets/userbootstrap.py | 11 +++++++++++ templates/userbootstrap.html | 3 ++- 5 files changed, 17 insertions(+), 2 deletions(-) diff --git a/doc b/doc index c3cbf1f..9449a6d 160000 --- a/doc +++ b/doc @@ -1 +1 @@ -Subproject commit c3cbf1fdabc81c3e73d831655fe4d9c2d3d8330c +Subproject commit 9449a6dc39843969d3b549f05848b5857c23cfa3 diff --git a/matemat/db/facade.py b/matemat/db/facade.py index 29562c8..b7f4291 100644 --- a/matemat/db/facade.py +++ b/matemat/db/facade.py @@ -76,6 +76,8 @@ class MatematDatabase(object): def list_users(self, with_touchkey: bool = False) -> List[User]: """ Return a list of users in the database. + + :param with_touchkey: If true, only lists those users that have a touchkey set. Defaults to false. :return: A list of users. """ users: List[User] = [] diff --git a/matemat/webserver/pagelets/main.py b/matemat/webserver/pagelets/main.py index 8524e4a..54f6f79 100644 --- a/matemat/webserver/pagelets/main.py +++ b/matemat/webserver/pagelets/main.py @@ -30,6 +30,7 @@ def main_page(method: str, authuser=user, products=products, authlevel=authlevel, setupname=config['InstanceName']) else: + # If there are no admin users registered, jump to the admin creation procedure if not db.has_admin_users(): return RedirectResponse('/userbootstrap') # If no user is logged in, fetch the list of users and render the userlist template diff --git a/matemat/webserver/pagelets/userbootstrap.py b/matemat/webserver/pagelets/userbootstrap.py index b9ac067..8efdb39 100644 --- a/matemat/webserver/pagelets/userbootstrap.py +++ b/matemat/webserver/pagelets/userbootstrap.py @@ -14,19 +14,30 @@ def userbootstrap(method: str, headers: Dict[str, str], config: Dict[str, str]) \ -> Union[bytes, str, PageletResponse]: + """ + The page for creating a first admin user. To be used when the system is set up the first time, or when there are no + admin users left. + """ with MatematDatabase(config['DatabaseFile']) as db: + # Redirect to main if there are still administrators registered if db.has_admin_users(): return RedirectResponse('/') + # Process submission if method == 'POST': + # Make sure all required values are present if 'username' not in args or 'password' not in args or 'password2' not in args: raise HttpException(400, 'Some arguments are missing') username: str = str(args.username) password: str = str(args.password) password2: str = str(args.password2) + # The 2 passwords must match if password != password2: return RedirectResponse('/userbootstrap') + # Create the admin user db.create_user(username, password, None, True, False) + # Redirect to the main page return RedirectResponse('/') + # Requested via GET; show the user creation UI else: return TemplateResponse('userbootstrap.html', setupname=config['InstanceName']) diff --git a/templates/userbootstrap.html b/templates/userbootstrap.html index 57c8a45..3bab41f 100644 --- a/templates/userbootstrap.html +++ b/templates/userbootstrap.html @@ -1,7 +1,8 @@ {% extends "base.html" %} {% block header %} -

Setup

+ {# Show the setup name, as set in the config file, as page title followed by "Setup". Don't escape HTML entities. #} +

{{ setupname|safe }} Setup

{{ super() }} {% endblock %}