Commit badbefe5ca9f8d542f9c943ed93b417470b3fa3f
1 parent
8c0c618d
Exists in
dev
move from bcrypt to argon2
move from bcrypt to argon2 ignore .venv directory update sqlalchemy models to 2.0
Showing
7 changed files
with
86 additions
and
102 deletions
Show diff stats
.gitignore
| @@ -0,0 +1,17 @@ | @@ -0,0 +1,17 @@ | ||
| 1 | +# TODO | ||
| 2 | + | ||
| 3 | +- opcao correct de testes que nao foram corrigidos automaticamente. | ||
| 4 | +- tests should store identification of the perguntations version. | ||
| 5 | +- detailed CSV devia ter as refs das perguntas e so preencher as que o aluno respondeu. | ||
| 6 | +- long pooling admin | ||
| 7 | +- student sends updated answers | ||
| 8 | +- questions with parts | ||
| 9 | + | ||
| 10 | + | ||
| 11 | +---- Estados ----- | ||
| 12 | + | ||
| 13 | +inicialização (sincrona) | ||
| 14 | + test factory | ||
| 15 | + database setup: define lista de estudantes com teste=None | ||
| 16 | + login admin: | ||
| 17 | + gera todos os testes para os alunos na lista |
perguntations/app.py
| @@ -14,7 +14,7 @@ import os | @@ -14,7 +14,7 @@ import os | ||
| 14 | from typing import Optional | 14 | from typing import Optional |
| 15 | 15 | ||
| 16 | # installed packages | 16 | # installed packages |
| 17 | -import bcrypt | 17 | +import argon2 |
| 18 | from sqlalchemy import create_engine, select | 18 | from sqlalchemy import create_engine, select |
| 19 | from sqlalchemy.exc import OperationalError, NoResultFound, IntegrityError | 19 | from sqlalchemy.exc import OperationalError, NoResultFound, IntegrityError |
| 20 | from sqlalchemy.orm import Session | 20 | from sqlalchemy.orm import Session |
| @@ -30,17 +30,25 @@ from .questions import question_from | @@ -30,17 +30,25 @@ from .questions import question_from | ||
| 30 | # setup logger for this module | 30 | # setup logger for this module |
| 31 | logger = logging.getLogger(__name__) | 31 | logger = logging.getLogger(__name__) |
| 32 | 32 | ||
| 33 | +# ============================================================================ | ||
| 34 | +ph = argon2.PasswordHasher() | ||
| 35 | + | ||
| 33 | async def check_password(password: str, hashed: bytes) -> bool: | 36 | async def check_password(password: str, hashed: bytes) -> bool: |
| 34 | '''check password in executor''' | 37 | '''check password in executor''' |
| 35 | loop = asyncio.get_running_loop() | 38 | loop = asyncio.get_running_loop() |
| 36 | - return await loop.run_in_executor(None, bcrypt.checkpw, | ||
| 37 | - password.encode('utf-8'), hashed) | 39 | + try: |
| 40 | + return await loop.run_in_executor(None, ph.verify, hashed, password.encode('utf-8')) | ||
| 41 | + except argon2.exceptions.VerifyMismatchError: | ||
| 42 | + return False | ||
| 43 | + except Exception: | ||
| 44 | + logger.error('Unkown error while verifying password') | ||
| 45 | + return False | ||
| 38 | 46 | ||
| 39 | async def hash_password(password: str) -> bytes: | 47 | async def hash_password(password: str) -> bytes: |
| 40 | '''get hash for password''' | 48 | '''get hash for password''' |
| 41 | loop = asyncio.get_running_loop() | 49 | loop = asyncio.get_running_loop() |
| 42 | - return await loop.run_in_executor(None, bcrypt.hashpw, | ||
| 43 | - password.encode('utf-8'), bcrypt.gensalt()) | 50 | + hashed = await loop.run_in_executor(None, ph.hash, password) |
| 51 | + return hashed.encode('utf-8') | ||
| 44 | 52 | ||
| 45 | # ============================================================================ | 53 | # ============================================================================ |
| 46 | class AppException(Exception): | 54 | class AppException(Exception): |
perguntations/initdb.py
| @@ -9,10 +9,9 @@ import csv | @@ -9,10 +9,9 @@ import csv | ||
| 9 | import argparse | 9 | import argparse |
| 10 | import re | 10 | import re |
| 11 | from string import capwords | 11 | from string import capwords |
| 12 | -from concurrent.futures import ThreadPoolExecutor | ||
| 13 | 12 | ||
| 14 | # installed packages | 13 | # installed packages |
| 15 | -import bcrypt | 14 | +from argon2 import PasswordHasher |
| 16 | from sqlalchemy import create_engine, select | 15 | from sqlalchemy import create_engine, select |
| 17 | from sqlalchemy.orm import Session | 16 | from sqlalchemy.orm import Session |
| 18 | from sqlalchemy.exc import IntegrityError | 17 | from sqlalchemy.exc import IntegrityError |
| @@ -76,7 +75,7 @@ def parse_commandline_arguments(): | @@ -76,7 +75,7 @@ def parse_commandline_arguments(): | ||
| 76 | 75 | ||
| 77 | 76 | ||
| 78 | # ============================================================================ | 77 | # ============================================================================ |
| 79 | -def get_students_from_csv(filename): | 78 | +def get_students_from_csv(filename: str): |
| 80 | ''' | 79 | ''' |
| 81 | SIIUE names have alien strings like "(TE)" and are sometimes capitalized | 80 | SIIUE names have alien strings like "(TE)" and are sometimes capitalized |
| 82 | We remove them so that students dont keep asking what it means | 81 | We remove them so that students dont keep asking what it means |
| @@ -105,24 +104,12 @@ def get_students_from_csv(filename): | @@ -105,24 +104,12 @@ def get_students_from_csv(filename): | ||
| 105 | 104 | ||
| 106 | 105 | ||
| 107 | # ============================================================================ | 106 | # ============================================================================ |
| 108 | -def hashpw(student, password=None) -> None: | ||
| 109 | - '''replace password by hash for a single student''' | ||
| 110 | - print('.', end='', flush=True) | ||
| 111 | - if password is None: | ||
| 112 | - student['pw'] = '' | ||
| 113 | - else: | ||
| 114 | - student['pw'] = bcrypt.hashpw(password.encode('utf-8'), | ||
| 115 | - bcrypt.gensalt()) | ||
| 116 | - | ||
| 117 | - | ||
| 118 | -# ============================================================================ | ||
| 119 | def insert_students_into_db(session, students) -> None: | 107 | def insert_students_into_db(session, students) -> None: |
| 120 | '''insert list of students into the database''' | 108 | '''insert list of students into the database''' |
| 121 | try: | 109 | try: |
| 122 | session.add_all([Student(id=s['uid'], name=s['name'], password=s['pw']) | 110 | session.add_all([Student(id=s['uid'], name=s['name'], password=s['pw']) |
| 123 | for s in students]) | 111 | for s in students]) |
| 124 | session.commit() | 112 | session.commit() |
| 125 | - | ||
| 126 | except IntegrityError: | 113 | except IntegrityError: |
| 127 | print('!!! Integrity error. Users already in database. Aborted !!!\n') | 114 | print('!!! Integrity error. Users already in database. Aborted !!!\n') |
| 128 | session.rollback() | 115 | session.rollback() |
| @@ -132,7 +119,6 @@ def insert_students_into_db(session, students) -> None: | @@ -132,7 +119,6 @@ def insert_students_into_db(session, students) -> None: | ||
| 132 | def show_students_in_database(session, verbose=False): | 119 | def show_students_in_database(session, verbose=False): |
| 133 | '''get students from database''' | 120 | '''get students from database''' |
| 134 | users = session.execute(select(Student)).scalars().all() | 121 | users = session.execute(select(Student)).scalars().all() |
| 135 | - # users = session.query(Student).all() | ||
| 136 | total = len(users) | 122 | total = len(users) |
| 137 | 123 | ||
| 138 | print('Registered users:') | 124 | print('Registered users:') |
| @@ -158,6 +144,7 @@ def show_students_in_database(session, verbose=False): | @@ -158,6 +144,7 @@ def show_students_in_database(session, verbose=False): | ||
| 158 | def main(): | 144 | def main(): |
| 159 | '''insert, update, show students from database''' | 145 | '''insert, update, show students from database''' |
| 160 | 146 | ||
| 147 | + ph = PasswordHasher() | ||
| 161 | args = parse_commandline_arguments() | 148 | args = parse_commandline_arguments() |
| 162 | 149 | ||
| 163 | # --- database | 150 | # --- database |
| @@ -166,15 +153,15 @@ def main(): | @@ -166,15 +153,15 @@ def main(): | ||
| 166 | Base.metadata.create_all(engine) # Criates schema if needed | 153 | Base.metadata.create_all(engine) # Criates schema if needed |
| 167 | session = Session(engine, future=True) | 154 | session = Session(engine, future=True) |
| 168 | 155 | ||
| 169 | - # --- make list of students to insert | 156 | + # --- build list of new students to insert |
| 170 | students = [] | 157 | students = [] |
| 171 | 158 | ||
| 172 | if args.admin: | 159 | if args.admin: |
| 173 | - print('Adding user: 0, Admin.') | 160 | + print('Adding administrator: 0, Admin') |
| 174 | students.append({'uid': '0', 'name': 'Admin'}) | 161 | students.append({'uid': '0', 'name': 'Admin'}) |
| 175 | 162 | ||
| 176 | for csvfile in args.csvfile: | 163 | for csvfile in args.csvfile: |
| 177 | - print('Adding users from:', csvfile) | 164 | + print(f'Adding users from: {csvfile}') |
| 178 | students.extend(get_students_from_csv(csvfile)) | 165 | students.extend(get_students_from_csv(csvfile)) |
| 179 | 166 | ||
| 180 | if args.add: | 167 | if args.add: |
| @@ -184,9 +171,14 @@ def main(): | @@ -184,9 +171,14 @@ def main(): | ||
| 184 | 171 | ||
| 185 | # --- insert new students | 172 | # --- insert new students |
| 186 | if students: | 173 | if students: |
| 187 | - print('Generating password hashes', end='') | ||
| 188 | - with ThreadPoolExecutor() as executor: # hashing | ||
| 189 | - executor.map(lambda s: hashpw(s, args.pw), students) | 174 | + print('Generating password hashes') |
| 175 | + if args.pw is None: | ||
| 176 | + for s in students: | ||
| 177 | + s['pw'] = '' | ||
| 178 | + else: | ||
| 179 | + for s in students: | ||
| 180 | + s['pw'] = ph.hash(args.pw) | ||
| 181 | + print('.', end='', flush=True) | ||
| 190 | print(f'\nInserting {len(students)}') | 182 | print(f'\nInserting {len(students)}') |
| 191 | insert_students_into_db(session, students) | 183 | insert_students_into_db(session, students) |
| 192 | 184 | ||
| @@ -196,10 +188,10 @@ def main(): | @@ -196,10 +188,10 @@ def main(): | ||
| 196 | select(Student).where(Student.id != '0') | 188 | select(Student).where(Student.id != '0') |
| 197 | ).scalars().all() | 189 | ).scalars().all() |
| 198 | 190 | ||
| 199 | - print(f'Updating password of {len(all_students)} users', end='') | 191 | + print(f'Updating password of {len(all_students)} users') |
| 200 | for student in all_students: | 192 | for student in all_students: |
| 201 | password = (args.pw or student.id).encode('utf-8') | 193 | password = (args.pw or student.id).encode('utf-8') |
| 202 | - student.password = bcrypt.hashpw(password, bcrypt.gensalt()) | 194 | + student.password = ph.hash(password) |
| 203 | print('.', end='', flush=True) | 195 | print('.', end='', flush=True) |
| 204 | print() | 196 | print() |
| 205 | session.commit() | 197 | session.commit() |
| @@ -213,7 +205,7 @@ def main(): | @@ -213,7 +205,7 @@ def main(): | ||
| 213 | where(Student.id == student_id) | 205 | where(Student.id == student_id) |
| 214 | ).scalar_one() | 206 | ).scalar_one() |
| 215 | new_password = (args.pw or student_id).encode('utf-8') | 207 | new_password = (args.pw or student_id).encode('utf-8') |
| 216 | - student.password = bcrypt.hashpw(new_password, bcrypt.gensalt()) | 208 | + student.password = ph.hash(new_password) |
| 217 | session.commit() | 209 | session.commit() |
| 218 | 210 | ||
| 219 | show_students_in_database(session, args.verbose) | 211 | show_students_in_database(session, args.verbose) |
perguntations/models.py
| @@ -3,91 +3,57 @@ perguntations/models.py | @@ -3,91 +3,57 @@ perguntations/models.py | ||
| 3 | SQLAlchemy ORM | 3 | SQLAlchemy ORM |
| 4 | ''' | 4 | ''' |
| 5 | 5 | ||
| 6 | -from typing import Any | 6 | +from typing import List |
| 7 | 7 | ||
| 8 | -from sqlalchemy import Column, ForeignKey, Integer, Float, String | ||
| 9 | -from sqlalchemy.orm import declarative_base, relationship | 8 | +from sqlalchemy import ForeignKey, Integer, String |
| 9 | +from sqlalchemy.orm import DeclarativeBase, Mapped, relationship, mapped_column | ||
| 10 | 10 | ||
| 11 | 11 | ||
| 12 | -# FIXME Any is a workaround for static type checking | ||
| 13 | -# (https://github.com/python/mypy/issues/6372) | ||
| 14 | -Base: Any = declarative_base() | 12 | +class Base(DeclarativeBase): |
| 13 | + pass | ||
| 15 | 14 | ||
| 16 | 15 | ||
| 16 | +# [Student] ---1:N--- [Test] ---1:N--- [Question] | ||
| 17 | + | ||
| 17 | # ---------------------------------------------------------------------------- | 18 | # ---------------------------------------------------------------------------- |
| 18 | class Student(Base): | 19 | class Student(Base): |
| 19 | - '''Student table''' | ||
| 20 | __tablename__ = 'students' | 20 | __tablename__ = 'students' |
| 21 | - id = Column(String, primary_key=True) | ||
| 22 | - name = Column(String) | ||
| 23 | - password = Column(String) | ||
| 24 | 21 | ||
| 22 | + id = mapped_column(String, primary_key=True) | ||
| 23 | + name: Mapped[str] | ||
| 24 | + password: Mapped[str] | ||
| 25 | # --- | 25 | # --- |
| 26 | - tests = relationship('Test', back_populates='student') | ||
| 27 | - | ||
| 28 | - def __repr__(self): | ||
| 29 | - return (f'Student(' | ||
| 30 | - f'id={self.id!r}, ' | ||
| 31 | - f'name={self.name!r}, ' | ||
| 32 | - f'password={self.password!r})') | ||
| 33 | - | 26 | + tests: Mapped[List['Test']] = relationship(back_populates='student') |
| 34 | 27 | ||
| 35 | # ---------------------------------------------------------------------------- | 28 | # ---------------------------------------------------------------------------- |
| 36 | class Test(Base): | 29 | class Test(Base): |
| 37 | - '''Test table''' | ||
| 38 | __tablename__ = 'tests' | 30 | __tablename__ = 'tests' |
| 39 | - id = Column(Integer, primary_key=True) # auto_increment | ||
| 40 | - ref = Column(String) | ||
| 41 | - title = Column(String) | ||
| 42 | - grade = Column(Float) | ||
| 43 | - state = Column(String) # ACTIVE, SUBMITTED, CORRECTED, QUIT, NULL | ||
| 44 | - comment = Column(String) | ||
| 45 | - starttime = Column(String) | ||
| 46 | - finishtime = Column(String) | ||
| 47 | - filename = Column(String) | ||
| 48 | - student_id = Column(String, ForeignKey('students.id')) | ||
| 49 | 31 | ||
| 32 | + id = mapped_column(Integer, primary_key=True) # auto_increment | ||
| 33 | + ref: Mapped[str] | ||
| 34 | + title: Mapped[str] | ||
| 35 | + grade: Mapped[float] | ||
| 36 | + state: Mapped[str] # ACTIVE, SUBMITTED, CORRECTED, QUIT, NULL | ||
| 37 | + comment: Mapped[str] | ||
| 38 | + starttime: Mapped[str] | ||
| 39 | + finishtime: Mapped[str] | ||
| 40 | + filename: Mapped[str] | ||
| 41 | + student_id = mapped_column(String, ForeignKey('students.id')) | ||
| 50 | # --- | 42 | # --- |
| 51 | - student = relationship('Student', back_populates='tests') | ||
| 52 | - questions = relationship('Question', back_populates='test') | ||
| 53 | - | ||
| 54 | - def __repr__(self): | ||
| 55 | - return (f'Test(' | ||
| 56 | - f'id={self.id!r}, ' | ||
| 57 | - f'ref={self.ref!r}, ' | ||
| 58 | - f'title={self.title!r}, ' | ||
| 59 | - f'grade={self.grade!r}, ' | ||
| 60 | - f'state={self.state!r}, ' | ||
| 61 | - f'comment={self.comment!r}, ' | ||
| 62 | - f'starttime={self.starttime!r}, ' | ||
| 63 | - f'finishtime={self.finishtime!r}, ' | ||
| 64 | - f'filename={self.filename!r}, ' | ||
| 65 | - f'student_id={self.student_id!r})') | ||
| 66 | - | 43 | + student: Mapped['Student'] = relationship(back_populates='tests') |
| 44 | + questions: Mapped[List['Question']] = relationship(back_populates='test') | ||
| 67 | 45 | ||
| 68 | # --------------------------------------------------------------------------- | 46 | # --------------------------------------------------------------------------- |
| 69 | class Question(Base): | 47 | class Question(Base): |
| 70 | - '''Question table''' | ||
| 71 | __tablename__ = 'questions' | 48 | __tablename__ = 'questions' |
| 72 | - id = Column(Integer, primary_key=True) # auto_increment | ||
| 73 | - number = Column(Integer) # question number (ref may be not be unique) | ||
| 74 | - ref = Column(String) | ||
| 75 | - grade = Column(Float) | ||
| 76 | - comment = Column(String) | ||
| 77 | - starttime = Column(String) | ||
| 78 | - finishtime = Column(String) | ||
| 79 | - test_id = Column(String, ForeignKey('tests.id')) | ||
| 80 | 49 | ||
| 50 | + id = mapped_column(Integer, primary_key=True) # auto_increment | ||
| 51 | + number: Mapped[int] # question number (ref may be repeated in the same test) | ||
| 52 | + ref: Mapped[str] | ||
| 53 | + grade: Mapped[float] | ||
| 54 | + comment: Mapped[str] | ||
| 55 | + starttime: Mapped[str] | ||
| 56 | + finishtime: Mapped[str] | ||
| 57 | + test_id = mapped_column(String, ForeignKey('tests.id')) | ||
| 81 | # --- | 58 | # --- |
| 82 | - test = relationship('Test', back_populates='questions') | ||
| 83 | - | ||
| 84 | - def __repr__(self): | ||
| 85 | - return (f'Question(' | ||
| 86 | - f'id={self.id!r}, ' | ||
| 87 | - f'number={self.number!r}, ' | ||
| 88 | - f'ref={self.ref!r}, ' | ||
| 89 | - f'grade={self.grade!r}, ' | ||
| 90 | - f'comment={self.comment!r}, ' | ||
| 91 | - f'starttime={self.starttime!r}, ' | ||
| 92 | - f'finishtime={self.finishtime!r}, ' | ||
| 93 | - f'test_id={self.test_id!r})') | 59 | + test: Mapped['Test'] = relationship(back_populates='questions') |
perguntations/serve.py
setup.py
| @@ -22,15 +22,15 @@ setup( | @@ -22,15 +22,15 @@ setup( | ||
| 22 | url="https://git.xdi.uevora.pt/mjsb/perguntations.git", | 22 | url="https://git.xdi.uevora.pt/mjsb/perguntations.git", |
| 23 | packages=find_packages(), | 23 | packages=find_packages(), |
| 24 | include_package_data=True, # install files from MANIFEST.in | 24 | include_package_data=True, # install files from MANIFEST.in |
| 25 | - python_requires='>=3.8.*', | 25 | + python_requires='>=3.9', |
| 26 | install_requires=[ | 26 | install_requires=[ |
| 27 | - 'bcrypt>=3.1', | 27 | + 'argon2-cffi>=23.1', |
| 28 | 'mistune<2.0', | 28 | 'mistune<2.0', |
| 29 | 'pyyaml>=5.1', | 29 | 'pyyaml>=5.1', |
| 30 | 'pygments', | 30 | 'pygments', |
| 31 | 'schema>=0.7.5', | 31 | 'schema>=0.7.5', |
| 32 | - 'sqlalchemy>=1.4', | ||
| 33 | - 'tornado>=6.3', | 32 | + 'sqlalchemy>=2.0', |
| 33 | + 'tornado>=6.4', | ||
| 34 | ], | 34 | ], |
| 35 | entry_points={ | 35 | entry_points={ |
| 36 | 'console_scripts': [ | 36 | 'console_scripts': [ |
-
mentioned in commit cc91e4c034aa4336bad33042438dd98d4f20d958
-
mentioned in commit cc91e4c034aa4336bad33042438dd98d4f20d958