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