Commit badbefe5ca9f8d542f9c943ed93b417470b3fa3f

Authored by Miguel Barão
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
  1 +# ignore virtual environment
  2 +.venv
  3 +
1 __pycache__/ 4 __pycache__/
2 .DS_Store 5 .DS_Store
3 demo/ans 6 demo/ans
perguntations/TODO 0 → 100644
@@ -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
1 -#!/usr/bin/env python3  
2 -  
3 ''' 1 '''
4 Handles the web, http & html part of the application interface. 2 Handles the web, http & html part of the application interface.
5 Uses the tornadoweb framework. 3 Uses the tornadoweb framework.
@@ -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': [