diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9ea447d..61f518d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,5 @@ --- -image: s3lph/matemat-ci:20180720-01 +image: s3lph/matemat-ci:20180802-02 stages: - test @@ -10,8 +10,8 @@ test: stage: test script: - pip3 install -r requirements.txt - - sudo -u matemat python3-coverage run --branch -m unittest discover matemat - - sudo -u matemat python3-coverage report -m --include 'matemat/*' --omit '*/test_*.py' + - sudo -u matemat python3 -m coverage run --branch -m unittest discover matemat + - sudo -u matemat python3 -m coverage report -m --include 'matemat/*' --omit '*/test_*.py' codestyle: stage: test diff --git a/Dockerfile b/Dockerfile index c427a2e..553cb64 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ -FROM python:3.6-alpine +FROM python:3.7-alpine RUN mkdir -p /var/matemat/db /var/matemat/upload RUN apk --update add libmagic diff --git a/README.md b/README.md index 49816f3..278c59e 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ This project intends to provide a well-tested and maintainable alternative to ## Dependencies -- Python 3 (>=3.6) +- Python 3 (>=3.7) - Python dependencies: - file-magic - jinja2 diff --git a/doc b/doc index ece840a..e52eec0 160000 --- a/doc +++ b/doc @@ -1 +1 @@ -Subproject commit ece840af5b19d2c78b2dbfa14adac145fab79f4f +Subproject commit e52eec0831ef72edab816d549d7ee2a85575e292 diff --git a/matemat/db/facade.py b/matemat/db/facade.py index 30fafbf..1c954db 100644 --- a/matemat/db/facade.py +++ b/matemat/db/facade.py @@ -1,4 +1,5 @@ +from __future__ import annotations from typing import List, Optional, Any, Type import crypt @@ -9,12 +10,6 @@ from matemat.exceptions import AuthenticationError, DatabaseConsistencyError from matemat.db import DatabaseWrapper from matemat.db.wrapper import Transaction -# TODO: Change to METHOD_BLOWFISH when adopting Python 3.7 -""" -The method to use for password hashing. -""" -_CRYPT_METHOD = crypt.METHOD_SHA512 - class MatematDatabase(object): """ @@ -42,7 +37,7 @@ class MatematDatabase(object): """ self.db: DatabaseWrapper = DatabaseWrapper(filename) - def __enter__(self) -> 'MatematDatabase': + def __enter__(self) -> MatematDatabase: # Pass context manager stuff through to the database wrapper self.db.__enter__() return self @@ -116,7 +111,7 @@ class MatematDatabase(object): :raises ValueError: If a user with the same name already exists. """ # Hash the password. - pwhash: str = crypt.crypt(password, crypt.mksalt(_CRYPT_METHOD)) + pwhash: str = crypt.crypt(password, crypt.mksalt()) user_id: int = -1 with self.db.transaction() as c: # Look up whether a user with the same name already exists. @@ -194,7 +189,7 @@ class MatematDatabase(object): if verify_password and not compare_digest(crypt.crypt(oldpass, row[0]), row[0]): raise AuthenticationError('Old password does not match.') # Hash the new password and write it to the database. - pwhash: str = crypt.crypt(newpass, crypt.mksalt(_CRYPT_METHOD)) + pwhash: str = crypt.crypt(newpass, crypt.mksalt()) c.execute(''' UPDATE users SET password = :pwhash, lastchange = STRFTIME('%s', 'now') WHERE user_id = :user_id ''', { @@ -224,7 +219,7 @@ class MatematDatabase(object): if verify_password and not compare_digest(crypt.crypt(password, row[0]), row[0]): raise AuthenticationError('Password does not match.') # Hash the new touchkey and write it to the database. - tkhash: Optional[str] = crypt.crypt(touchkey, crypt.mksalt(_CRYPT_METHOD)) if touchkey is not None else None + tkhash: Optional[str] = crypt.crypt(touchkey, crypt.mksalt()) if touchkey is not None else None c.execute(''' UPDATE users SET touchkey = :tkhash, lastchange = STRFTIME('%s', 'now') WHERE user_id = :user_id ''', { diff --git a/matemat/db/primitives/Product.py b/matemat/db/primitives/Product.py index dd25738..18b5003 100644 --- a/matemat/db/primitives/Product.py +++ b/matemat/db/primitives/Product.py @@ -1,75 +1,22 @@ -from typing import Any +from dataclasses import dataclass -class Product(object): +@dataclass +class Product: """ Representation of a product offered by the Matemat, with a name, prices for users, and the number of items currently in stock. + + :param id: The product ID in the database. + :param name: The product's name. + :param price_member: The price of a unit of this product for users marked as "members". + :param price_non_member: The price of a unit of this product for users NOT marked as "members". + :param stock: The number of items of this product currently in stock. """ - def __init__(self, - product_id: int, - name: str, - price_member: int, - price_non_member: int, - stock: int) -> None: - """ - Create a new product instance with the given arguments. - - :param product_id: The product ID in the database. - :param name: The product's name. - :param price_member: The price of a unit of this product for users marked as "members". - :param price_non_member: The price of a unit of this product for users NOT marked as "members". - :param stock: The number of items of this product currently in stock. - """ - self._product_id: int = product_id - self._name: str = name - self._price_member: int = price_member - self._price_non_member: int = price_non_member - self._stock: int = stock - - def __eq__(self, other: Any) -> bool: - if other is None or not isinstance(other, Product): - return False - return self._product_id == other._product_id \ - and self._name == other._name \ - and self._price_member == other._price_member \ - and self._price_non_member == other._price_non_member \ - and self._stock == other._stock - - @property - def id(self) -> int: - return self._product_id - - @property - def name(self) -> str: - return self._name - - @name.setter - def name(self, name: str) -> None: - self._name = name - - @property - def price_member(self) -> int: - return self._price_member - - @price_member.setter - def price_member(self, price: int) -> None: - self._price_member = price - - @property - def price_non_member(self) -> int: - return self._price_non_member - - @price_non_member.setter - def price_non_member(self, price: int) -> None: - self._price_non_member = price - - @property - def stock(self) -> int: - return self._stock - - @stock.setter - def stock(self, stock: int) -> None: - self._stock = stock + id: int + name: str + price_member: int + price_non_member: int + stock: int diff --git a/matemat/db/primitives/User.py b/matemat/db/primitives/User.py index 45e026d..4d2cee6 100644 --- a/matemat/db/primitives/User.py +++ b/matemat/db/primitives/User.py @@ -1,87 +1,27 @@ -from typing import Optional, Any +from typing import Optional + +from dataclasses import dataclass -class User(object): +@dataclass +class User: """ Representation of a user registered with the Matemat, with a name, e-mail address (optional), whether the user is a member of the organization the Matemat instance is used in, whether the user is an administrator, and the user's account balance. + + :param id: The user ID in the database. + :param username: The user's name. + :param balance: The balance of the user's account. + :param email: The user's e-mail address (optional). + :param admin: Whether the user is an administrator. + :param member: Whether the user is a member. """ - def __init__(self, - user_id: int, - username: str, - balance: int, - email: Optional[str] = None, - admin: bool = False, - member: bool = True) -> None: - """ - Create a new user instance with the given arguments. - - :param user_id: The user ID in the database. - :param username: The user's name. - :param balance: The balance of the user's account. - :param email: The user's e-mail address (optional). - :param admin: Whether the user is an administrator. - :param member: Whether the user is a member. - """ - self._user_id: int = user_id - self._username: str = username - self._email: Optional[str] = email - self._admin: bool = admin - self._member: bool = member - self._balance: int = balance - - def __eq__(self, other: Any) -> bool: - if other is None or not isinstance(other, User): - return False - return self._user_id == other._user_id \ - and self._username == other._username \ - and self._email == other._email \ - and self._admin == other._admin \ - and self._member == other._member - - @property - def id(self) -> int: - return self._user_id - - @property - def name(self) -> str: - return self._username - - @name.setter - def name(self, value): - self._username = value - - @property - def email(self) -> Optional[str]: - return self._email - - @email.setter - def email(self, email: str) -> None: - self._email = email - - @property - def is_admin(self) -> bool: - return self._admin - - @is_admin.setter - def is_admin(self, admin: bool) -> None: - self._admin = admin - - @property - def is_member(self) -> bool: - return self._member - - @is_member.setter - def is_member(self, member: bool) -> None: - self._member = member - - @property - def balance(self) -> int: - return self._balance - - @balance.setter - def balance(self, balance: int) -> None: - self._balance = balance + id: int + name: str + balance: int + email: Optional[str] = None + is_admin: bool = False + is_member: bool = False diff --git a/matemat/db/test/test_facade.py b/matemat/db/test/test_facade.py index 04c0eb4..d82b62c 100644 --- a/matemat/db/test/test_facade.py +++ b/matemat/db/test/test_facade.py @@ -71,7 +71,7 @@ class DatabaseTest(unittest.TestCase): u = db.create_user('testuser', 'supersecurepassword', 'testuser@example.com') # Add a touchkey without using the provided function c.execute('''UPDATE users SET touchkey = :tkhash WHERE user_id = :user_id''', { - 'tkhash': crypt.crypt('0123', crypt.mksalt(crypt.METHOD_SHA512)), + 'tkhash': crypt.crypt('0123', crypt.mksalt()), 'user_id': u.id }) user = db.login('testuser', 'supersecurepassword') @@ -112,7 +112,7 @@ class DatabaseTest(unittest.TestCase): with self.assertRaises(AuthenticationError): db.login('testuser', 'mynewpassword') db.login('testuser', 'adminpasswordreset') - user._user_id = -1 + user.id = -1 with self.assertRaises(AuthenticationError): # Password change for an inexistent user should fail db.change_password(user, 'adminpasswordreset', 'passwordwithoutuser') @@ -136,7 +136,7 @@ class DatabaseTest(unittest.TestCase): with self.assertRaises(AuthenticationError): db.login('testuser', touchkey='4567') db.login('testuser', touchkey='89ab') - user._user_id = -1 + user.id = -1 with self.assertRaises(AuthenticationError): # Touchkey change for an inexistent user should fail db.change_touchkey(user, '89ab', '048c') @@ -157,7 +157,7 @@ class DatabaseTest(unittest.TestCase): self.assertFalse(checkuser.is_admin) self.assertFalse(checkuser.is_member) self.assertEqual(4200, checkuser.balance) - user._user_id = -1 + user.id = -1 with self.assertRaises(DatabaseConsistencyError): db.change_user(user, agent, is_member='True') @@ -238,7 +238,7 @@ class DatabaseTest(unittest.TestCase): self.assertEqual(150, checkproduct.price_member) self.assertEqual(250, checkproduct.price_non_member) self.assertEqual(42, checkproduct.stock) - product._product_id = -1 + product.id = -1 with self.assertRaises(DatabaseConsistencyError): db.change_product(product) product2 = db.create_product('Club Mate', 200, 200) @@ -281,7 +281,7 @@ class DatabaseTest(unittest.TestCase): with self.assertRaises(ValueError): # Should fail, negative amount db.deposit(user, -42) - user._user_id = -1 + user.id = -1 with self.assertRaises(DatabaseConsistencyError): # Should fail, user id -1 does not exist db.deposit(user, 42) @@ -300,7 +300,7 @@ class DatabaseTest(unittest.TestCase): self.assertEqual(42, c.fetchone()[0]) c.execute('''SELECT stock FROM products WHERE product_id = ?''', [product2.id]) self.assertEqual(0, c.fetchone()[0]) - product._product_id = -1 + product.id = -1 with self.assertRaises(DatabaseConsistencyError): # Should fail, product id -1 does not exist db.restock(product, 42) diff --git a/matemat/db/wrapper.py b/matemat/db/wrapper.py index c0cddc4..fcf85e7 100644 --- a/matemat/db/wrapper.py +++ b/matemat/db/wrapper.py @@ -1,4 +1,5 @@ +from __future__ import annotations from typing import Any, Optional import sqlite3 @@ -48,7 +49,7 @@ class DatabaseWrapper(object): self._filename: str = filename self._sqlite_db: Optional[sqlite3.Connection] = None - def __enter__(self) -> 'DatabaseWrapper': + def __enter__(self) -> DatabaseWrapper: self.connect() return self diff --git a/matemat/webserver/requestargs.py b/matemat/webserver/requestargs.py index 373db90..6ce11ef 100644 --- a/matemat/webserver/requestargs.py +++ b/matemat/webserver/requestargs.py @@ -1,4 +1,5 @@ +from __future__ import annotations from typing import Dict, Iterator, List, Tuple, Union @@ -24,7 +25,7 @@ class RequestArguments(object): """ self.__container: Dict[str, RequestArgument] = dict() - def __getitem__(self, key: str) -> 'RequestArgument': + def __getitem__(self, key: str) -> RequestArgument: """ Retrieve the argument for the given name, creating it on the fly, if it doesn't exist. @@ -40,7 +41,7 @@ class RequestArguments(object): # Return the argument for the name return self.__container[key] - def __getattr__(self, key: str) -> 'RequestArgument': + def __getattr__(self, key: str) -> RequestArgument: """ Syntactic sugar for accessing values with a name that can be used in Python attributes. The value will be returned as an immutable view. @@ -278,7 +279,7 @@ class RequestArgument(object): # Yield an immutable scalar view for each (ctype, value) element in the array yield _View(self.__name, v) - def __getitem__(self, index: Union[int, slice]) -> 'RequestArgument': + def __getitem__(self, index: Union[int, slice]) -> RequestArgument: """ Index the argument with either an int or a slice. The returned values are represented as immutable RequestArgument views. diff --git a/testing/Dockerfile b/testing/Dockerfile index 94644e6..a67d602 100644 --- a/testing/Dockerfile +++ b/testing/Dockerfile @@ -1,11 +1,15 @@ -FROM debian:buster +# There is no buster image yet and stretch doesn't have a docker package. So let's just "upgrade" the image to buster. +FROM python:3.7-stretch -RUN useradd -d /home/matemat -m matemat -RUN mkdir -p /var/matemat/db && chown matemat:matemat -R /var/matemat/db -RUN mkdir -p /var/matemat/upload && chown matemat:matemat -R /var/matemat/upload -RUN apt-get update -qy -RUN apt-get install -y --no-install-recommends file sudo openssh-client git docker.io python3-dev python3-pip python3-coverage python3-setuptools build-essential -RUN pip3 install wheel pycodestyle mypy +RUN sed -re 's/stretch/buster/g' -i /etc/apt/sources.list \ + && useradd -d /home/matemat -m matemat \ + && mkdir -p /var/matemat/db /var/matemat/upload \ + && chown matemat:matemat -R /var/matemat/db \ + && chown matemat:matemat -R /var/matemat/upload \ + && apt-get update -qy \ + && apt-get install -y --no-install-recommends file sudo openssh-client git docker.io build-essential \ + && python3.7 -m pip install coverage wheel pycodestyle mypy \ + && rm -rf /var/lib/apt/lists/* WORKDIR /home/matemat