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