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