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 @@ |
| 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 | 14 | from typing import Optional |
| 15 | 15 | |
| 16 | 16 | # installed packages |
| 17 | -import bcrypt | |
| 17 | +import argon2 | |
| 18 | 18 | from sqlalchemy import create_engine, select |
| 19 | 19 | from sqlalchemy.exc import OperationalError, NoResultFound, IntegrityError |
| 20 | 20 | from sqlalchemy.orm import Session |
| ... | ... | @@ -30,17 +30,25 @@ from .questions import question_from |
| 30 | 30 | # setup logger for this module |
| 31 | 31 | logger = logging.getLogger(__name__) |
| 32 | 32 | |
| 33 | +# ============================================================================ | |
| 34 | +ph = argon2.PasswordHasher() | |
| 35 | + | |
| 33 | 36 | async def check_password(password: str, hashed: bytes) -> bool: |
| 34 | 37 | '''check password in executor''' |
| 35 | 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 | 47 | async def hash_password(password: str) -> bytes: |
| 40 | 48 | '''get hash for password''' |
| 41 | 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 | 54 | class AppException(Exception): | ... | ... |
perguntations/initdb.py
| ... | ... | @@ -9,10 +9,9 @@ import csv |
| 9 | 9 | import argparse |
| 10 | 10 | import re |
| 11 | 11 | from string import capwords |
| 12 | -from concurrent.futures import ThreadPoolExecutor | |
| 13 | 12 | |
| 14 | 13 | # installed packages |
| 15 | -import bcrypt | |
| 14 | +from argon2 import PasswordHasher | |
| 16 | 15 | from sqlalchemy import create_engine, select |
| 17 | 16 | from sqlalchemy.orm import Session |
| 18 | 17 | from sqlalchemy.exc import IntegrityError |
| ... | ... | @@ -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 | 80 | SIIUE names have alien strings like "(TE)" and are sometimes capitalized |
| 82 | 81 | We remove them so that students dont keep asking what it means |
| ... | ... | @@ -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 | 107 | def insert_students_into_db(session, students) -> None: |
| 120 | 108 | '''insert list of students into the database''' |
| 121 | 109 | try: |
| 122 | 110 | session.add_all([Student(id=s['uid'], name=s['name'], password=s['pw']) |
| 123 | 111 | for s in students]) |
| 124 | 112 | session.commit() |
| 125 | - | |
| 126 | 113 | except IntegrityError: |
| 127 | 114 | print('!!! Integrity error. Users already in database. Aborted !!!\n') |
| 128 | 115 | session.rollback() |
| ... | ... | @@ -132,7 +119,6 @@ def insert_students_into_db(session, students) -> None: |
| 132 | 119 | def show_students_in_database(session, verbose=False): |
| 133 | 120 | '''get students from database''' |
| 134 | 121 | users = session.execute(select(Student)).scalars().all() |
| 135 | - # users = session.query(Student).all() | |
| 136 | 122 | total = len(users) |
| 137 | 123 | |
| 138 | 124 | print('Registered users:') |
| ... | ... | @@ -158,6 +144,7 @@ def show_students_in_database(session, verbose=False): |
| 158 | 144 | def main(): |
| 159 | 145 | '''insert, update, show students from database''' |
| 160 | 146 | |
| 147 | + ph = PasswordHasher() | |
| 161 | 148 | args = parse_commandline_arguments() |
| 162 | 149 | |
| 163 | 150 | # --- database |
| ... | ... | @@ -166,15 +153,15 @@ def main(): |
| 166 | 153 | Base.metadata.create_all(engine) # Criates schema if needed |
| 167 | 154 | session = Session(engine, future=True) |
| 168 | 155 | |
| 169 | - # --- make list of students to insert | |
| 156 | + # --- build list of new students to insert | |
| 170 | 157 | students = [] |
| 171 | 158 | |
| 172 | 159 | if args.admin: |
| 173 | - print('Adding user: 0, Admin.') | |
| 160 | + print('Adding administrator: 0, Admin') | |
| 174 | 161 | students.append({'uid': '0', 'name': 'Admin'}) |
| 175 | 162 | |
| 176 | 163 | for csvfile in args.csvfile: |
| 177 | - print('Adding users from:', csvfile) | |
| 164 | + print(f'Adding users from: {csvfile}') | |
| 178 | 165 | students.extend(get_students_from_csv(csvfile)) |
| 179 | 166 | |
| 180 | 167 | if args.add: |
| ... | ... | @@ -184,9 +171,14 @@ def main(): |
| 184 | 171 | |
| 185 | 172 | # --- insert new students |
| 186 | 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 | 182 | print(f'\nInserting {len(students)}') |
| 191 | 183 | insert_students_into_db(session, students) |
| 192 | 184 | |
| ... | ... | @@ -196,10 +188,10 @@ def main(): |
| 196 | 188 | select(Student).where(Student.id != '0') |
| 197 | 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 | 192 | for student in all_students: |
| 201 | 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 | 195 | print('.', end='', flush=True) |
| 204 | 196 | print() |
| 205 | 197 | session.commit() |
| ... | ... | @@ -213,7 +205,7 @@ def main(): |
| 213 | 205 | where(Student.id == student_id) |
| 214 | 206 | ).scalar_one() |
| 215 | 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 | 209 | session.commit() |
| 218 | 210 | |
| 219 | 211 | show_students_in_database(session, args.verbose) | ... | ... |
perguntations/models.py
| ... | ... | @@ -3,91 +3,57 @@ perguntations/models.py |
| 3 | 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 | 19 | class Student(Base): |
| 19 | - '''Student table''' | |
| 20 | 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 | 29 | class Test(Base): |
| 37 | - '''Test table''' | |
| 38 | 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 | 47 | class Question(Base): |
| 70 | - '''Question table''' | |
| 71 | 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 | 22 | url="https://git.xdi.uevora.pt/mjsb/perguntations.git", |
| 23 | 23 | packages=find_packages(), |
| 24 | 24 | include_package_data=True, # install files from MANIFEST.in |
| 25 | - python_requires='>=3.8.*', | |
| 25 | + python_requires='>=3.9', | |
| 26 | 26 | install_requires=[ |
| 27 | - 'bcrypt>=3.1', | |
| 27 | + 'argon2-cffi>=23.1', | |
| 28 | 28 | 'mistune<2.0', |
| 29 | 29 | 'pyyaml>=5.1', |
| 30 | 30 | 'pygments', |
| 31 | 31 | 'schema>=0.7.5', |
| 32 | - 'sqlalchemy>=1.4', | |
| 33 | - 'tornado>=6.3', | |
| 32 | + 'sqlalchemy>=2.0', | |
| 33 | + 'tornado>=6.4', | |
| 34 | 34 | ], |
| 35 | 35 | entry_points={ |
| 36 | 36 | 'console_scripts': [ | ... | ... |
-
mentioned in commit cc91e4c034aa4336bad33042438dd98d4f20d958
-
mentioned in commit cc91e4c034aa4336bad33042438dd98d4f20d958