| @@ -2,7 +2,7 @@ | |||||
| CREATE TABLE users ( | CREATE TABLE users ( | ||||
| userid INT UNSIGNED AUTO_INCREMENT, | userid INT UNSIGNED AUTO_INCREMENT, | ||||
| username VARCHAR(45) UNIQUE NOT NULL, | username VARCHAR(45) UNIQUE NOT NULL, | ||||
| password VARCHAR(45) NOT NULL, | |||||
| password VARCHAR(60) NOT NULL, | |||||
| full_name VARCHAR(200) NOT NULL, | full_name VARCHAR(200) NOT NULL, | ||||
| company VARCHAR(50), | company VARCHAR(50), | ||||
| email VARCHAR(50) NOT NULL, | email VARCHAR(50) NOT NULL, | ||||
| @@ -84,7 +84,7 @@ CREATE TABLE task_files ( | |||||
| * Initial data | * Initial data | ||||
| */ | */ | ||||
| insert into users values (NULL, "admin", "48bead1bb864138c2cafaf1bd41332ab", "Admin Modsen", "ntnu", 'mail@ntnu.no', "street", "trondheim", "trondheim", "1234", "norway"); | |||||
| insert into users values (NULL, "admin", "$2b$12$iKbYZ0MFwWWxoYUXKRhFiOPo7itaQO2DIRnLgXbECsj8XKVzkNCSi", "Admin Modsen", "ntnu", 'mail@ntnu.no', "street", "trondheim", "trondheim", "1234", "norway"); | |||||
| insert into project_category values (NULL, "Gardening"); | insert into project_category values (NULL, "Gardening"); | ||||
| insert into project_category values (NULL, "Programming"); | insert into project_category values (NULL, "Programming"); | ||||
| @@ -4,10 +4,11 @@ FROM tiangolo/uwsgi-nginx:python3.7 | |||||
| ENV LISTEN_PORT 8080 | ENV LISTEN_PORT 8080 | ||||
| EXPOSE 8080 | EXPOSE 8080 | ||||
| # Define environment | |||||
| COPY ./app /app | |||||
| RUN echo ${groupid} > /app/models/.env | |||||
| WORKDIR /app | WORKDIR /app | ||||
| # The requirements file is copied first, so if there are no changes | |||||
| # docker will skip installing them again, and use a cached image instead | |||||
| COPY ./app/requirements.txt /app/ | |||||
| ENV PYTHONPATH=/app | ENV PYTHONPATH=/app | ||||
| # Install python dependencies | # Install python dependencies | ||||
| @@ -23,6 +24,10 @@ COPY entrypoint.sh /entrypoint.sh | |||||
| RUN chmod +x /entrypoint.sh | RUN chmod +x /entrypoint.sh | ||||
| ENTRYPOINT ["/entrypoint.sh"] | ENTRYPOINT ["/entrypoint.sh"] | ||||
| # Copy the rest of the project files and define the environment | |||||
| COPY ./app /app | |||||
| RUN echo ${groupid} > /app/models/.env | |||||
| # Allow waiting script to be executed | # Allow waiting script to be executed | ||||
| RUN chmod +x ./wait-for-it.sh | RUN chmod +x ./wait-for-it.sh | ||||
| @@ -2,6 +2,33 @@ from models.database import db | |||||
| import mysql.connector | import mysql.connector | ||||
| def get_user(username): | |||||
| """ | |||||
| Get the user with the given username | |||||
| :param username: The username | |||||
| :type username: str | |||||
| :return: user | |||||
| """ | |||||
| db.connect() | |||||
| cursor = db.cursor() | |||||
| query = ("SELECT userid, username, password from users where username = %s") | |||||
| user = None | |||||
| try: | |||||
| cursor.execute(query, (username,)) | |||||
| users = cursor.fetchall() | |||||
| if len(users): | |||||
| user = users[0] | |||||
| except mysql.connector.Error as err: | |||||
| print("Failed executing query: {}".format(err)) | |||||
| cursor.fetchall() | |||||
| exit(1) | |||||
| finally: | |||||
| cursor.close() | |||||
| db.close() | |||||
| return user | |||||
| def get_users(): | def get_users(): | ||||
| """ | """ | ||||
| Retreive all registrered users from the database | Retreive all registrered users from the database | ||||
| @@ -73,32 +100,3 @@ def get_user_name_by_id(userid): | |||||
| cursor.close() | cursor.close() | ||||
| db.close() | db.close() | ||||
| return username | return username | ||||
| def match_user(username, password): | |||||
| """ | |||||
| Check if user credentials are correct, return if exists | |||||
| :param username: The user attempting to authenticate | |||||
| :param password: The corresponding password | |||||
| :type username: str | |||||
| :type password: str | |||||
| :return: user | |||||
| """ | |||||
| db.connect() | |||||
| cursor = db.cursor() | |||||
| query = ("SELECT userid, username from users where username = %s and password = %s") | |||||
| user = None | |||||
| try: | |||||
| cursor.execute(query, (username, password)) | |||||
| users = cursor.fetchall() | |||||
| if len(users): | |||||
| user = users[0] | |||||
| except mysql.connector.Error as err: | |||||
| print("Failed executing query: {}".format(err)) | |||||
| cursor.fetchall() | |||||
| exit(1) | |||||
| finally: | |||||
| cursor.close() | |||||
| db.close() | |||||
| return user | |||||
| @@ -1,3 +1,4 @@ | |||||
| web.py==0.40 | web.py==0.40 | ||||
| mysql-connector-python==8.0.* | mysql-connector-python==8.0.* | ||||
| python-dotenv | python-dotenv | ||||
| bcrypt | |||||
| @@ -5,7 +5,7 @@ import models.session | |||||
| import models.user | import models.user | ||||
| import random | import random | ||||
| import string | import string | ||||
| import hashlib | |||||
| import bcrypt | |||||
| import time | import time | ||||
| # Get html templates | # Get html templates | ||||
| @@ -45,11 +45,9 @@ class Login(): | |||||
| data = web.input(username="", password="", remember=False) | data = web.input(username="", password="", remember=False) | ||||
| # Validate login credential with database query | # Validate login credential with database query | ||||
| password_hash = hashlib.md5(b'TDT4237' + data.password.encode('utf-8')).hexdigest() | |||||
| user = models.user.match_user(data.username, password_hash) | |||||
| user = models.user.get_user(data.username) | |||||
| # If there is a matching user/password in the database the user is logged in | |||||
| if user: | |||||
| if bcrypt.checkpw(data.password.encode('UTF-8'), user[2].encode('UTF-8')): | |||||
| self.login(user[1], user[0], data.remember) | self.login(user[1], user[0], data.remember) | ||||
| raise web.seeother("/") | raise web.seeother("/") | ||||
| else: | else: | ||||
| @@ -1,9 +1,9 @@ | |||||
| import web | import web | ||||
| from views.forms import register_form | from views.forms import register_form | ||||
| from views.utils import get_nav_bar, csrf_protected | |||||
| from views.utils import get_nav_bar, csrf_protected, password_weakness | |||||
| import models.register | import models.register | ||||
| import models.user | import models.user | ||||
| import hashlib | |||||
| import bcrypt | |||||
| import re | import re | ||||
| # Get html templates | # Get html templates | ||||
| @@ -41,9 +41,15 @@ class Register: | |||||
| if models.user.get_user_id_by_name(data.username): | if models.user.get_user_id_by_name(data.username): | ||||
| return render.register(nav, register, "Invalid user, already exists.") | return render.register(nav, register, "Invalid user, already exists.") | ||||
| models.register.set_user(data.username, | |||||
| hashlib.md5(b'TDT4237' + data.password.encode('utf-8')).hexdigest(), | |||||
| data.full_name, data.company, data.email, data.street_address, | |||||
| data.city, data.state, data.postal_code, data.country) | |||||
| # Check password security | |||||
| weakness = password_weakness(data.password, data.username) | |||||
| if weakness is not None: | |||||
| return render.register(nav, register, weakness) | |||||
| password_hash = bcrypt.hashpw(data.password.encode('UTF-8'), bcrypt.gensalt()) | |||||
| models.register.set_user(data.username, password_hash, data.full_name, data.company, | |||||
| data.email, data.street_address, data.city, data.state, | |||||
| data.postal_code, data.country) | |||||
| return render.register(nav, register_form, "User registered!") | return render.register(nav, register_form, "User registered!") | ||||
| @@ -78,3 +78,61 @@ def csrf_protected(f): | |||||
| return f(*args, **kwargs) | return f(*args, **kwargs) | ||||
| return decorated | return decorated | ||||
| def is_common_password(password): | |||||
| """Helper function that checks various common passwords.""" | |||||
| def common_sequences(n): | |||||
| # Check sequences of the same number | |||||
| for i in range(n): | |||||
| for j in range(n): | |||||
| yield ''.join([str(i) for _ in range(j)]) | |||||
| # Check incrementing sequences | |||||
| for i in range(n): | |||||
| # Starting at 0 | |||||
| seq = ''.join([str(j) for j in range(i)]) | |||||
| yield seq | |||||
| # Starting at 1 | |||||
| yield seq[1:] | |||||
| # Decrementing | |||||
| # Starting at 0 | |||||
| yield seq[::-1] | |||||
| # Starting at 1 | |||||
| yield seq[1::-1] | |||||
| common_passwords = [ | |||||
| 'password', 'qwerty', 'iloveyou', '123123', 'abc123', 'admin', | |||||
| 'passwrod', 'password1', 'beelance', 'beelance2' | |||||
| ] | |||||
| if password in common_passwords or password in common_sequences(12): | |||||
| return True | |||||
| return False | |||||
| def password_weakness(password, username): | |||||
| """ | |||||
| Check if the password fulfills the password policy. | |||||
| The policy is: | |||||
| - At least 8 characters, but not more than 70 (due to bcrypt) | |||||
| - Does not overlap with the username | |||||
| - Not a common password | |||||
| :param password: The password to check | |||||
| :param username: The username of the user (used to check similarity) | |||||
| :return: The most important weakness of the password, or None if it fulfills the policy | |||||
| """ | |||||
| if len(password) < 8: | |||||
| return "The password must be at least 5 characters long." | |||||
| elif len(password) > 70: | |||||
| return "The password can't be longer than 70 characters." | |||||
| elif password in username or username in password: | |||||
| return "The password can't overlap with your username." | |||||
| elif is_common_password(password): | |||||
| return "The password is too common. Choose something more unique." | |||||
| return None | |||||