From badbefe5ca9f8d542f9c943ed93b417470b3fa3f Mon Sep 17 00:00:00 2001 From: Miguel Barão Date: Mon, 8 Jul 2024 21:12:49 +0100 Subject: [PATCH] move from bcrypt to argon2 --- .gitignore | 3 +++ perguntations/TODO | 17 +++++++++++++++++ perguntations/app.py | 18 +++++++++++++----- perguntations/initdb.py | 42 +++++++++++++++++------------------------- perguntations/models.py | 98 ++++++++++++++++++++++++++++++++------------------------------------------------------------------ perguntations/serve.py | 2 -- setup.py | 8 ++++---- 7 files changed, 86 insertions(+), 102 deletions(-) create mode 100644 perguntations/TODO diff --git a/.gitignore b/.gitignore index 58fb6d2..4a9c790 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# ignore virtual environment +.venv + __pycache__/ .DS_Store demo/ans diff --git a/perguntations/TODO b/perguntations/TODO new file mode 100644 index 0000000..f100c9c --- /dev/null +++ b/perguntations/TODO @@ -0,0 +1,17 @@ +# TODO + +- opcao correct de testes que nao foram corrigidos automaticamente. +- tests should store identification of the perguntations version. +- detailed CSV devia ter as refs das perguntas e so preencher as que o aluno respondeu. +- long pooling admin +- student sends updated answers +- questions with parts + + +---- Estados ----- + +inicialização (sincrona) + test factory + database setup: define lista de estudantes com teste=None + login admin: + gera todos os testes para os alunos na lista diff --git a/perguntations/app.py b/perguntations/app.py index 5b30389..e9a5a66 100644 --- a/perguntations/app.py +++ b/perguntations/app.py @@ -14,7 +14,7 @@ import os from typing import Optional # installed packages -import bcrypt +import argon2 from sqlalchemy import create_engine, select from sqlalchemy.exc import OperationalError, NoResultFound, IntegrityError from sqlalchemy.orm import Session @@ -30,17 +30,25 @@ from .questions import question_from # setup logger for this module logger = logging.getLogger(__name__) +# ============================================================================ +ph = argon2.PasswordHasher() + async def check_password(password: str, hashed: bytes) -> bool: '''check password in executor''' loop = asyncio.get_running_loop() - return await loop.run_in_executor(None, bcrypt.checkpw, - password.encode('utf-8'), hashed) + try: + return await loop.run_in_executor(None, ph.verify, hashed, password.encode('utf-8')) + except argon2.exceptions.VerifyMismatchError: + return False + except Exception: + logger.error('Unkown error while verifying password') + return False async def hash_password(password: str) -> bytes: '''get hash for password''' loop = asyncio.get_running_loop() - return await loop.run_in_executor(None, bcrypt.hashpw, - password.encode('utf-8'), bcrypt.gensalt()) + hashed = await loop.run_in_executor(None, ph.hash, password) + return hashed.encode('utf-8') # ============================================================================ class AppException(Exception): diff --git a/perguntations/initdb.py b/perguntations/initdb.py index b355076..55d0cfb 100644 --- a/perguntations/initdb.py +++ b/perguntations/initdb.py @@ -9,10 +9,9 @@ import csv import argparse import re from string import capwords -from concurrent.futures import ThreadPoolExecutor # installed packages -import bcrypt +from argon2 import PasswordHasher from sqlalchemy import create_engine, select from sqlalchemy.orm import Session from sqlalchemy.exc import IntegrityError @@ -76,7 +75,7 @@ def parse_commandline_arguments(): # ============================================================================ -def get_students_from_csv(filename): +def get_students_from_csv(filename: str): ''' SIIUE names have alien strings like "(TE)" and are sometimes capitalized We remove them so that students dont keep asking what it means @@ -105,24 +104,12 @@ def get_students_from_csv(filename): # ============================================================================ -def hashpw(student, password=None) -> None: - '''replace password by hash for a single student''' - print('.', end='', flush=True) - if password is None: - student['pw'] = '' - else: - student['pw'] = bcrypt.hashpw(password.encode('utf-8'), - bcrypt.gensalt()) - - -# ============================================================================ def insert_students_into_db(session, students) -> None: '''insert list of students into the database''' try: session.add_all([Student(id=s['uid'], name=s['name'], password=s['pw']) for s in students]) session.commit() - except IntegrityError: print('!!! Integrity error. Users already in database. Aborted !!!\n') session.rollback() @@ -132,7 +119,6 @@ def insert_students_into_db(session, students) -> None: def show_students_in_database(session, verbose=False): '''get students from database''' users = session.execute(select(Student)).scalars().all() - # users = session.query(Student).all() total = len(users) print('Registered users:') @@ -158,6 +144,7 @@ def show_students_in_database(session, verbose=False): def main(): '''insert, update, show students from database''' + ph = PasswordHasher() args = parse_commandline_arguments() # --- database @@ -166,15 +153,15 @@ def main(): Base.metadata.create_all(engine) # Criates schema if needed session = Session(engine, future=True) - # --- make list of students to insert + # --- build list of new students to insert students = [] if args.admin: - print('Adding user: 0, Admin.') + print('Adding administrator: 0, Admin') students.append({'uid': '0', 'name': 'Admin'}) for csvfile in args.csvfile: - print('Adding users from:', csvfile) + print(f'Adding users from: {csvfile}') students.extend(get_students_from_csv(csvfile)) if args.add: @@ -184,9 +171,14 @@ def main(): # --- insert new students if students: - print('Generating password hashes', end='') - with ThreadPoolExecutor() as executor: # hashing - executor.map(lambda s: hashpw(s, args.pw), students) + print('Generating password hashes') + if args.pw is None: + for s in students: + s['pw'] = '' + else: + for s in students: + s['pw'] = ph.hash(args.pw) + print('.', end='', flush=True) print(f'\nInserting {len(students)}') insert_students_into_db(session, students) @@ -196,10 +188,10 @@ def main(): select(Student).where(Student.id != '0') ).scalars().all() - print(f'Updating password of {len(all_students)} users', end='') + print(f'Updating password of {len(all_students)} users') for student in all_students: password = (args.pw or student.id).encode('utf-8') - student.password = bcrypt.hashpw(password, bcrypt.gensalt()) + student.password = ph.hash(password) print('.', end='', flush=True) print() session.commit() @@ -213,7 +205,7 @@ def main(): where(Student.id == student_id) ).scalar_one() new_password = (args.pw or student_id).encode('utf-8') - student.password = bcrypt.hashpw(new_password, bcrypt.gensalt()) + student.password = ph.hash(new_password) session.commit() show_students_in_database(session, args.verbose) diff --git a/perguntations/models.py b/perguntations/models.py index d1c6c07..568441d 100644 --- a/perguntations/models.py +++ b/perguntations/models.py @@ -3,91 +3,57 @@ perguntations/models.py SQLAlchemy ORM ''' -from typing import Any +from typing import List -from sqlalchemy import Column, ForeignKey, Integer, Float, String -from sqlalchemy.orm import declarative_base, relationship +from sqlalchemy import ForeignKey, Integer, String +from sqlalchemy.orm import DeclarativeBase, Mapped, relationship, mapped_column -# FIXME Any is a workaround for static type checking -# (https://github.com/python/mypy/issues/6372) -Base: Any = declarative_base() +class Base(DeclarativeBase): + pass +# [Student] ---1:N--- [Test] ---1:N--- [Question] + # ---------------------------------------------------------------------------- class Student(Base): - '''Student table''' __tablename__ = 'students' - id = Column(String, primary_key=True) - name = Column(String) - password = Column(String) + id = mapped_column(String, primary_key=True) + name: Mapped[str] + password: Mapped[str] # --- - tests = relationship('Test', back_populates='student') - - def __repr__(self): - return (f'Student(' - f'id={self.id!r}, ' - f'name={self.name!r}, ' - f'password={self.password!r})') - + tests: Mapped[List['Test']] = relationship(back_populates='student') # ---------------------------------------------------------------------------- class Test(Base): - '''Test table''' __tablename__ = 'tests' - id = Column(Integer, primary_key=True) # auto_increment - ref = Column(String) - title = Column(String) - grade = Column(Float) - state = Column(String) # ACTIVE, SUBMITTED, CORRECTED, QUIT, NULL - comment = Column(String) - starttime = Column(String) - finishtime = Column(String) - filename = Column(String) - student_id = Column(String, ForeignKey('students.id')) + id = mapped_column(Integer, primary_key=True) # auto_increment + ref: Mapped[str] + title: Mapped[str] + grade: Mapped[float] + state: Mapped[str] # ACTIVE, SUBMITTED, CORRECTED, QUIT, NULL + comment: Mapped[str] + starttime: Mapped[str] + finishtime: Mapped[str] + filename: Mapped[str] + student_id = mapped_column(String, ForeignKey('students.id')) # --- - student = relationship('Student', back_populates='tests') - questions = relationship('Question', back_populates='test') - - def __repr__(self): - return (f'Test(' - f'id={self.id!r}, ' - f'ref={self.ref!r}, ' - f'title={self.title!r}, ' - f'grade={self.grade!r}, ' - f'state={self.state!r}, ' - f'comment={self.comment!r}, ' - f'starttime={self.starttime!r}, ' - f'finishtime={self.finishtime!r}, ' - f'filename={self.filename!r}, ' - f'student_id={self.student_id!r})') - + student: Mapped['Student'] = relationship(back_populates='tests') + questions: Mapped[List['Question']] = relationship(back_populates='test') # --------------------------------------------------------------------------- class Question(Base): - '''Question table''' __tablename__ = 'questions' - id = Column(Integer, primary_key=True) # auto_increment - number = Column(Integer) # question number (ref may be not be unique) - ref = Column(String) - grade = Column(Float) - comment = Column(String) - starttime = Column(String) - finishtime = Column(String) - test_id = Column(String, ForeignKey('tests.id')) + id = mapped_column(Integer, primary_key=True) # auto_increment + number: Mapped[int] # question number (ref may be repeated in the same test) + ref: Mapped[str] + grade: Mapped[float] + comment: Mapped[str] + starttime: Mapped[str] + finishtime: Mapped[str] + test_id = mapped_column(String, ForeignKey('tests.id')) # --- - test = relationship('Test', back_populates='questions') - - def __repr__(self): - return (f'Question(' - f'id={self.id!r}, ' - f'number={self.number!r}, ' - f'ref={self.ref!r}, ' - f'grade={self.grade!r}, ' - f'comment={self.comment!r}, ' - f'starttime={self.starttime!r}, ' - f'finishtime={self.finishtime!r}, ' - f'test_id={self.test_id!r})') + test: Mapped['Test'] = relationship(back_populates='questions') diff --git a/perguntations/serve.py b/perguntations/serve.py index 89ab390..c5b1801 100644 --- a/perguntations/serve.py +++ b/perguntations/serve.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python3 - ''' Handles the web, http & html part of the application interface. Uses the tornadoweb framework. diff --git a/setup.py b/setup.py index 266b9d1..a5c68b3 100644 --- a/setup.py +++ b/setup.py @@ -22,15 +22,15 @@ setup( url="https://git.xdi.uevora.pt/mjsb/perguntations.git", packages=find_packages(), include_package_data=True, # install files from MANIFEST.in - python_requires='>=3.8.*', + python_requires='>=3.9', install_requires=[ - 'bcrypt>=3.1', + 'argon2-cffi>=23.1', 'mistune<2.0', 'pyyaml>=5.1', 'pygments', 'schema>=0.7.5', - 'sqlalchemy>=1.4', - 'tornado>=6.3', + 'sqlalchemy>=2.0', + 'tornado>=6.4', ], entry_points={ 'console_scripts': [ -- libgit2 0.21.2