From 0a71a6c84039f13da7ff254c9197f15d2336d658 Mon Sep 17 00:00:00 2001 From: Gergely Hegedus Date: Wed, 1 Feb 2023 23:15:18 +0200 Subject: [PATCH] flask server --- server/flask/Dockerfile | 32 ++ server/flask/README.md | 34 ++ server/flask/application/backend/__init__.py | 0 .../application/backend/admin_requests.py | 93 +++++ .../application/backend/auth_requests.py | 82 +++++ server/flask/application/backend/config.json | 12 + .../application/backend/data/__init__.py | 0 .../backend/data/dao_file_metadata.py | 33 ++ .../backend/data/dao_file_metadata_of_user.py | 30 ++ .../backend/data/dao_registration_tokens.py | 34 ++ .../backend/data/dao_reset_password_tokens.py | 34 ++ .../application/backend/data/dao_session.py | 78 +++++ .../application/backend/data/dao_users.py | 107 ++++++ .../application/backend/data/data_models.py | 106 ++++++ server/flask/application/backend/data/db.py | 82 +++++ .../flask/application/backend/data/schema.sql | 47 +++ .../application/backend/flask_project.py | 144 ++++++++ .../application/backend/require_decorators.py | 108 ++++++ .../backend/token_generator_util.py | 47 +++ .../backend/user_action_requests.py | 88 +++++ server/flask/application/flask-wsgi.py | 5 + server/flask/application/home-vod-server.conf | 11 + server/flask/application/home-vod-server.ini | 11 + server/flask/application/nginx-proxy-config | 64 ++++ server/flask/application/start.sh | 1 + server/flask/application/test/__init__.py | 5 + .../application/test/backend/__init__.py | 5 + .../test/backend/test_token_generator_util.py | 78 +++++ server/flask/application/test/context.py | 13 + .../flask/application/test/data/__init__.py | 5 + .../test/data/test_dao_registration_tokens.py | 75 ++++ .../data/test_dao_reset_password_tokens.py | 83 +++++ .../application/test/data/test_dao_session.py | 247 +++++++++++++ .../application/test/data/test_dao_users.py | 301 ++++++++++++++++ .../test/test_add_file_metadata.py | 238 +++++++++++++ .../test/test_add_file_metadata_of_user.py | 228 ++++++++++++ .../application/test/test_change_password.py | 315 +++++++++++++++++ .../test/test_create_registration_token.py | 295 ++++++++++++++++ .../test/test_create_reset_password_token.py | 327 ++++++++++++++++++ .../test/test_delete_registration_token.py | 214 ++++++++++++ .../application/test/test_delete_user.py | 234 +++++++++++++ .../test/test_get_file_metadata.py | 187 ++++++++++ .../test/test_get_file_metadata_of_user.py | 159 +++++++++ .../test/test_get_registration_tokens.py | 154 +++++++++ .../flask/application/test/test_get_users.py | 153 ++++++++ .../test/test_is_user_priviliged.py | 142 ++++++++ server/flask/application/test/test_login.py | 129 +++++++ server/flask/application/test/test_logout.py | 71 ++++ .../application/test/test_otp_verification.py | 165 +++++++++ .../application/test/test_refresh_token.py | 118 +++++++ .../application/test/test_registration.py | 136 ++++++++ .../application/test/test_reset_password.py | 204 +++++++++++ .../test/test_reset_user_otp_verification.py | 243 +++++++++++++ server/flask/setup.sh | 69 ++++ 54 files changed, 5876 insertions(+) create mode 100644 server/flask/Dockerfile create mode 100644 server/flask/README.md create mode 100644 server/flask/application/backend/__init__.py create mode 100644 server/flask/application/backend/admin_requests.py create mode 100644 server/flask/application/backend/auth_requests.py create mode 100644 server/flask/application/backend/config.json create mode 100644 server/flask/application/backend/data/__init__.py create mode 100644 server/flask/application/backend/data/dao_file_metadata.py create mode 100644 server/flask/application/backend/data/dao_file_metadata_of_user.py create mode 100644 server/flask/application/backend/data/dao_registration_tokens.py create mode 100644 server/flask/application/backend/data/dao_reset_password_tokens.py create mode 100644 server/flask/application/backend/data/dao_session.py create mode 100644 server/flask/application/backend/data/dao_users.py create mode 100644 server/flask/application/backend/data/data_models.py create mode 100644 server/flask/application/backend/data/db.py create mode 100644 server/flask/application/backend/data/schema.sql create mode 100644 server/flask/application/backend/flask_project.py create mode 100644 server/flask/application/backend/require_decorators.py create mode 100644 server/flask/application/backend/token_generator_util.py create mode 100644 server/flask/application/backend/user_action_requests.py create mode 100644 server/flask/application/flask-wsgi.py create mode 100644 server/flask/application/home-vod-server.conf create mode 100644 server/flask/application/home-vod-server.ini create mode 100644 server/flask/application/nginx-proxy-config create mode 100755 server/flask/application/start.sh create mode 100644 server/flask/application/test/__init__.py create mode 100644 server/flask/application/test/backend/__init__.py create mode 100644 server/flask/application/test/backend/test_token_generator_util.py create mode 100644 server/flask/application/test/context.py create mode 100644 server/flask/application/test/data/__init__.py create mode 100644 server/flask/application/test/data/test_dao_registration_tokens.py create mode 100644 server/flask/application/test/data/test_dao_reset_password_tokens.py create mode 100644 server/flask/application/test/data/test_dao_session.py create mode 100644 server/flask/application/test/data/test_dao_users.py create mode 100644 server/flask/application/test/test_add_file_metadata.py create mode 100644 server/flask/application/test/test_add_file_metadata_of_user.py create mode 100644 server/flask/application/test/test_change_password.py create mode 100644 server/flask/application/test/test_create_registration_token.py create mode 100644 server/flask/application/test/test_create_reset_password_token.py create mode 100644 server/flask/application/test/test_delete_registration_token.py create mode 100644 server/flask/application/test/test_delete_user.py create mode 100644 server/flask/application/test/test_get_file_metadata.py create mode 100644 server/flask/application/test/test_get_file_metadata_of_user.py create mode 100644 server/flask/application/test/test_get_registration_tokens.py create mode 100644 server/flask/application/test/test_get_users.py create mode 100644 server/flask/application/test/test_is_user_priviliged.py create mode 100644 server/flask/application/test/test_login.py create mode 100644 server/flask/application/test/test_logout.py create mode 100644 server/flask/application/test/test_otp_verification.py create mode 100644 server/flask/application/test/test_refresh_token.py create mode 100644 server/flask/application/test/test_registration.py create mode 100644 server/flask/application/test/test_reset_password.py create mode 100644 server/flask/application/test/test_reset_user_otp_verification.py create mode 100755 server/flask/setup.sh diff --git a/server/flask/Dockerfile b/server/flask/Dockerfile new file mode 100644 index 0000000..aa9a4f3 --- /dev/null +++ b/server/flask/Dockerfile @@ -0,0 +1,32 @@ +FROM python:3.9-bullseye + +# ENV setup +RUN ["python","--version"] +RUN ["apt-get", "update"] +RUN ["apt-get", "install", "nginx", "-y"] +RUN ["pip", "install", "uwsgi"] +RUN ["uwsgi", "--version"] +RUN ["pip", "install", "flask"] +RUN ["pip", "install", "passlib"] +RUN ["pip", "install", "pyotp"] + +# NGINX configuration setup +COPY ./application/nginx-proxy-config /etc/nginx/sites-available/nginx-proxy-config +RUN ln -s /etc/nginx/sites-available/nginx-proxy-config /etc/nginx/sites-enabled + +COPY ./certificate /certificate + +WORKDIR $HOME/server + +# load source files +COPY ./application $HOME/server + +# run server commands +RUN nginx -t +RUN service nginx configtest + +# create database - no longer needed, since binding volume +#RUN mkdir instance +#RUN python db.py + +CMD service nginx restart & uwsgi --ini home-vod-server.ini \ No newline at end of file diff --git a/server/flask/README.md b/server/flask/README.md new file mode 100644 index 0000000..f1709cd --- /dev/null +++ b/server/flask/README.md @@ -0,0 +1,34 @@ +# Home-VOD Flask Backend + +Learning Python Flask Project. + +## Development + +For Runtime Environment I have used a docker container. The `setup.sh` script should create it. + +The source code is bound to the container so any changes are reflected inside the container as well. + +## Tests + +### Just run all tests + +You can run all tests just from docker, using docker exec: + +`docker exec home-vod-server python -m unittest discover -v -s .` + +This should run all the tests. + +> Ensure the testdb is deleted if you have stopped the execution of tests. + +### Interactive mode + +While developing you may want to run a single test multiple times. Suggestion is to connect interactively with the container: +`docker exec -it home-vod-server /bin/bash` + +Then use the following command to run the specific test: +`python -m unittest ./test/test_logout.py` + +You can also run all the tests with the following at this point. +`python -m unittest discover -v -s .` + +> Ensure you are in the `/server` folder \ No newline at end of file diff --git a/server/flask/application/backend/__init__.py b/server/flask/application/backend/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/flask/application/backend/admin_requests.py b/server/flask/application/backend/admin_requests.py new file mode 100644 index 0000000..66747a8 --- /dev/null +++ b/server/flask/application/backend/admin_requests.py @@ -0,0 +1,93 @@ +from flask import request, jsonify +from .require_decorators import get_cropped_username +from .require_decorators import get_cropped_otp +from .require_decorators import get_cropped_token +from . import token_generator_util +from .data import dao_registration_tokens +from .data import dao_reset_password_tokens +from .data import dao_users +from .data import dao_session +from .data.data_models import DataError +from .data.data_models import User +from .data.data_models import ResponseCode + +def handle_create_registration_token(user: User): + new_registration_token = get_cropped_otp(request.form.get('registration_token')) + if new_registration_token is None or new_registration_token.strip() == '': + errorResponse = jsonify({'message':'Invalid Registration Token given!','code':ResponseCode.INVALID_REGISTRATION_TOKEN}) + return errorResponse, 400 + + result = dao_registration_tokens.insert_token(new_registration_token) + if (result is DataError.REGISTRATION_CODE_ALREADY_EXISTS): + errorResponse = jsonify({'message':'Invalid Registration Token given!','code':ResponseCode.INVALID_REGISTRATION_TOKEN}) + return errorResponse, 400 + + return jsonify({'message':'Registration token Saved!','code':ResponseCode.SUCCESS_SAVED_REGISTRATION_TOKEN}), 200 + +def handle_create_reset_password_token(user: User): + reset_password_token = get_cropped_otp(request.form.get('reset_password_token')) + username_to_reset = get_cropped_username(request.form.get('username_to_reset')) + if reset_password_token is None or reset_password_token.strip() == '': + errorResponse = jsonify({'message':'Invalid Reset Password Token given!','code':ResponseCode.INVALID_RESET_PASSWORD_TOKEN}) + return errorResponse, 400 + + if username_to_reset is None or username_to_reset.strip() == '': + errorResponse = jsonify({'message':'username_to_reset cannot be empty!','code':ResponseCode.INVALID_USERNAME_TO_EDIT}) + return errorResponse, 400 + + expires_at = token_generator_util.generate_reset_password_expires_at() + + dao_reset_password_tokens.insert_token(token = reset_password_token, username = username_to_reset, expires_at = expires_at) + + return jsonify({'message':'Reset Password token Saved!','code':ResponseCode.SUCCESS_SAVED_RESET_PASSWORD_TOKEN}), 200 + +def handle_reset_user_otp_verification(user: User): + username = get_cropped_username(request.form.get('username_to_reset')) + if username is None or username.strip() == '': + errorResponse = jsonify({'message':'username_to_reset cannot be empty!','code':ResponseCode.INVALID_USERNAME_TO_EDIT}) + return errorResponse, 400 + + user_to_update = dao_users.get_user_by_name(username) + if user_to_update is None: + errorResponse = jsonify({'message':'User cannot be found!','code':ResponseCode.NOT_FOUND_USER}) + return errorResponse, 400 + + dao_users.update_user_otp_verification(user_to_update.id, False) + + return jsonify({'message':'OTP Verification Reset!','code':ResponseCode.SUCCESS_RESET_OTP_VERIFICATION}), 200 + +def handle_get_users(user: User): + users = dao_users.get_users() + simplified_users = map(lambda user: {'name':user.name,'privileged':user.privileged}, users) + + return jsonify({'users': list(simplified_users)}), 200 + +def handle_get_registration_tokens(user: User): + tokens = dao_registration_tokens.get_tokens() + + return jsonify({'registration_tokens': list(tokens)}), 200 + +def handle_delete_user_by_name(user: User): + user_name_to_delete = get_cropped_username(request.form.get('username_to_delete')) + success_response = jsonify({'message':'User deleted!','code':ResponseCode.SUCCESS_DELETED_USER}) + if user_name_to_delete is None: + return success_response, 200 + + user_to_delete = dao_users.get_user_by_name(user_name_to_delete) + if user_to_delete is None: + return success_response, 200 + + dao_session.delete_all_user_session_by_user_id(user_to_delete.id) + dao_users.delete_user_by_id(user_to_delete.id) + + return success_response, 200 + +def handle_delete_registration_token(user: User): + registration_token_to_delete = get_cropped_otp(request.form.get('registration_token')) + success_response = jsonify({'message':'Token deleted!','code':ResponseCode.SUCCESS_DELETED_TOKEN}) + if registration_token_to_delete is None: + return success_response, 200 + + dao_registration_tokens.delete_token(registration_token_to_delete) + + return success_response, 200 diff --git a/server/flask/application/backend/auth_requests.py b/server/flask/application/backend/auth_requests.py new file mode 100644 index 0000000..940f592 --- /dev/null +++ b/server/flask/application/backend/auth_requests.py @@ -0,0 +1,82 @@ +from flask import request, jsonify +from .require_decorators import get_cropped_otp +from .require_decorators import get_cropped_token +from . import token_generator_util +from .data import dao_registration_tokens +from .data import dao_users +from .data import dao_session +from .data.data_models import DataError +from .data.data_models import RegisteringUser +from .data.data_models import User +from .data.data_models import ResponseCode + +def handle_register(username, password): + one_time_password = get_cropped_otp(request.form.get('otp') or '') + if not dao_registration_tokens.is_valid_token(one_time_password): + errorResponse = jsonify({'message':'Invalid Token!','code':ResponseCode.UNKNOWN_REGISTRATION_TOKEN}) + return errorResponse, 400 + + one_time_password_secret = token_generator_util.generate_otp_secret() + user = RegisteringUser(name = username, password = password, otp_secret = one_time_password_secret) + result = dao_users.insert_user(user) + if (result is DataError.USER_NAME_NOT_VALID): + errorResponse = jsonify({'message':'Username is already taken!','code':ResponseCode.ALREADY_TAKEN_USERNAME}) + return errorResponse, 400 + + dao_registration_tokens.delete_token(one_time_password) + secret_url = token_generator_util.get_url(user.name, one_time_password_secret) + return jsonify({'otp_secret': secret_url}), 200 + +def handle_login(user: User): + if user.was_otp_verified: + successResponse = jsonify({'message':'User found!','code':ResponseCode.SUCCESS_FOUND_USER}) + return successResponse, 200 + else: + secret_url = token_generator_util.get_url(username=user.name, secret=user.otp_secret) + return jsonify({'otp_secret': secret_url}), 200 + +def handle_otp_verification(user: User): + one_time_password = get_cropped_otp(request.form.get('otp') or '') + is_otp_ok = token_generator_util.verify_otp(user.otp_secret, one_time_password) + if (is_otp_ok): + dao_users.update_user_otp_verification(user.id, True) + session = token_generator_util.generate_session(user.id) + dao_session.insert_user_session(session) + sessionResponse = jsonify({ + 'access_token': session.access_token, + 'refresh_token': session.refresh_token, + 'expires_at': session.access_expires_at + }) + return sessionResponse, 200 + else: + errorResponse = jsonify({'message':'Invalid Token!','code':ResponseCode.INVALID_OTP}) + return errorResponse, 400 + +def handle_logout(): + access_token = get_cropped_token(request.headers.get('Authorization')) + if (access_token is None): + return '', 200 + else: + dao_session.delete_user_session(access_token = access_token) + return '', 200 + +def handle_refresh_token(): + refresh_token = get_cropped_token(request.form.get('refresh_token')) + if refresh_token is None: + errorResponse = jsonify({'message':'Invalid Refresh Token!','code':ResponseCode.INVALID_REFRESH_TOKEN}) + return errorResponse, 400 + + user_id = dao_session.get_user_for_refresh_token(refresh_token) + if user_id is None: + errorResponse = jsonify({'message':'Invalid Refresh Token!','code':ResponseCode.INVALID_REFRESH_TOKEN}) + return errorResponse, 400 + + new_session = token_generator_util.generate_session(user_id) + dao_session.swap_refresh_session(refresh_token = refresh_token, session = new_session) + + sessionResponse = jsonify({ + 'access_token': new_session.access_token, + 'refresh_token': new_session.refresh_token, + 'expires_at': new_session.access_expires_at + }) + return sessionResponse, 200 \ No newline at end of file diff --git a/server/flask/application/backend/config.json b/server/flask/application/backend/config.json new file mode 100644 index 0000000..91f6416 --- /dev/null +++ b/server/flask/application/backend/config.json @@ -0,0 +1,12 @@ +{ + "SECRECT_BYTE_COUNT": 64, + "SESSION_ACCESS_EXPIRATION_IN_SECONDS": 86400, + "SESSION_REFRESH_EXPIRATION_IN_SECONDS": 259200, + "RESET_PASSWORD_EXPIRATION_IN_SECONDS": 432000, + "MAX_PASSWORD_LENGTH": 64, + "MAX_USERNAME_LENGTH": 64, + "MAX_TOKEN_LENGTH": 200, + "KEY_LENGTH": 150, + "MAX_OTP_LENGTH": 16, + "DATABASE_NAME": "sqlitedb" +} \ No newline at end of file diff --git a/server/flask/application/backend/data/__init__.py b/server/flask/application/backend/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/flask/application/backend/data/dao_file_metadata.py b/server/flask/application/backend/data/dao_file_metadata.py new file mode 100644 index 0000000..ded5231 --- /dev/null +++ b/server/flask/application/backend/data/dao_file_metadata.py @@ -0,0 +1,33 @@ +from .db import get_db +from .data_models import DataError +from sqlite3 import IntegrityError + +_INSER_METADATA_SQL = "INSERT INTO file_metadata(file_key,metadata) "\ +"VALUES(:file_key, :metadata)" +_DELETE_METADATA_SQL = "DELETE FROM file_metadata WHERE file_key=:file_key" +def insert_metadata(metadata: dict): + db = get_db() + db_cursor = db.cursor() + + delete_params = map(lambda key: {'file_key':key}, metadata.keys()) + delete_params = list(delete_params) + db_cursor.executemany(_DELETE_METADATA_SQL, delete_params) + + insert_params = map(lambda item: {'file_key':item[0], 'metadata':item[1]}, metadata.items()) + insert_params = list(insert_params) + + + db_cursor.executemany(_INSER_METADATA_SQL, insert_params) + db.commit() + +def get_metadata(file_key: str): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute("SELECT metadata FROM file_metadata WHERE file_key=:file_key",{"file_key":file_key}) + rows = db_cursor.fetchone() + + if (rows is None): + return dict() + + converted_tuple_array = map(lambda metadata: (file_key, metadata), rows) + return dict(converted_tuple_array) \ No newline at end of file diff --git a/server/flask/application/backend/data/dao_file_metadata_of_user.py b/server/flask/application/backend/data/dao_file_metadata_of_user.py new file mode 100644 index 0000000..2490c0b --- /dev/null +++ b/server/flask/application/backend/data/dao_file_metadata_of_user.py @@ -0,0 +1,30 @@ +from .db import get_db +from .data_models import DataError +from sqlite3 import IntegrityError + +_INSER_METADATA_SQL = "INSERT INTO file_metadata_of_user(user_id,file_key,metadata) "\ +"VALUES(:user_id, :file_key, :metadata)" +_DELETE_METADATA_SQL = "DELETE FROM file_metadata_of_user WHERE user_id=:user_id AND file_key=:file_key" +def insert_metadata(user_id: str, metadata: dict): + db = get_db() + db_cursor = db.cursor() + + delete_params = map(lambda key: {'user_id':user_id, 'file_key':key}, metadata.keys()) + delete_params = list(delete_params) + db_cursor.executemany(_DELETE_METADATA_SQL, delete_params) + + insert_params = map(lambda item: {'user_id':user_id, 'file_key':item[0], 'metadata':item[1]}, metadata.items()) + insert_params = list(insert_params) + + + db_cursor.executemany(_INSER_METADATA_SQL, insert_params) + db.commit() + +def get_metadata(user_id: str): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute("SELECT file_key, metadata FROM file_metadata_of_user WHERE user_id=:user_id",{"user_id":user_id}) + rows = db_cursor.fetchall() + + converted_tuple_array = map(lambda row: (row['file_key'], row['metadata']), rows) + return dict(converted_tuple_array) \ No newline at end of file diff --git a/server/flask/application/backend/data/dao_registration_tokens.py b/server/flask/application/backend/data/dao_registration_tokens.py new file mode 100644 index 0000000..8bb0f1a --- /dev/null +++ b/server/flask/application/backend/data/dao_registration_tokens.py @@ -0,0 +1,34 @@ +from .db import get_db +from .data_models import DataError +from sqlite3 import IntegrityError + +def is_valid_token(token): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute("SELECT COUNT(*) FROM registration_token where token = :token", {"token": token}) + rows = db_cursor.fetchone() + + return rows[0] == 1 + +def insert_token(token): + db = get_db() + db_cursor = db.cursor() + try: + db_cursor.execute("INSERT INTO registration_token(token) VALUES(:token)", {"token": token}) + except IntegrityError as e: + return DataError.REGISTRATION_CODE_ALREADY_EXISTS + db.commit() + +def delete_token(token): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute("DELETE FROM registration_token WHERE token=:token", {"token": token}) + db.commit() + +def get_tokens(): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute("SELECT * FROM registration_token") + rows = db_cursor.fetchall() + + return list(map(lambda row: row['token'],rows)) \ No newline at end of file diff --git a/server/flask/application/backend/data/dao_reset_password_tokens.py b/server/flask/application/backend/data/dao_reset_password_tokens.py new file mode 100644 index 0000000..87961c6 --- /dev/null +++ b/server/flask/application/backend/data/dao_reset_password_tokens.py @@ -0,0 +1,34 @@ +from .db import get_db +import time + +_DELETE_EXPIRED_SESSION_SQL = "DELETE FROM reset_password_token where expires_at <= :time" +def _delete_expired_tokens(db): + db_cursor = db.cursor() + db_cursor.execute(_DELETE_EXPIRED_SESSION_SQL, {"time": time.time()}) + db.commit() + +def is_valid_token(token, username): + db = get_db() + _delete_expired_tokens(db) + db_cursor = db.cursor() + db_cursor.execute("SELECT COUNT(*) FROM reset_password_token where token = :token AND username = :username", {"token": token, "username": username}) + rows = db_cursor.fetchone() + + return rows[0] == 1 + +def insert_token(token, username, expires_at): + db = get_db() + db_cursor = db.cursor() + params = { + "token": token, + "username": username, + "expires_at": expires_at + } + db_cursor.execute("INSERT INTO reset_password_token(token, username, expires_at) VALUES(:token, :username, :expires_at)", params) + db.commit() + +def delete_tokens(username): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute("DELETE FROM reset_password_token WHERE username=:username", {"username": username}) + db.commit() diff --git a/server/flask/application/backend/data/dao_session.py b/server/flask/application/backend/data/dao_session.py new file mode 100644 index 0000000..1e6d648 --- /dev/null +++ b/server/flask/application/backend/data/dao_session.py @@ -0,0 +1,78 @@ +from .db import get_db +from .data_models import Session +import time + +_DELETE_EXPIRED_SESSION_SQL = "DELETE FROM session where refresh_expires_at <= :time" +def _delete_expired_tokens(db): + db_cursor = db.cursor() + db_cursor.execute(_DELETE_EXPIRED_SESSION_SQL, {"time": time.time()}) + db.commit() + +_GET_USER_FOR_ACCESS_TOKEN_SQL = "SELECT user_id FROM session where access_token = :token and access_expires_at >= :time" +def get_user_for_token(access_token: str): + db = get_db() + _delete_expired_tokens(db) + db_cursor = db.cursor() + db_cursor.execute(_GET_USER_FOR_ACCESS_TOKEN_SQL, {"token": access_token, "time": time.time()}) + rows = db_cursor.fetchall() + + if (len(rows) == 1): + return rows[0][0] + return None + +_INSER_SESSION_SQL = "INSERT INTO session(user_id, access_token, refresh_token, access_expires_at, refresh_expires_at)"\ +"VALUES(:user_id, :access_token, :refresh_token, :access_expires_at, :refresh_expires_at)" + +def _session_insert(db_cursor, session: Session): + params = { + "user_id": session.user_id, + "access_token": session.access_token, + "refresh_token": session.refresh_token, + "access_expires_at": session.access_expires_at, + "refresh_expires_at": session.refresh_expires_at, + } + db_cursor.execute(_INSER_SESSION_SQL, params) + +def insert_user_session(session: Session): + db = get_db() + db_cursor = db.cursor() + _session_insert(db_cursor, session) + db.commit() + +def delete_user_session(access_token: str): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute("DELETE FROM session WHERE access_token = :token", {"token": access_token}) + db.commit() + +def delete_all_user_session_by_user_id(user_id: int): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute("DELETE FROM session WHERE user_id = :id", {"id": user_id}) + db.commit() + +def create_new_single_session(session: Session): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute("DELETE FROM session where user_id = :id", {"id": session.user_id}) + _session_insert(db_cursor, session) + db.commit() + +def get_user_for_refresh_token(refresh_token: str): + db = get_db() + _delete_expired_tokens(db) + db_cursor = db.cursor() + db_cursor.execute("SELECT user_id FROM session where refresh_token = :token", {"token": refresh_token}) + rows = db_cursor.fetchall() + + if (len(rows) == 1): + return rows[0][0] + return None + +def swap_refresh_session(refresh_token: str, session: Session): + db = get_db() + _delete_expired_tokens(db) + db_cursor = db.cursor() + db_cursor.execute("DELETE FROM session WHERE refresh_token = :token", {"token": refresh_token}) + _session_insert(db_cursor, session) + db.commit() \ No newline at end of file diff --git a/server/flask/application/backend/data/dao_users.py b/server/flask/application/backend/data/dao_users.py new file mode 100644 index 0000000..16798e6 --- /dev/null +++ b/server/flask/application/backend/data/dao_users.py @@ -0,0 +1,107 @@ +from .data_models import User +from .data_models import RegisteringUser +from .data_models import DataError +from .db import get_db +from sqlite3 import IntegrityError +from passlib.hash import sha256_crypt + +def _user_from_row(row): + return User( + id = row['id'], + name = row['username'], + otp_secret = row['otp_secret'], + was_otp_verified = row['was_otp_verified'] != 0, + privileged = row['privileged'] != 0, + ) + +def get_user_by_id(user_id: int): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute("SELECT * FROM user where id = :user_id", {"user_id": user_id}) + row = db_cursor.fetchone() + + if (row is None): + return None + + return _user_from_row(row) + +def get_user_by_name(username: str): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute("SELECT * FROM user where username = :name", {"name": username}) + row = db_cursor.fetchone() + + if (row is None): + return None + + return _user_from_row(row) + +def get_user_by_name_and_password(user_name: str, password: str): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute("SELECT * FROM user where username = :user_name", {"user_name": user_name}) + row = db_cursor.fetchone() + + if (row is None): + sha256_crypt.hash('') # do hashing even if no user is found + return None + + is_password_wrong = not sha256_crypt.verify(password, row['password'] or '') + if is_password_wrong: + return None + + return _user_from_row(row) + +def get_users(): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute("SELECT * FROM user") + rows = db_cursor.fetchall() + + return map(_user_from_row, rows) + +_INSER_USER_SQL = "INSERT INTO user(username, password, otp_secret, privileged, was_otp_verified)"\ +"VALUES(:name, :pass, :otp, :privileged, :otp_verified)" +def insert_user(user: RegisteringUser): + db = get_db() + db_cursor = db.cursor() + hashed_password = sha256_crypt.hash(user.password) + + params = { + "name": user.name, + "pass": hashed_password, + "otp": user.otp_secret, + "privileged": 1 if user.privileged else 0, + "otp_verified": 1 if user.was_otp_verified else 0, + } + try: + db_cursor.execute(_INSER_USER_SQL, params) + except IntegrityError as e: + return DataError.USER_NAME_NOT_VALID + db.commit() + return db_cursor.lastrowid + +def update_user_privilige(user_id: int, privileged: bool): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute('UPDATE user SET privileged = :privileged WHERE id=:id',{'id':user_id, 'privileged': privileged}) + db.commit() + +def update_user_otp_verification(user_id: int, was_otp_verified: bool): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute('UPDATE user SET was_otp_verified = :otp_verified WHERE id=:id',{'id':user_id, 'otp_verified': was_otp_verified}) + db.commit() + +def update_user_password(user_id: int, new_password: str): + hashed_password = sha256_crypt.hash(new_password) + db = get_db() + db_cursor = db.cursor() + db_cursor.execute('UPDATE user SET password = :pass WHERE id=:id',{'id':user_id, 'pass': hashed_password}) + db.commit() + +def delete_user_by_id(user_id: int): + db = get_db() + db_cursor = db.cursor() + db_cursor.execute('DELETE FROM user WHERE id=:id',{'id':user_id}) + db.commit() diff --git a/server/flask/application/backend/data/data_models.py b/server/flask/application/backend/data/data_models.py new file mode 100644 index 0000000..5391632 --- /dev/null +++ b/server/flask/application/backend/data/data_models.py @@ -0,0 +1,106 @@ +from enum import Enum +from enum import IntEnum + +class Session: + def __init__(self, user_id, access_token, refresh_token, access_expires_at, refresh_expires_at): + self.user_id = user_id + self.access_token = access_token + self.refresh_token = refresh_token + self.access_expires_at = access_expires_at + self.refresh_expires_at = refresh_expires_at + + def __eq__(self, other): + if not isinstance(other, Session): + return False + return self.user_id == other.user_id \ + and self.access_token == other.access_token \ + and self.refresh_token == other.refresh_token \ + and self.access_expires_at == other.access_expires_at \ + and self.refresh_expires_at == other.refresh_expires_at \ + + def __str__(self): + return 'Session(user_id={},access_token={},refresh_token={},access_expires_at={},refresh_expires_at={})'.format(self.user_id, self.access_token, self.refresh_token, self.access_expires_at, self.refresh_expires_at) + + def __repr__(self): + return self.__str__() + +class RegisteringUser: + def __init__(self, name, password, otp_secret, privileged = False, was_otp_verified = False): + self.name = name + self.password = password + self.otp_secret = otp_secret + self.privileged = privileged + self.was_otp_verified = was_otp_verified + + def __eq__(self, other): + if not isinstance(other, User): + return False + + return self.name == other.name \ + and self.password == other.password \ + and self.otp_secret == other.otp_secret \ + and self.privileged == other.privileged \ + and self.was_otp_verified == other.was_otp_verified \ + + def __str__(self): + return 'User(name={},pass={},otp={},privileged={},otp_active={})'\ + .format(self.name, self.password, self.otp_secret, self.privileged, self.was_otp_verified) + + def __repr__(self): + return self.__str__() + +class User: + def __init__(self, id, name, otp_secret, privileged, was_otp_verified): + self.id = id + self.name = name + self.otp_secret = otp_secret + self.privileged = privileged + self.was_otp_verified = was_otp_verified + + def __eq__(self, other): + if not isinstance(other, User): + return False + + return self.id == other.id \ + and self.name == other.name \ + and self.otp_secret == other.otp_secret \ + and self.privileged == other.privileged \ + and self.was_otp_verified == other.was_otp_verified \ + + def __str__(self): + return 'User(id={},name={},otp={},privileged={},otp_active={})'\ + .format(self.id, self.name, self.otp_secret, self.privileged, self.was_otp_verified) + + def __repr__(self): + return self.__str__() + +class DataError(Enum): + USER_NAME_NOT_VALID = 1 + REGISTRATION_CODE_ALREADY_EXISTS = 2 + +class ResponseCode(IntEnum): + SUCCESS_FOUND_USER = 200 + SUCCESS_SAVED_PASSWORD = 201 + SUCCESS_SAVED_USER_FILE_METADATA = 202 + SUCCESS_SAVED_FILE_METADATA = 203 + SUCCESS_SAVED_REGISTRATION_TOKEN = 205 + SUCCESS_DELETED_USER = 206 + SUCCESS_RESET_OTP_VERIFICATION = 207 + SUCCESS_SAVED_RESET_PASSWORD_TOKEN = 208 + SUCCESS_DELETED_TOKEN = 209 + + + ALREADY_TAKEN_USERNAME = 411 + NOT_FOUND_USER = 412 + INVALID_USERNAME_TO_EDIT = 413 + CANT_SAVE_USER_FILE_METADATA = 414 + CANT_SAVE_FILE_METADATA = 415 + INVALID_FILE_KEY = 416 + INVALID_PASSWORD = 421 + INVALID_NEW_PASSWORD = 422 + UNKNOWN_REGISTRATION_TOKEN = 430 + INVALID_OTP = 431 + INVALID_REFRESH_TOKEN = 450 + INVALID_RESET_PASSWORD_TOKEN = 459 + INVALID_REGISTRATION_TOKEN = 460 + UNKNOWN_RESET_PASSWORD_TOKEN = 461 \ No newline at end of file diff --git a/server/flask/application/backend/data/db.py b/server/flask/application/backend/data/db.py new file mode 100644 index 0000000..6c85a87 --- /dev/null +++ b/server/flask/application/backend/data/db.py @@ -0,0 +1,82 @@ +import sqlite3 +from os import path +import argparse +from flask import current_app, g +from passlib.hash import sha256_crypt + +default_database_name = "sqlitedb" + +def get_db(): + current_app.config.get('DATABASE_PATH') + if 'db' not in g: + db_path = current_app.config.get('DATABASE_PATH') + if (db_path is None): + db_path = path.join(current_app.instance_path, current_app.config['DATABASE_NAME']) + g.db = sqlite3.connect(db_path, detect_types=sqlite3.PARSE_DECLTYPES) + g.db.row_factory = sqlite3.Row + + return g.db + +def close_db(e=None): + db = g.pop('db', None) + + if db is not None: + db.close() + +def init_db(db_path = None, schema_path = None): + if db_path is None: + db = get_db() + else: + db = sqlite3.connect(db_path, detect_types=sqlite3.PARSE_DECLTYPES) + + if schema_path is None: + with current_app.open_resource('data/schema.sql') as f: + script = f.read().decode('UTF-8') + else: + with open(schema_path, "r") as f: + script = f.read() + + db.executescript(script) + db.commit() + db.close() + +def init_app(app): + app.teardown_appcontext(close_db) + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="DB Init ArgumentParser", formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument("-u", "--username", type=str, help="username of adming user", required=True) + parser.add_argument("-p", "--password", type=str, help="password of adming user", required=True) + parser.add_argument("-s", "--otp-secret", type=str, help="otp secret of admin user, python pyotp.random_base32()", required=False) + args = parser.parse_args() + config = vars(args) + username = config['username'] + password = config['password'] + otp_secret = config['otp_secret'] + if (otp_secret is None): + import pyotp + otp_secret = pyotp.random_base32() + + db_path = path.join('/server/instance/', default_database_name) + if path.exists(db_path): + print('Database already exists at {}. Will NOT override, if necessary first delete first then restart initialization!'.format(db_path)) + exit(1) + schema_path = path.join(path.dirname(__file__), 'schema.sql') + init_db(db_path = db_path, schema_path = schema_path) + db = sqlite3.connect(db_path, detect_types=sqlite3.PARSE_DECLTYPES) + db.row_factory = sqlite3.Row + db_cursor = db.cursor() + + hashed_password = sha256_crypt.hash(password) + sql = "INSERT INTO user(username, password, otp_secret, privileged, was_otp_verified)"\ + "VALUES(:name, :pass, :otp, :privileged, :otp_verified)" + params = { + "name": username, + "pass": hashed_password, + "otp": otp_secret, + "privileged": True, + "otp_verified": False, + } + db_cursor.execute(sql, params) + db.commit() + db.close() \ No newline at end of file diff --git a/server/flask/application/backend/data/schema.sql b/server/flask/application/backend/data/schema.sql new file mode 100644 index 0000000..f79bbc7 --- /dev/null +++ b/server/flask/application/backend/data/schema.sql @@ -0,0 +1,47 @@ +DROP TABLE IF EXISTS user; +DROP TABLE IF EXISTS registration_token; +DROP TABLE IF EXISTS reset_password_token; +DROP TABLE IF EXISTS session; +DROP TABLE IF EXISTS file_metadata_of_user; + +CREATE TABLE user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + username TEXT UNIQUE NOT NULL, + password TEXT NOT NULL, + otp_secret TEXT NOT NULL, + privileged INTEGER NOT NULL, + was_otp_verified INTEGER NOT NULL +); + +CREATE TABLE registration_token ( + token TEXT PRIMARY KEY NOT NULL +); + +CREATE TABLE reset_password_token ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + token TEXT NOT NULL, + username TEXT NOT NULL, + expires_at INTEGER NOT NULL +); + +CREATE TABLE session ( + user_id INTEGER NOT NULL, + access_token TEXT NOT NULL, + refresh_token TEXT NOT NULL, + access_expires_at INTEGER NOT NULL, + refresh_expires_at INTEGER NOT NULL, + FOREIGN KEY (user_id) REFERENCES user (id) +); + +CREATE TABLE file_metadata_of_user ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + file_key TEXT NOT NULL, + metadata TEXT NOT NULL +); + +CREATE TABLE file_metadata ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + file_key TEXT NOT NULL, + metadata TEXT NOT NULL +); \ No newline at end of file diff --git a/server/flask/application/backend/flask_project.py b/server/flask/application/backend/flask_project.py new file mode 100644 index 0000000..c1bff39 --- /dev/null +++ b/server/flask/application/backend/flask_project.py @@ -0,0 +1,144 @@ +from os import path + +from flask import Flask +import json +from .data import db as db +from .data.data_models import User +from .require_decorators import require_username_and_password +from .require_decorators import require_user_exists_by_username_and_password +from .require_decorators import requires_session +from .require_decorators import require_otp_verification_after_session +from .require_decorators import require_user_priviliged_after_session +from . import auth_requests as auth_requests_handler +from . import admin_requests as admin_requests_handler +from . import user_action_requests as user_action_requests_handler + +# for chrome to accept the certificate run in console `endCommand(SecurityInterstitialCommandId.CMD_PROCEED)` +# to restart = `uwsgi --ini home-vod-server.ini` like in Dockerimage +def create_app(test_config=None): + app = Flask(__name__) + if (test_config == None): + app.config.from_file('config.json', silent=True, load=json.load) + else: + app.config.from_mapping(test_config) + db.init_app(app) + +# region auth requests + @app.route("/register", methods=['POST']) + @require_username_and_password + def register(username, password): + return auth_requests_handler.handle_register(username = username, password = password) + + @app.route("/otp_verification", methods=['POST']) + @require_username_and_password + @require_user_exists_by_username_and_password + def otp_verification(user: User): + return auth_requests_handler.handle_otp_verification(user = user) + + @app.route("/login", methods=['POST']) + @require_username_and_password + @require_user_exists_by_username_and_password + def login(user: User): + return auth_requests_handler.handle_login(user = user) + + @app.route("/logout", methods=['POST']) + def logout(): + return auth_requests_handler.handle_logout() + + @app.route("/refresh/token", methods=['POST']) + def refresh_token(): + return auth_requests_handler.handle_refresh_token() +# endregion + +# region user_actions + @app.route("/user/is_privileged", methods=['GET']) + @requires_session + def get_is_user_priviliged(user: User): + return user_action_requests_handler.handle_get_is_user_priviliged(user = user) + + @app.route("/change/password", methods=['POST']) + @requires_session + @require_otp_verification_after_session + def change_password(user: User): + return user_action_requests_handler.handle_change_password(user = user) + + @app.route("/reset/password", methods=['POST']) + @require_username_and_password + def reset_password(username, password): + return user_action_requests_handler.handle_reset_password(username = username, password = password) + + @app.route("/user/file/metadata", methods=['POST']) + @requires_session + def add_user_file_data(user: User): + return user_action_requests_handler.handle_add_user_file_data(user = user) + + @app.route("/user/file/metadata", methods=['GET']) + @requires_session + def get_user_file_data(user: User): + return user_action_requests_handler.handle_get_user_file_data(user = user) + + @app.route("/file/metadata", methods=['POST']) + @requires_session + def add_file_metadata(user: User): + return user_action_requests_handler.handle_add_file_metadata(user = user) + + @app.route("/file/metadata", methods=['GET']) + @requires_session + def get_file_metadata(user: User): + return user_action_requests_handler.handle_get_file_metadata(user = user) +# endregion + +# region admin requests + @app.route("/admin/registration_token", methods=['POST']) + @requires_session + @require_otp_verification_after_session + @require_user_priviliged_after_session + def create_registration_token(user: User): + return admin_requests_handler.handle_create_registration_token(user = user) + + @app.route("/admin/reset_password_token", methods=['POST']) + @requires_session + @require_otp_verification_after_session + @require_user_priviliged_after_session + def create_reset_password_token(user: User): + return admin_requests_handler.handle_create_reset_password_token(user = user) + + @app.route("/admin/reset_otp_verification", methods=['POST']) + @requires_session + @require_otp_verification_after_session + @require_user_priviliged_after_session + def reset_user_otp_verification(user: User): + return admin_requests_handler.handle_reset_user_otp_verification(user = user) + + @app.route("/admin/get_users", methods=['GET']) + @requires_session + @require_user_priviliged_after_session + def get_users(user: User): + return admin_requests_handler.handle_get_users(user = user) + + @app.route("/admin/get_registration_tokens", methods=['GET']) + @requires_session + @require_user_priviliged_after_session + def get_registration_tokens(user: User): + return admin_requests_handler.handle_get_registration_tokens(user = user) + + @app.route("/admin/delete/user", methods=['POST']) + @requires_session + @require_otp_verification_after_session + @require_user_priviliged_after_session + def delete_user_by_name(user: User): + return admin_requests_handler.handle_delete_user_by_name(user = user) + + @app.route("/admin/delete/registration_token", methods=['POST']) + @requires_session + @require_otp_verification_after_session + @require_user_priviliged_after_session + def delete_registration_token(user: User): + return admin_requests_handler.handle_delete_registration_token(user = user) +# endregion + + return app + +if __name__ == "__main__": + app = create_app() + app.run(host='0.0.0.0') \ No newline at end of file diff --git a/server/flask/application/backend/require_decorators.py b/server/flask/application/backend/require_decorators.py new file mode 100644 index 0000000..ed9c6ae --- /dev/null +++ b/server/flask/application/backend/require_decorators.py @@ -0,0 +1,108 @@ +from flask import request, jsonify, current_app +import functools +from . import token_generator_util +from .data import dao_users +from .data.data_models import User +from .data import dao_session + +def get_cropped_username(username: str): + max_length = current_app.config['MAX_USERNAME_LENGTH'] + if (isinstance(username, str)): + return username[0:max_length] + else: + return None + +def get_cropped_password(password: str): + max_length = current_app.config['MAX_PASSWORD_LENGTH'] + if (isinstance(password, str)): + return password[0:max_length] + else: + return None + +def get_cropped_otp(otp: str): + max_length = current_app.config['MAX_OTP_LENGTH'] + if (isinstance(otp, str)): + return otp[0:max_length] + else: + return None + +def get_cropped_token(token: str): + max_length = current_app.config['MAX_TOKEN_LENGTH'] + if (isinstance(token, str)): + return token[0:max_length] + else: + return None + +def get_cropped_key(key: str): + max_length = current_app.config['KEY_LENGTH'] + if (isinstance(key, str)): + return key[0:max_length] + else: + return None + +def require_username_and_password(request_processor): + @functools.wraps(request_processor) + def do_require_username_and_password(): + username = request.form.get('username') + if username is None: + errorResponse = jsonify({'message':'Username cannot be empty!','code':410}) + return errorResponse, 400 + + password = request.form.get('password') + if password is None: + errorResponse = jsonify({'message':'Password cannot be empty!','code':420}) + return errorResponse, 400 + return request_processor(get_cropped_username(username), get_cropped_password(password)) + return do_require_username_and_password + +def require_user_exists_by_username_and_password(request_processor): + @functools.wraps(request_processor) + def do_require_user(username, password): + user = dao_users.get_user_by_name_and_password(user_name = username, password = password) + if user is None: + errorResponse = jsonify({'message':'User cannot be found!','code':412}) + return errorResponse, 400 + return request_processor(user) + return do_require_user + +def requires_session(request_processor): + @functools.wraps(request_processor) + def do_require_session(): + access_token = get_cropped_token(request.headers.get('Authorization')) + if (access_token is None): + errorResponse = jsonify({'message':'Missing Authorization!','code':440}) + return errorResponse, 401 + + user_id = dao_session.get_user_for_token(access_token) + if (user_id is None): + errorResponse = jsonify({'message':'Invalid Authorization!','code':441}) + return errorResponse, 401 + + user = dao_users.get_user_by_id(user_id) + if user is None: + errorResponse = jsonify({'message':'Invalid Authorization!','code':442}) + return errorResponse, 400 + return request_processor(user) + return do_require_session + +def require_otp_verification_after_session(request_processor): + @functools.wraps(request_processor) + def do_require_otp(user: User): + one_time_password = get_cropped_otp(request.form.get('otp') or '') + is_otp_ok = token_generator_util.verify_otp(user.otp_secret, one_time_password) + if not is_otp_ok: + errorResponse = jsonify({'message':'Invalid Token!','code':431}) + return errorResponse, 400 + + return request_processor(user) + return do_require_otp + +def require_user_priviliged_after_session(request_processor): + @functools.wraps(request_processor) + def do_require_user_priviliged(user: User): + if not user.privileged: + errorResponse = jsonify({'message':'Not Authorized!','code':460}) + return errorResponse, 400 + return request_processor(user) + + return do_require_user_priviliged \ No newline at end of file diff --git a/server/flask/application/backend/token_generator_util.py b/server/flask/application/backend/token_generator_util.py new file mode 100644 index 0000000..38310b2 --- /dev/null +++ b/server/flask/application/backend/token_generator_util.py @@ -0,0 +1,47 @@ +from secrets import token_urlsafe +from flask import current_app +import time +import pyotp +from .data.data_models import Session + +def _get_byte_count(): + return current_app.config.get('SECRECT_BYTE_COUNT') or 64 + +def _get_access_expires_in(): + return current_app.config.get('SESSION_ACCESS_EXPIRATION_IN_SECONDS') or 86400 + +def _get_refresh_expires_in(): + return current_app.config.get('SESSION_REFRESH_EXPIRATION_IN_SECONDS') or 2*86400 + +def _get_reset_password_expires_in(): + return current_app.config.get('RESET_PASSWORD_EXPIRATION_IN_SECONDS') or 2*86400 + +def generate_session(user_id, byte_count = None, access_expires_in = None, refresh_expires_in = None): + byte_count = byte_count or _get_byte_count() + access_expires_in = access_expires_in or _get_access_expires_in() + refresh_expires_in = refresh_expires_in or _get_refresh_expires_in() + current_time = time.time() + return Session( + user_id = user_id, + access_token = token_urlsafe(byte_count), + refresh_token = token_urlsafe(byte_count), + access_expires_at = access_expires_in + current_time, + refresh_expires_at = refresh_expires_in + current_time, + ) + +def generate_reset_password_expires_at(reset_password_expires_in = None): + current_time = time.time() + reset_password_expires_in = reset_password_expires_in or _get_reset_password_expires_in() + return reset_password_expires_in + current_time + +def generate_otp_secret(): + return pyotp.random_base32() + +def verify_otp(secret, otp_code): + totp = pyotp.TOTP(secret) + timestampNow = time.time() + return totp.verify(otp_code, time.time(), 2) + +def get_url(secret, username): + return pyotp.totp.TOTP(secret).provisioning_uri(name=username,issuer_name='FnivesVOD') + \ No newline at end of file diff --git a/server/flask/application/backend/user_action_requests.py b/server/flask/application/backend/user_action_requests.py new file mode 100644 index 0000000..7680676 --- /dev/null +++ b/server/flask/application/backend/user_action_requests.py @@ -0,0 +1,88 @@ +from flask import request, jsonify +import json +from .require_decorators import get_cropped_password +from .require_decorators import get_cropped_otp +from .require_decorators import get_cropped_key +from . import token_generator_util +from .data import dao_users +from .data import dao_session +from .data import dao_reset_password_tokens +from .data import dao_file_metadata_of_user +from .data import dao_file_metadata +from .data.data_models import User +from .data.data_models import ResponseCode + +def handle_change_password(user: User): + password = get_cropped_password(request.form.get('password')) + if password is None: + errorResponse = jsonify({'message':'Invalid Password!','code':ResponseCode.INVALID_PASSWORD}) + return errorResponse, 400 + + new_password = get_cropped_password(request.form.get('new_password')) + if new_password is None: + errorResponse = jsonify({'message':'New Password cannot be empty!','code':ResponseCode.INVALID_NEW_PASSWORD}) + return errorResponse, 400 + + foundUser = dao_users.get_user_by_name_and_password(user_name = user.name, password = password) + if (foundUser is None): + errorResponse = jsonify({'message':'Invalid Password!','code':ResponseCode.INVALID_PASSWORD}) + return errorResponse, 400 + + session = token_generator_util.generate_session(user.id) + dao_users.update_user_password(user_id = user.id, new_password = new_password) + dao_session.create_new_single_session(session = session) + sessionResponse = jsonify({ + 'access_token': session.access_token, + 'refresh_token': session.refresh_token, + 'expires_at': session.access_expires_at + }) + return sessionResponse, 200 + +def handle_reset_password(username: str, password: str): + reset_password_token = get_cropped_otp(request.form.get('reset_password_token')) + if reset_password_token is None: + errorResponse = jsonify({'message':'Invalid Reset Password Token given!','code':ResponseCode.UNKNOWN_RESET_PASSWORD_TOKEN}) + return errorResponse, 400 + + if dao_reset_password_tokens.is_valid_token(token = reset_password_token, username = username) is False: + errorResponse = jsonify({'message':'Invalid Reset Password Token given!','code':ResponseCode.UNKNOWN_RESET_PASSWORD_TOKEN}) + return errorResponse, 400 + + foundUser = dao_users.get_user_by_name(username = username) + if (foundUser is None): + errorResponse = jsonify({'message':'User cannot be found!','code':ResponseCode.NOT_FOUND_USER}) + return errorResponse, 400 + + dao_users.update_user_password(user_id = foundUser.id, new_password = password) + + dao_reset_password_tokens.delete_tokens(username = username) + + return jsonify({'message':'Password was Saved!','code':ResponseCode.SUCCESS_SAVED_PASSWORD}), 200 + +def handle_get_is_user_priviliged(user: User): + return jsonify({'is_privileged': user.privileged}), 200 + +def handle_add_user_file_data(user: User): + metadata_to_save = request.get_json(force=True, silent = True) + if (metadata_to_save is not None and isinstance(metadata_to_save,dict)): + dao_file_metadata_of_user.insert_metadata(user_id = user.id, metadata = metadata_to_save) + return jsonify({'message': 'User\'s File MetaData Saved!', 'code': ResponseCode.SUCCESS_SAVED_USER_FILE_METADATA}), 200 + return jsonify({'message': 'Couldn\'t save user\'s metadata!', 'code': ResponseCode.CANT_SAVE_USER_FILE_METADATA}), 400 + +def handle_get_user_file_data(user: User): + return jsonify(dao_file_metadata_of_user.get_metadata(user_id = user.id)), 200 + + +def handle_add_file_metadata(user: User): + metadata_to_save = request.get_json(force=True, silent = True) + if (metadata_to_save is not None and isinstance(metadata_to_save,dict)): + dao_file_metadata.insert_metadata(metadata = metadata_to_save) + return jsonify({'message': 'File MetaData Saved!', 'code': ResponseCode.SUCCESS_SAVED_FILE_METADATA}), 200 + return jsonify({'message': 'Couldn\'t save metadata!', 'code': ResponseCode.CANT_SAVE_FILE_METADATA}), 400 + + +def handle_get_file_metadata(user: User): + file_key = get_cropped_key(request.args.get('file_key')) + if (file_key is None): + return jsonify({'message': 'Invalid FileKey (file_key)!', 'code': ResponseCode.INVALID_FILE_KEY}), 400 + return jsonify(dao_file_metadata.get_metadata(file_key = file_key)), 200 \ No newline at end of file diff --git a/server/flask/application/flask-wsgi.py b/server/flask/application/flask-wsgi.py new file mode 100644 index 0000000..5c0a3e3 --- /dev/null +++ b/server/flask/application/flask-wsgi.py @@ -0,0 +1,5 @@ +from backend.flask_project import create_app + +app = create_app() +if __name__ == "__main__": + app.run() \ No newline at end of file diff --git a/server/flask/application/home-vod-server.conf b/server/flask/application/home-vod-server.conf new file mode 100644 index 0000000..e935f86 --- /dev/null +++ b/server/flask/application/home-vod-server.conf @@ -0,0 +1,11 @@ +description "uWSGI instance to serve home-vod-server" + +start on runlevel [2345] +stop on runlevel [!2345] + +setgid www-data + +script + cd $HOME/server + uwsgi --ini home-vod-server.ini + end-script \ No newline at end of file diff --git a/server/flask/application/home-vod-server.ini b/server/flask/application/home-vod-server.ini new file mode 100644 index 0000000..7e6a61e --- /dev/null +++ b/server/flask/application/home-vod-server.ini @@ -0,0 +1,11 @@ +[uwsgi] +module = flask-wsgi:app + +master = true +processes = 5 + +socket = /tmp/myapp.sock +chmod-socket = 666 +vacuum = true + +die-on-term = true \ No newline at end of file diff --git a/server/flask/application/nginx-proxy-config b/server/flask/application/nginx-proxy-config new file mode 100644 index 0000000..9b15d91 --- /dev/null +++ b/server/flask/application/nginx-proxy-config @@ -0,0 +1,64 @@ +# rate limiting: https://www.nginx.com/blog/rate-limiting-nginx/ +limit_req_zone $binary_remote_addr zone=ip:10m rate=5r/s; +limit_req_zone $binary_remote_addr zone=restricted_ip:10m rate=10r/m; + +# http server +server { + server_name _; + listen 8080 default_server; + return 404; +} + +# https server +server { + listen 443 ssl; + server_name home_vod_server; + + ssl_certificate /certificate/cert.pem; + ssl_certificate_key /certificate/key.pem; + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256; + ssl_prefer_server_ciphers on; + + root /server; + + # static media + location /media { + root /media-data/; + autoindex on; + auth_request /user/is_privileged; + limit_req zone=ip burst=12 delay=8; + + # enable cache + expires 1d; + add_header Cache-Control "public, no-transform"; + + # kill cache + # add_header Last-Modified $date_gmt; + # add_header Cache-Control 'no-store, no-cache'; + # if_modified_since off; + # expires off; + # etag off; + } + + # flask server + location / { + include uwsgi_params; + uwsgi_pass unix:///tmp/myapp.sock; + limit_req zone=ip burst=12 delay=8; + } + + # flask server login + location /login { + include uwsgi_params; + uwsgi_pass unix:///tmp/myapp.sock; + limit_req zone=restricted_ip burst=4; + } + + # flask server otp_verification + location /otp_verification { + include uwsgi_params; + uwsgi_pass unix:///tmp/myapp.sock; + limit_req zone=restricted_ip burst=4; + } +} \ No newline at end of file diff --git a/server/flask/application/start.sh b/server/flask/application/start.sh new file mode 100755 index 0000000..d5bef24 --- /dev/null +++ b/server/flask/application/start.sh @@ -0,0 +1 @@ +uwsgi --ini home-vod-server.ini diff --git a/server/flask/application/test/__init__.py b/server/flask/application/test/__init__.py new file mode 100644 index 0000000..fd09fc7 --- /dev/null +++ b/server/flask/application/test/__init__.py @@ -0,0 +1,5 @@ +# import os, sys + +# currentDir = os.path.dirname(__file__) +# parentDir = os.path.join(currentDir, '..') +# sys.path.append(os.path.abspath(parentDir)) \ No newline at end of file diff --git a/server/flask/application/test/backend/__init__.py b/server/flask/application/test/backend/__init__.py new file mode 100644 index 0000000..f1233e1 --- /dev/null +++ b/server/flask/application/test/backend/__init__.py @@ -0,0 +1,5 @@ +import os, sys + +currentDir = os.path.dirname(__file__) +parentDir = os.path.join(currentDir, '..') +sys.path.append(os.path.abspath(parentDir)) diff --git a/server/flask/application/test/backend/test_token_generator_util.py b/server/flask/application/test/backend/test_token_generator_util.py new file mode 100644 index 0000000..789ebcf --- /dev/null +++ b/server/flask/application/test/backend/test_token_generator_util.py @@ -0,0 +1,78 @@ +import context +import unittest +import unittest.mock +from copy import copy +from flask import current_app + +import backend.token_generator_util as sut + + +class SessionDAOTest(unittest.TestCase): + + app = context.create_app({**context.default_test_config, "SECRECT_BYTE_COUNT": 52}) + + @unittest.mock.patch('time.time', return_value=1000) + def test_session_generated_is_proper(self, mock_time): + expected = None + user_id = 123 + + with self.app.app_context(): + actual = sut.generate_session(user_id) + + self.assertEqual(20000+1000, actual.access_expires_at) + self.assertEqual(50000+1000, actual.refresh_expires_at) + self.assertEqual(user_id, actual.user_id) + self.assertEqual(70, len(actual.access_token)) + self.assertEqual(70, len(actual.refresh_token)) + + @unittest.mock.patch('time.time', return_value=1000) + def test_two_sessions_are_not_equal(self, mock_time): + expected = None + user_id = 123 + + with self.app.app_context(): + session1 = sut.generate_session(user_id) + session2 = sut.generate_session(user_id) + + session1Copy = copy(session1) + self.assertEqual(session1Copy, session1) + self.assertIsNot(session1Copy, session1) + self.assertNotEqual(session1, session2) + + @unittest.mock.patch('time.time', return_value=1000) + def test_two_sessions_are_not_equal(self, mock_time): + # how to get values = pyotp.TOTP('base32secret3232').at(1000+90) + secret = 'base32secret3232' + nowCode = 585501 + nowMinus30SecCode = 572292 + nowMinus60SecCode = 512128 + nowMinus90SecCode = 440932 + nowPlus30SecCode = 602066 + nowPlus60SecCode = 893795 + nowPlus90SecCode = 11418 + with self.app.app_context(): + actual = sut.verify_otp(secret, nowCode) + actual30SecBefore = sut.verify_otp(secret, nowMinus30SecCode) + actual60SecBefore = sut.verify_otp(secret, nowMinus60SecCode) + actual90SecBefore = sut.verify_otp(secret, nowMinus90SecCode) + actual30SecAfter = sut.verify_otp(secret, nowPlus30SecCode) + actual60SecAfter = sut.verify_otp(secret, nowPlus60SecCode) + actual90SecAfter = sut.verify_otp(secret, nowPlus90SecCode) + + self.assertEqual(True, actual) + self.assertEqual(True, actual30SecBefore) + self.assertEqual(True, actual60SecBefore) + self.assertEqual(True, actual30SecAfter) + self.assertEqual(True, actual60SecAfter) + self.assertEqual(False, actual90SecBefore) + self.assertEqual(False, actual90SecAfter) + + def test_url_is_proper(self): + secret = 'base32secret3232' + actual = sut.get_url(secret=secret, username='admin') + # URL can be verified by using text->QR code on https://www.the-qrcode-generator.com/ + self.assertEqual('otpauth://totp/FnivesVOD:admin?secret=base32secret3232&issuer=FnivesVOD', actual) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/server/flask/application/test/context.py b/server/flask/application/test/context.py new file mode 100644 index 0000000..355ea1f --- /dev/null +++ b/server/flask/application/test/context.py @@ -0,0 +1,13 @@ +from backend.flask_project import create_app + +default_test_config = { + "DATABASE_PATH": "testdb", + "SESSION_ACCESS_EXPIRATION_IN_SECONDS": 20000, + "SESSION_REFRESH_EXPIRATION_IN_SECONDS": 50000, + "DATABASE_PATH": "testdb", + "MAX_PASSWORD_LENGTH": 64, + "MAX_USERNAME_LENGTH": 64, + "MAX_OTP_LENGTH": 16, + "MAX_TOKEN_LENGTH": 200, + "KEY_LENGTH": 30, +} \ No newline at end of file diff --git a/server/flask/application/test/data/__init__.py b/server/flask/application/test/data/__init__.py new file mode 100644 index 0000000..7cf4fc0 --- /dev/null +++ b/server/flask/application/test/data/__init__.py @@ -0,0 +1,5 @@ +import os, sys + +currentDir = os.path.dirname(__file__) +parentDir = os.path.join(currentDir, '..') +sys.path.append(os.path.abspath(parentDir)) \ No newline at end of file diff --git a/server/flask/application/test/data/test_dao_registration_tokens.py b/server/flask/application/test/data/test_dao_registration_tokens.py new file mode 100644 index 0000000..ed95186 --- /dev/null +++ b/server/flask/application/test/data/test_dao_registration_tokens.py @@ -0,0 +1,75 @@ +import os,sys +sys.path.append('../') +import context +import unittest +import unittest.mock +import time +from flask import current_app +import backend.data.db as db +from backend.data.data_models import DataError + +import backend.data.dao_registration_tokens as sut + +class RegistrationTokenDAOTest(unittest.TestCase): + + app = context.create_app(context.default_test_config) + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def test_empty_db_contains_no_token(self): + token = "token" + + with self.app.app_context(): + actual = sut.is_valid_token(token) + + self.assertEqual(False, actual) + + def test_inserted_token_is_found(self): + token = "token" + + with self.app.app_context(): + sut.insert_token(token) + actual = sut.is_valid_token(token) + + self.assertEqual(True, actual) + + def test_same_token_cannot_be_inserted_twice(self): + token = "token" + + with self.app.app_context(): + sut.insert_token(token) + result = sut.insert_token(token) + + self.assertEqual(DataError.REGISTRATION_CODE_ALREADY_EXISTS, result) + + def test_token_deleted_is_not_found(self): + token = "token" + + with self.app.app_context(): + sut.insert_token(token) + sut.delete_token(token) + actual = sut.is_valid_token(token) + + self.assertEqual(False, actual) + + def test_tokens_can_be_requested(self): + expected = ['token-1', 'token-3'] + + with self.app.app_context(): + sut.insert_token('token-1') + sut.insert_token('token-2') + sut.insert_token('token-3') + sut.delete_token('token-2') + actual = sut.get_tokens() + + self.assertEqual(expected, actual) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/server/flask/application/test/data/test_dao_reset_password_tokens.py b/server/flask/application/test/data/test_dao_reset_password_tokens.py new file mode 100644 index 0000000..6c25031 --- /dev/null +++ b/server/flask/application/test/data/test_dao_reset_password_tokens.py @@ -0,0 +1,83 @@ +import os,sys +sys.path.append('../') +import context +import unittest +import unittest.mock +import time +from flask import current_app +import backend.data.db as db + +import backend.data.dao_reset_password_tokens as sut + +class ResetPasswordTokenDAOTest(unittest.TestCase): + + app = context.create_app(context.default_test_config) + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + @unittest.mock.patch('time.time', return_value=1000) + def test_empty_db_contains_no_token(self, mock_time): + token = "token" + + with self.app.app_context(): + actual = sut.is_valid_token(token = token, username = "") + + self.assertEqual(False, actual) + + @unittest.mock.patch('time.time', return_value=1000) + def test_inserted_token_is_found(self, mock_time): + token = "token" + username = "usr" + + with self.app.app_context(): + sut.insert_token(token = token, username = username, expires_at = 2000) + actual = sut.is_valid_token(token = token, username = username) + + self.assertEqual(True, actual) + + @unittest.mock.patch('time.time', return_value=1000) + def test_inserted_expired_token_is_not_found(self, mock_time): + token = "token" + username = "usr" + + with self.app.app_context(): + sut.insert_token(token = token, username = username, expires_at = 1000) + actual = sut.is_valid_token(token = token, username = username) + + self.assertEqual(False, actual) + + @unittest.mock.patch('time.time', return_value=1000) + def test_token_deleted_is_not_found(self, mock_time): + token = "token" + username = "usr" + + with self.app.app_context(): + sut.insert_token(token = token, username = username, expires_at = 2000) + sut.delete_tokens(username = username) + actual = sut.is_valid_token(token = token, username = username) + + self.assertEqual(False, actual) + + @unittest.mock.patch('time.time', return_value=1000) + def test_only_given_user_tokens_are_deleted(self, mock_time): + token = "token" + username_given = "usr_given" + username = "usr_other" + + with self.app.app_context(): + sut.insert_token(token = token, username = username_given, expires_at = 2000) + sut.insert_token(token = token, username = username, expires_at = 2000) + sut.delete_tokens(username = username_given) + actual = sut.is_valid_token(token = token, username = username) + + self.assertEqual(True, actual) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/server/flask/application/test/data/test_dao_session.py b/server/flask/application/test/data/test_dao_session.py new file mode 100644 index 0000000..982b29b --- /dev/null +++ b/server/flask/application/test/data/test_dao_session.py @@ -0,0 +1,247 @@ +import os,sys +sys.path.append('../') +import context +import unittest +import unittest.mock +import time +from flask import current_app +from backend.data import db +from backend.data.data_models import Session + +import backend.data.dao_session as sut + +# Notes to myself: +# test nees tu start with test +# sys.path.append('../') appends the path so context can be imported +# with self.app.app_context(): is required to have current_app, if no request is running + +class SessionDAOTest(unittest.TestCase): + + app = context.create_app(context.default_test_config) + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def test_empty_db_contains_no_token(self): + expected = None + token = "token" + + with self.app.app_context(): + actual = sut.get_user_for_token(token) + + self.assertEqual(expected, actual) + + @unittest.mock.patch('time.time', return_value=1000) + def test_inserted_token_is_found(self, mock_time): + assert time.time() == 1000 + expected = 13 + token = "token" + session = Session( + user_id = 13, + access_token = token, + refresh_token = "refresh_token", + access_expires_at = 2000, + refresh_expires_at = 4000 + ) + + with self.app.app_context(): + sut.insert_user_session(session) + actual = sut.get_user_for_token(token) + + self.assertEqual(expected, actual) + + @unittest.mock.patch('time.time', return_value=1000) + def test_same_token_results_in_not_found(self, mock_time): + assert time.time() == 1000 + expected = None + token = "token" + session1 = Session( + user_id = 13, + access_token = token, + refresh_token = "refresh_token", + access_expires_at = 2000, + refresh_expires_at = 4000 + ) + session2 = Session( + user_id = 14, + access_token = token, + refresh_token = "refresh_token", + access_expires_at = 2000, + refresh_expires_at = 4000 + ) + + with self.app.app_context(): + sut.insert_user_session(session1) + sut.insert_user_session(session2) + actual = sut.get_user_for_token(token) + + self.assertEqual(expected, actual) + + @unittest.mock.patch('time.time', return_value=1000) + def test_expired_access_token_isnt_returned(self, mock_time): + assert time.time() == 1000 + expected = None + token = "token" + session = Session( + user_id = 13, + access_token = token, + refresh_token = "refresh_token", + access_expires_at = 500, + refresh_expires_at = 2000 + ) + + with self.app.app_context(): + sut.insert_user_session(session) + actual = sut.get_user_for_token(token) + + self.assertEqual(expected, actual) + + @unittest.mock.patch('time.time', return_value=1000) + def test_expired_refresh_token_isnt_returned(self, mock_time): + assert time.time() == 1000 + expected = None + token = "token" + session = Session( + user_id = 13, + access_token = token, + refresh_token = "refresh_token", + access_expires_at = 2500, + refresh_expires_at = 500 + ) + + with self.app.app_context(): + sut.insert_user_session(session) + actual = sut.get_user_for_token(token) + + self.assertEqual(expected, actual) + + @unittest.mock.patch('time.time', return_value=1000) + def test_deleted_session_isnt_returned(self, mock_time): + assert time.time() == 1000 + expected = None + token = "token" + session = Session( + user_id = 13, + access_token = token, + refresh_token = "refresh_token", + access_expires_at = 1500, + refresh_expires_at = 5000 + ) + + with self.app.app_context(): + sut.insert_user_session(session = session) + sut.delete_user_session(access_token = token) + actual = sut.get_user_for_token(access_token = token) + + self.assertEqual(expected, actual) + + @unittest.mock.patch('time.time', return_value=1000) + def test_deleted_all_user_session_isnt_returned(self, mock_time): + assert time.time() == 1000 + expected = None + session1 = Session( + user_id = 13, + access_token = "token1", + refresh_token = "refresh_token1", + access_expires_at = 1500, + refresh_expires_at = 5000 + ) + session2 = Session( + user_id = 13, + access_token = "token2", + refresh_token = "refresh_token2", + access_expires_at = 1500, + refresh_expires_at = 5000 + ) + session_of_different_user = Session( + user_id = 14, + access_token = "token3", + refresh_token = "refresh_token3", + access_expires_at = 1500, + refresh_expires_at = 5000 + ) + + with self.app.app_context(): + sut.insert_user_session(session = session1) + sut.insert_user_session(session = session2) + sut.insert_user_session(session = session_of_different_user) + sut.delete_all_user_session_by_user_id(user_id=13) + actual1 = sut.get_user_for_token(access_token = session1.access_token) + actual2 = sut.get_user_for_token(access_token = session2.access_token) + actual_of_different_user = sut.get_user_for_token(access_token = session_of_different_user.access_token) + + self.assertEqual(expected, actual1) + self.assertEqual(expected, actual2) + self.assertEqual(session_of_different_user.user_id, actual_of_different_user) + + @unittest.mock.patch('time.time', return_value=1000) + def test_after_new_single_session_old_session_is_not_returned(self, mock_time): + assert time.time() == 1000 + token = "token" + new_token = "new_token" + session = Session( + user_id = 13, + access_token = token, + refresh_token = "refresh_token", + access_expires_at = 1500, + refresh_expires_at = 5000 + ) + new_session = session = Session( + user_id = 13, + access_token = new_token, + refresh_token = "refresh_token", + access_expires_at = 1500, + refresh_expires_at = 5000 + ) + expected_old = None + expected_new = 13 + + with self.app.app_context(): + sut.insert_user_session(session = session) + sut.create_new_single_session(session = new_session) + actual_old = sut.get_user_for_token(access_token = token) + actual_new = sut.get_user_for_token(access_token = new_token) + + self.assertEqual(expected_old, actual_old) + self.assertEqual(expected_new, actual_new) + + @unittest.mock.patch('time.time', return_value=1000) + def test_after_swap_refresh_session_old_session_is_not_returned(self, mock_time): + assert time.time() == 1000 + token = "token" + new_token = "new_token" + session = Session( + user_id = 13, + access_token = token, + refresh_token = "refresh_token", + access_expires_at = 1500, + refresh_expires_at = 5000 + ) + new_session = session = Session( + user_id = 13, + access_token = new_token, + refresh_token = "refresh_token2", + access_expires_at = 1500, + refresh_expires_at = 5000 + ) + expected_old = None + expected_new = 13 + + with self.app.app_context(): + sut.insert_user_session(session = session) + sut.swap_refresh_session(refresh_token = session.refresh_token, session = new_session) + actual_old = sut.get_user_for_token(access_token = token) + actual_new = sut.get_user_for_token(access_token = new_token) + + self.assertEqual(expected_old, actual_old) + self.assertEqual(expected_new, actual_new) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/server/flask/application/test/data/test_dao_users.py b/server/flask/application/test/data/test_dao_users.py new file mode 100644 index 0000000..2ad2d6c --- /dev/null +++ b/server/flask/application/test/data/test_dao_users.py @@ -0,0 +1,301 @@ +import os,sys +sys.path.append('../') +import context +import unittest +import unittest.mock +import time +from flask import current_app +from backend.data import db +from backend.data.data_models import RegisteringUser +from backend.data.data_models import User +from backend.data.data_models import DataError + +import backend.data.dao_users as sut + +class RegistrationTokenDAOTest(unittest.TestCase): + + app = context.create_app(context.default_test_config) + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def test_empty_db_contains_no_user(self): + user_id = 1 + + with self.app.app_context(): + actual = sut.get_user_by_id(user_id) + + self.assertEqual(None, actual) + + def test_user_inserted_can_be_found_by_id(self): + inserted = RegisteringUser( + name = "admin", + password = "admin", + otp_secret = "secret", + privileged = True, + was_otp_verified = True + ) + expected = User( + id = 1, + name = "admin", + otp_secret = "secret", + privileged = True, + was_otp_verified = True + ) + + with self.app.app_context(): + user_id = sut.insert_user(inserted) + actual = sut.get_user_by_id(user_id) + + self.assertEqual(expected.id, user_id) + self.assertEqual(expected, actual) + + def test_deleted_user_cannot_be_found(self): + inserted = RegisteringUser( + name = "admin", + password = "admin", + otp_secret = "secret", + privileged = True, + was_otp_verified = True + ) + expected = None + + with self.app.app_context(): + user_id = sut.insert_user(inserted) + sut.delete_user_by_id(user_id) + actual = sut.get_user_by_id(user_id) + + self.assertEqual(expected, actual) + + def test_user_inserted_can_be_found_by_name(self): + inserted = RegisteringUser( + name = "admin", + password = "admin", + otp_secret = "secret", + privileged = True, + was_otp_verified = True + ) + expected = User( + id = 1, + name = "admin", + otp_secret = "secret", + privileged = True, + was_otp_verified = True + ) + + with self.app.app_context(): + user_id = sut.insert_user(inserted) + actual = sut.get_user_by_name('admin') + + self.assertEqual(expected.id, user_id) + self.assertEqual(expected, actual) + + def test_2_user_inserted_can_be_found_by_id(self): + inserted1 = RegisteringUser( + name = "admin", + password = "pass", + otp_secret = "secret", + privileged = True, + was_otp_verified = True + ) + expected1 = User( + id = 1, + name = "admin", + otp_secret = "secret", + privileged = True, + was_otp_verified = True + ) + inserted2 = RegisteringUser( + name = "admin2", + password = "pass", + otp_secret = "secret", + privileged = False, + ) + expected2 = User( + id = 2, + name = "admin2", + otp_secret = "secret", + privileged = False, + was_otp_verified = False + ) + + with self.app.app_context(): + user_id1 = sut.insert_user(inserted1) + actual1 = sut.get_user_by_id(user_id1) + user_id2 = sut.insert_user(inserted2) + actual2 = sut.get_user_by_id(user_id2) + + self.assertEqual(expected1.id, user_id1) + self.assertEqual(expected1, actual1) + self.assertEqual(expected2.id, user_id2) + self.assertEqual(expected2, actual2) + + def test_2_user_inserted_can_be_get(self): + inserted1 = RegisteringUser( + name = "admin", + password = "pass", + otp_secret = "secret", + privileged = True, + was_otp_verified = True + ) + expected1 = User( + id = 1, + name = "admin", + otp_secret = "secret", + privileged = True, + was_otp_verified = True + ) + inserted2 = RegisteringUser( + name = "admin2", + password = "pass", + otp_secret = "secret", + privileged = False, + ) + expected2 = User( + id = 2, + name = "admin2", + otp_secret = "secret", + privileged = False, + was_otp_verified = False + ) + + with self.app.app_context(): + user_id1 = sut.insert_user(inserted1) + user_id2 = sut.insert_user(inserted2) + actual = sut.get_users() + + self.assertEqual([expected1,expected2], list(actual)) + + + def test_user_inserted_can_not_be_found_by_good_name_and_wrong_password(self): + inserted = RegisteringUser( + name = "admin", + password = "pass", + otp_secret = "secret" + ) + with self.app.app_context(): + sut.insert_user(inserted) + actual = sut.get_user_by_name_and_password('admin', 'pass2') + + self.assertEqual(None, actual) + + def test_user_inserted_can_not_be_found_by_wrong_name_and_good_password(self): + inserted = RegisteringUser( + name = "admin", + password = "pass", + otp_secret = "secret" + ) + with self.app.app_context(): + sut.insert_user(inserted) + actual = sut.get_user_by_name_and_password('admin2', 'pass') + + self.assertEqual(None, actual) + + + def test_user_inserted_can_be_found_by_name_and_password(self): + inserted = RegisteringUser( + name = "admin", + password = "pass", + otp_secret = "secret" + ) + expected = User( + id = 1, + name = "admin", + otp_secret = "secret", + privileged = False, + was_otp_verified = False + ) + + with self.app.app_context(): + sut.insert_user(inserted) + actual = sut.get_user_by_name_and_password('admin', 'pass') + + self.assertEqual(expected, actual) + + def test_update_user_privilige(self): + user = RegisteringUser( + name = "admin2", + password = "pass", + otp_secret = "secret" + ) + expected = User( + id = 1, + name = "admin2", + otp_secret = "secret", + privileged = True, + was_otp_verified = False + ) + + with self.app.app_context(): + user_id = sut.insert_user(user) + sut.update_user_privilige(user_id, True) + actual = sut.get_user_by_id(user_id) + + self.assertEqual(expected, actual) + + def test_update_user_otp_verification(self): + user = RegisteringUser( + name = "admin2", + password = "pass", + otp_secret = "secret" + ) + expected = User( + id = 1, + name = "admin2", + otp_secret = "secret", + privileged = False, + was_otp_verified = True + ) + + with self.app.app_context(): + user_id = sut.insert_user(user) + sut.update_user_otp_verification(user_id, True) + actual = sut.get_user_by_id(user_id) + + self.assertEqual(expected, actual) + + def test_insert_user_twice(self): + user = RegisteringUser( + name = "admin2", + password = "pass", + otp_secret = "secret" + ) + + with self.app.app_context(): + sut.insert_user(user) + actual = sut.insert_user(user) + + self.assertEqual(DataError.USER_NAME_NOT_VALID, actual) + + def test_update_user_password(self): + user = RegisteringUser( + name = "admin2", + password = "pass", + otp_secret = "secret" + ) + expected_old = None + expected_new = User( + id = 1, + name = "admin2", + otp_secret = "secret", + privileged = False, + was_otp_verified = False + ) + + with self.app.app_context(): + user_id = sut.insert_user(user) + sut.update_user_password(user_id = user_id, new_password = "alma") + actual_old = sut.get_user_by_name_and_password(user_name = "admin2", password = "pass") + actual_new = sut.get_user_by_name_and_password(user_name = "admin2", password = "alma") + + self.assertEqual(expected_old, actual_old) + self.assertEqual(expected_new, actual_new) + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/server/flask/application/test/test_add_file_metadata.py b/server/flask/application/test/test_add_file_metadata.py new file mode 100644 index 0000000..71c82b2 --- /dev/null +++ b/server/flask/application/test/test_add_file_metadata.py @@ -0,0 +1,238 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_session +from backend.data.data_models import Session +from backend.data.data_models import RegisteringUser + +class AddFileMetadataUnitTest(unittest.TestCase): + + url_path = '/file/metadata' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def test_no_headers_returns_unauthorized(self): + expected = {'message':'Missing Authorization!','code':440} + + response = self.client.post(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_access_token_headers_returns_unauthorized(self): + expected = {'message':'Invalid Authorization!','code':441} + + response = self.client.post(self.url_path, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_expired_access_token_headers_returns_unauthorized(self): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=950, + refresh_expires_at=1050, + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + response = self.client.post(self.url_path, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_sending_non_saved_user_error_is_shown(self): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_authenticated_user_sending_data_shows_success(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'File MetaData Saved!','code':203} + + header = {'Authorization': 'token'} + data = json.dumps({'key': 'value'}) + response = self.client.post(self.url_path, headers = header, data = data, content_type='application/json') + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_authenticated_user_sending_invalid_data_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Couldn\'t save metadata!','code':415} + + header = {'Authorization': 'token'} + data = "" + response = self.client.post(self.url_path, headers = header, data = data, content_type='application/json') + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_authenticated_user_sending_data_can_be_read(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected_of_key_query = {'key': 'value'} + expected_of_key2_query = {'key2':'value2'} + + data = json.dumps({'key': 'value', 'key2': 'value2'}) + header = {'Authorization': 'token'} + self.client.post(self.url_path, headers = header, data = data, content_type='application/json') + get_query_key1 = {'file_key': 'key'} + response_of_key1 = self.client.get(self.url_path, query_string = get_query_key1, headers = header) + get_query_key2 = {'file_key': 'key2'} + response_of_key2 = self.client.get(self.url_path, query_string = get_query_key2, headers = header) + + self.assertEqual(200, response_of_key1.status_code) + actual_response_json = json.loads(response_of_key1.data.decode()) + self.assertEqual(expected_of_key_query, actual_response_json) + + actual_response_json = json.loads(response_of_key2.data.decode()) + self.assertEqual(200, response_of_key2.status_code) + self.assertEqual(expected_of_key2_query, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_authenticated_user_data_can_be_overwritten_and_extended(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'key': 'value1'} + + header = {'Authorization': 'token'} + data = json.dumps({'key': 'value', 'key2': 'value2'}) + self.client.post(self.url_path, headers = header, data = data, content_type='application/json') + data = json.dumps({'key': 'value1', 'key3': 'value3'}) + self.client.post(self.url_path, headers = header, data = data, content_type='application/json') + get_query = {'file_key': 'key'} + response = self.client.get(self.url_path, query_string=get_query, headers = header) + + self.assertEqual(200, response.status_code) + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_authenticated_user_invalid_data_is_not_saved(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'key': 'value'} + + header = {'Authorization': 'token'} + data = json.dumps({'key': 'value', 'key2': 'value2'}) + self.client.post(self.url_path, headers = header, data = data, content_type='application/json') + invalid_data = json.dumps([{'key': 'value1', 'key3': 'value3'}]) + self.client.post(self.url_path, headers = header, data = invalid_data, content_type='application/json') + get_query={'file_key':'key'} + response = self.client.get(self.url_path, query_string = get_query, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_add_file_metadata_of_user.py b/server/flask/application/test/test_add_file_metadata_of_user.py new file mode 100644 index 0000000..fa00722 --- /dev/null +++ b/server/flask/application/test/test_add_file_metadata_of_user.py @@ -0,0 +1,228 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_session +from backend.data.data_models import Session +from backend.data.data_models import RegisteringUser + +class AddFileMetadataOfUserUnitTest(unittest.TestCase): + + url_path = '/user/file/metadata' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def test_no_headers_returns_unauthorized(self): + expected = {'message':'Missing Authorization!','code':440} + + response = self.client.post(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_access_token_headers_returns_unauthorized(self): + expected = {'message':'Invalid Authorization!','code':441} + + response = self.client.post(self.url_path, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_expired_access_token_headers_returns_unauthorized(self): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=950, + refresh_expires_at=1050, + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + response = self.client.post(self.url_path, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_sending_non_saved_user_error_is_shown(self): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_authenticated_user_sending_data_shows_success(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'User\'s File MetaData Saved!','code':202} + + header = {'Authorization': 'token'} + data = json.dumps({'key': 'value'}) + response = self.client.post(self.url_path, headers = header, data = data, content_type='application/json') + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_authenticated_user_sending_invalid_data_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Couldn\'t save user\'s metadata!','code':414} + + header = {'Authorization': 'token'} + data = "" + response = self.client.post(self.url_path, headers = header, data = data, content_type='application/json') + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_authenticated_user_sending_data_can_be_read(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'key': 'value', 'key2':'value2'} + + header = {'Authorization': 'token'} + data = json.dumps({'key': 'value', 'key2': 'value2'}) + self.client.post(self.url_path, headers = header, data = data, content_type='application/json') + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_authenticated_user_data_can_be_overwritten_and_extended(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'key': 'value1', 'key2':'value2', 'key3': 'value3'} + + header = {'Authorization': 'token'} + data = json.dumps({'key': 'value', 'key2': 'value2'}) + self.client.post(self.url_path, headers = header, data = data, content_type='application/json') + data = json.dumps({'key': 'value1', 'key3': 'value3'}) + self.client.post(self.url_path, headers = header, data = data, content_type='application/json') + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_authenticated_user_invalid_data_is_not_saved(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'key': 'value', 'key2':'value2'} + + header = {'Authorization': 'token'} + data = json.dumps({'key': 'value', 'key2': 'value2'}) + self.client.post(self.url_path, headers = header, data = data, content_type='application/json') + invalid_data = json.dumps([{'key': 'value1', 'key3': 'value3'}]) + self.client.post(self.url_path, headers = header, data = invalid_data, content_type='application/json') + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_change_password.py b/server/flask/application/test/test_change_password.py new file mode 100644 index 0000000..465bf22 --- /dev/null +++ b/server/flask/application/test/test_change_password.py @@ -0,0 +1,315 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_session +from backend.data.data_models import Session +from backend.data.data_models import RegisteringUser + +class PasswordChangeUnitTest(unittest.TestCase): + + url_path = '/change/password' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def assertHasSession(self, access_token, user_id): + with self.app.app_context(): + actual = dao_session.get_user_for_token(access_token) + self.assertEqual(user_id, actual) + + def test_no_headers_returns_unauthorized(self): + expected = {'message':'Missing Authorization!','code':440} + + response = self.client.post(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_access_token_headers_returns_unauthorized(self): + expected = {'message':'Invalid Authorization!','code':441} + + response = self.client.post(self.url_path, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_expired_access_token_headers_returns_unauthorized(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=950, + refresh_expires_at=1050, + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + response = self.client.post(self.url_path, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_non_saved_user_error_is_shown(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':442} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_no_otp_token_then_error_is_shown(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Token!','code':431} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_invalid_otp_token_then_error_is_shown(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Token!','code':431} + + data = {'otp': 0} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data=data, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_auth_correct_but_no_password_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'Invalid Password!','code':421} + + data = {'otp': correct_code} + response = self.client.post(self.url_path, data = data, headers = {'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_auth_correct_but_no_new_password_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'New Password cannot be empty!','code':422} + + data = {'otp': correct_code, 'password':'pass'} + response = self.client.post(self.url_path, data=data, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_auth_correct_but_invalid_current_password_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'Invalid Password!','code':421} + + data = {'otp': correct_code, 'password':'pass', 'new_password': 'new_pass'} + response = self.client.post(self.url_path, data=data, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_valid_password_change_results_in_new_session(self, mock_time): + user = RegisteringUser( + name = 'alma', + password = 'pass', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected_keys = {'access_token', 'refresh_token', 'expires_at'} + + correct_code = 585501 #for 1000 and base32secret3232 + data = {'password':'pass', 'new_password': 'pass2', 'otp': correct_code} + response = self.client.post(self.url_path, data=data, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(200, response.status_code) + self.assertEqual(expected_keys, set(actual_response_json.keys())) + self.assertEqual(actual_response_json['expires_at'], 1000+20000) + self.assertHasSession(access_token = actual_response_json['access_token'], user_id = user_id) + + @unittest.mock.patch('time.time', return_value=1000) + def test_after_password_change_old_session_is_invalid(self, mock_time): + user = RegisteringUser( + name = 'alma', + password = 'pass', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + change_pass_data = {'password':'pass', 'new_password': 'pass2', 'otp': correct_code} + response = self.client.post(self.url_path, data=change_pass_data, headers={'Authorization': 'token'}) + json.loads(response.data.decode()) + expected = {'message':'Invalid Authorization!','code':441} + + data = {'password':'pass2', 'new_password': 'pass3', 'otp': correct_code} + response = self.client.post(self.url_path, data=data, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_after_password_change_new_session_is_valid(self, mock_time): + user = RegisteringUser( + name = 'alma', + password = 'pass', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + change_pass_data = {'password':'pass', 'new_password': 'pass2', 'otp': correct_code} + response = self.client.post(self.url_path, data=change_pass_data, headers={'Authorization': 'token'}) + session_response = json.loads(response.data.decode()) + expected_keys = {'access_token', 'refresh_token', 'expires_at'} + + data = {'password':'pass2', 'new_password': 'pass3', 'otp': correct_code} + response = self.client.post(self.url_path, data=data, headers={'Authorization': session_response['access_token']}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(200, response.status_code) + self.assertEqual(expected_keys, set(actual_response_json.keys())) + self.assertEqual(actual_response_json['expires_at'], 1000+20000) + self.assertHasSession(access_token = actual_response_json['access_token'], user_id = user_id) + + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_create_registration_token.py b/server/flask/application/test/test_create_registration_token.py new file mode 100644 index 0000000..da6e922 --- /dev/null +++ b/server/flask/application/test/test_create_registration_token.py @@ -0,0 +1,295 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_registration_tokens +from backend.data import dao_session +from backend.data.data_models import RegisteringUser +from backend.data.data_models import Session + +class CreateRegistrationTokenUnitTest(unittest.TestCase): + + url_path = '/admin/registration_token' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def assertRegistrationTokenIs(self, token: str, is_valid: bool): + with self.app.app_context(): + actual = dao_registration_tokens.is_valid_token(token) + self.assertEqual(is_valid, actual) + + def test_no_headers_returns_unauthorized(self): + expected = {'message':'Missing Authorization!','code':440} + + response = self.client.post(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_access_token_headers_returns_unauthorized(self): + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_expired_access_token_headers_returns_unauthorized(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=950, + refresh_expires_at=1050, + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_non_saved_user_error_is_shown(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':442} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_no_otp_token_then_error_is_shown(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Token!','code':431} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_invalid_otp_token_then_error_is_shown(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Token!','code':431} + + data = {'otp': 0} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data=data, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_correct_data_but_not_priviliged_user_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'Not Authorized!','code':460} + + data = {'otp': correct_code} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data=data, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_no_registration_token_returns_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'Invalid Registration Token given!','code':460} + + data = {'otp': correct_code} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data = data, headers = header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_blank_registration_token_returns_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'Invalid Registration Token given!','code':460} + + data = {'registration_token': ' ', 'otp': correct_code} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data = data, headers = header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + self.assertRegistrationTokenIs(token = ' ', is_valid = False) + + + @unittest.mock.patch('time.time', return_value=1000) + def test_priviliged_user_sending_correct_data_token_is_saved(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'Registration token Saved!','code':205} + + data = {'registration_token': '123456', 'otp': correct_code} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data=data, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + self.assertRegistrationTokenIs(token = '123456', is_valid = True) + + @unittest.mock.patch('time.time', return_value=1000) + def test_priviliged_user_authenticated_sending_same_token_shows_ok(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + data = {'registration_token': '123456', 'otp': correct_code} + header = {'Authorization': 'token'} + self.client.post(self.url_path, data=data, headers = header) + expected = {'message':'Invalid Registration Token given!','code':460} + + response = self.client.post(self.url_path, data=data, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + self.assertRegistrationTokenIs(token = '123456', is_valid = True) + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_create_reset_password_token.py b/server/flask/application/test/test_create_reset_password_token.py new file mode 100644 index 0000000..e67ce4d --- /dev/null +++ b/server/flask/application/test/test_create_reset_password_token.py @@ -0,0 +1,327 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_registration_tokens +from backend.data import dao_session +from backend.data.data_models import RegisteringUser +from backend.data.data_models import Session + +class CreateResetPasswordTokenTest(unittest.TestCase): + + url_path = '/admin/reset_password_token' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def assertResetPasswordTokenIs(self, token: str, is_valid: bool): + with self.app.app_context(): + actual = dao_registration_tokens.is_valid_token(token) + self.assertEqual(is_valid, actual) + + def test_no_headers_returns_unauthorized(self): + expected = {'message':'Missing Authorization!','code':440} + + response = self.client.post(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_access_token_headers_returns_unauthorized(self): + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_expired_access_token_headers_returns_unauthorized(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=950, + refresh_expires_at=1050, + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_non_saved_user_error_is_shown(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':442} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_no_otp_token_then_error_is_shown(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Token!','code':431} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_invalid_otp_token_then_error_is_shown(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Token!','code':431} + + data = {'otp': 0} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data=data, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_correct_data_but_not_priviliged_user_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'Not Authorized!','code':460} + + data = {'otp': correct_code} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data=data, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_no_reset_token_returns_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'Invalid Reset Password Token given!','code':459} + + data = {'otp': correct_code} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data = data, headers = header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_blank_reset_token_returns_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'Invalid Reset Password Token given!','code':459} + + data = {'reset_password_token':' ','otp': correct_code} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data = data, headers = header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_usernametoreset_not_send_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'username_to_reset cannot be empty!','code':413} + + data = {'reset_password_token':'a','otp': correct_code} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data = data, headers = header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_as_authenticated_privilidged_proper_data_then_reset_password_token_is_saved(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'Reset Password token Saved!','code':208} + + data = {'reset_password_token':'a', 'username_to_reset': 'c','otp': correct_code} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data = data, headers = header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_resetted_password_token_can_be_user_to_change_password(self, mock_time): + admin = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + admin_id = self.insert_user(admin) + session = Session( + user_id=admin_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + user = RegisteringUser( + name = 'alma', + password = '', + otp_secret = '' + ) + self.insert_user(user) + correct_code = 585501 #for 1000 and base32secret3232 + reset_token = 'a' + data = {'reset_password_token':reset_token, 'username_to_reset': user.name,'otp': correct_code} + header = {'Authorization': 'token'} + self.client.post(self.url_path, data = data, headers = header) + expected = {'message':'Password was Saved!','code':201} + + reset_passwod_data = {'reset_password_token':reset_token, 'username': user.name,'password': 'a'} + response = self.client.post('/reset/password', data = reset_passwod_data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + +if __name__ == '__main__': + unittest.main(verbosity=1) \ No newline at end of file diff --git a/server/flask/application/test/test_delete_registration_token.py b/server/flask/application/test/test_delete_registration_token.py new file mode 100644 index 0000000..064fed8 --- /dev/null +++ b/server/flask/application/test/test_delete_registration_token.py @@ -0,0 +1,214 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_registration_tokens +from backend.data import dao_session +from backend.data.data_models import RegisteringUser +from backend.data.data_models import Session + +class DeleteRegistrationTokenUnitTest(unittest.TestCase): + + url_path = '/admin/delete/registration_token' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def insert_registration_token(self, token: str): + with self.app.app_context(): + dao_registration_tokens.insert_token(token) + + def assertRegistrationTokenIs(self, token: str, is_valid: bool): + with self.app.app_context(): + actual = dao_registration_tokens.is_valid_token(token) + self.assertEqual(is_valid, actual) + + def test_no_headers_returns_unauthorized(self): + expected = {'message':'Missing Authorization!','code':440} + + response = self.client.post(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_access_token_headers_returns_unauthorized(self): + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_expired_access_token_headers_returns_unauthorized(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=950, + refresh_expires_at=1050, + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_non_saved_user_error_is_shown(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':442} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_no_otp_token_then_error_is_shown(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Token!','code':431} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_invalid_otp_token_then_error_is_shown(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Token!','code':431} + + data = {'otp': 0} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data=data, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_correct_data_but_not_priviliged_user_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'Not Authorized!','code':460} + + data = {'otp': correct_code} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data=data, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_priviliged_user_sending_correct_data_token_is_deleted(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + self.insert_registration_token('123456') + self.insert_registration_token('abcdef') + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'Token deleted!','code':209} + + data = {'registration_token': '123456', 'otp': correct_code} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data=data, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + self.assertRegistrationTokenIs(token = '123456', is_valid = False) + self.assertRegistrationTokenIs(token = 'abcdef', is_valid = True) + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_delete_user.py b/server/flask/application/test/test_delete_user.py new file mode 100644 index 0000000..f4a6ce5 --- /dev/null +++ b/server/flask/application/test/test_delete_user.py @@ -0,0 +1,234 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_registration_tokens +from backend.data import dao_session +from backend.data.data_models import RegisteringUser +from backend.data.data_models import Session + +class DeleteTokenUnitTest(unittest.TestCase): + + url_path = '/admin/delete/user' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def assert_user(self, user_id: str, exists: bool): + with self.app.app_context(): + actual = dao_users.get_user_by_id(user_id) + self.assertEqual(exists, actual is not None) + + def assert_session(self, access_token: str, exists: bool): + with self.app.app_context(): + actual = dao_session.get_user_for_token(access_token) + self.assertEqual(exists, actual is not None) + + def test_no_headers_returns_unauthorized(self): + expected = {'message':'Missing Authorization!','code':440} + + response = self.client.post(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_access_token_headers_returns_unauthorized(self): + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_expired_access_token_headers_returns_unauthorized(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=950, + refresh_expires_at=1050, + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_non_saved_user_error_is_shown(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':442} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_no_otp_token_then_error_is_shown(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Token!','code':431} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_invalid_otp_token_then_error_is_shown(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Token!','code':431} + + data = {'otp': 0} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data=data, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_correct_data_but_not_priviliged_user_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'Not Authorized!','code':460} + + data = {'username_to_delete': 'guest-2', 'otp': correct_code} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data=data, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_priviliged_user_sending_correct_data_user_is_deleted(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + guest1_user_id=self.insert_user(RegisteringUser(name = 'guest-1',password = '123',otp_secret = '',)) + guest2_user_id=self.insert_user(RegisteringUser(name = 'guest-2',password = '123',otp_secret = '',)) + guest1_session = Session( + user_id=guest1_user_id, + access_token='a-1', + refresh_token='r-1', + access_expires_at=2000, + refresh_expires_at=3000 + ) + guest2_session = Session( + user_id=guest2_user_id, + access_token='a-2', + refresh_token='r-2', + access_expires_at=2000, + refresh_expires_at=3000 + ) + self.insert_session(guest1_session) + self.insert_session(guest2_session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'User deleted!','code':206} + + data = {'username_to_delete': 'guest-2', 'otp': correct_code} + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, data=data, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + self.assert_user(user_id = guest1_user_id, exists = True) + self.assert_user(user_id = guest2_user_id, exists = False) + self.assert_session(access_token = 'a-1', exists = True) + self.assert_session(access_token = 'a-2', exists = False) + + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_get_file_metadata.py b/server/flask/application/test/test_get_file_metadata.py new file mode 100644 index 0000000..d74385a --- /dev/null +++ b/server/flask/application/test/test_get_file_metadata.py @@ -0,0 +1,187 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_session +from backend.data.data_models import Session +from backend.data.data_models import RegisteringUser + +class GetFileMetadataOfUserUnitTest(unittest.TestCase): + + url_path = '/file/metadata' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def test_no_headers_returns_unauthorized(self): + expected = {'message':'Missing Authorization!','code':440} + + response = self.client.get(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_access_token_headers_returns_unauthorized(self): + expected = {'message':'Invalid Authorization!','code':441} + + response = self.client.get(self.url_path, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_expired_access_token_headers_returns_unauthorized(self): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=950, + refresh_expires_at=1050, + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + response = self.client.get(self.url_path, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_sending_non_saved_user_error_is_shown(self): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_authenticated_user_without_parameter_error_is_shown(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message': 'Invalid FileKey (file_key)!', 'code': 416} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_authenticated_user_with_no_data_returns_empty(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {} + + header = {'Authorization': 'token'} + get_query = {'file_key': 'a'} + response = self.client.get(self.url_path, headers = header, query_string = get_query) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_one_users_saved_data_other_can_access_it(self, mock_time): + user_with_metadata = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_with_metadata_id = self.insert_user(user_with_metadata) + usersession_with_metadata = Session( + user_id=user_with_metadata_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(usersession_with_metadata) + + user = RegisteringUser( + name = 'other', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token2', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'key': 'value'} + + post_header = {'Authorization': 'token'} + data = json.dumps({'key': 'value'}) + self.client.post(self.url_path, headers = post_header, data = data, content_type='application/json') + + get_header = {'Authorization': 'token2'} + get_query = {'file_key': 'key'} + response = self.client.get(self.url_path, headers = get_header, query_string=get_query, content_type='application/json') + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_get_file_metadata_of_user.py b/server/flask/application/test/test_get_file_metadata_of_user.py new file mode 100644 index 0000000..d38afeb --- /dev/null +++ b/server/flask/application/test/test_get_file_metadata_of_user.py @@ -0,0 +1,159 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_session +from backend.data.data_models import Session +from backend.data.data_models import RegisteringUser + +class GetFileMetadataOfUserUnitTest(unittest.TestCase): + + url_path = '/user/file/metadata' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def test_no_headers_returns_unauthorized(self): + expected = {'message':'Missing Authorization!','code':440} + + response = self.client.get(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_access_token_headers_returns_unauthorized(self): + expected = {'message':'Invalid Authorization!','code':441} + + response = self.client.get(self.url_path, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_expired_access_token_headers_returns_unauthorized(self): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=950, + refresh_expires_at=1050, + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + response = self.client.get(self.url_path, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_sending_non_saved_user_error_is_shown(self): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_authenticated_user_with_no_data_returns_empty(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_one_users_saved_data_other_cant_access_it(self, mock_time): + user_with_metadata = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_with_metadata_id = self.insert_user(user_with_metadata) + usersession_with_metadata = Session( + user_id=user_with_metadata_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(usersession_with_metadata) + + user = RegisteringUser( + name = 'other', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token2', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {} + + post_header = {'Authorization': 'token'} + data = json.dumps({'key': 'value'}) + self.client.post(self.url_path, headers = post_header, data = data, content_type='application/json') + + get_header = {'Authorization': 'token2'} + response = self.client.get(self.url_path, headers = get_header, data = data, content_type='application/json') + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_get_registration_tokens.py b/server/flask/application/test/test_get_registration_tokens.py new file mode 100644 index 0000000..d61a7ff --- /dev/null +++ b/server/flask/application/test/test_get_registration_tokens.py @@ -0,0 +1,154 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_registration_tokens +from backend.data import dao_session +from backend.data.data_models import RegisteringUser +from backend.data.data_models import Session + +class GetRegistrationTokensUnitTest(unittest.TestCase): + + url_path = '/admin/get_registration_tokens' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def inser_registration_token(self, token: str): + with self.app.app_context(): + dao_registration_tokens.insert_token(token) + + def test_no_headers_returns_unauthorized(self): + expected = {'message':'Missing Authorization!','code':440} + + response = self.client.get(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_access_token_headers_returns_unauthorized(self): + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_expired_access_token_headers_returns_unauthorized(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=950, + refresh_expires_at=1050, + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_non_saved_user_error_is_shown(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':442} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_correct_data_but_not_priviliged_user_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Not Authorized!','code':460} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_priviliged_user_with_correct_session_returns_tokens(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + self.inser_registration_token('token-abc-1') + self.inser_registration_token('token-abc-2') + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = { + 'registration_tokens':['token-abc-1','token-abc-2'] + } + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_get_users.py b/server/flask/application/test/test_get_users.py new file mode 100644 index 0000000..51ac202 --- /dev/null +++ b/server/flask/application/test/test_get_users.py @@ -0,0 +1,153 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_session +from backend.data.data_models import RegisteringUser +from backend.data.data_models import Session + +class GetUsersUnitTest(unittest.TestCase): + + url_path = '/admin/get_users' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def test_no_headers_returns_unauthorized(self): + expected = {'message':'Missing Authorization!','code':440} + + response = self.client.get(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_access_token_headers_returns_unauthorized(self): + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_expired_access_token_headers_returns_unauthorized(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=950, + refresh_expires_at=1050, + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_non_saved_user_error_is_shown(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':442} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_correct_data_but_not_priviliged_user_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Not Authorized!','code':460} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_priviliged_user_with_correct_session_returns_users(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + self.insert_user(RegisteringUser(name = 'guest-1',password = 'citrom',otp_secret = '')) + self.insert_user(RegisteringUser(name = 'guest-2',password = 'citrom',otp_secret = '')) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = { + 'users':[ + {'name': 'banan', 'privileged': True}, + {'name': 'guest-1', 'privileged': False}, + {'name': 'guest-2', 'privileged': False}, + ] + } + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_is_user_priviliged.py b/server/flask/application/test/test_is_user_priviliged.py new file mode 100644 index 0000000..3dd3c94 --- /dev/null +++ b/server/flask/application/test/test_is_user_priviliged.py @@ -0,0 +1,142 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_session +from backend.data.data_models import Session +from backend.data.data_models import RegisteringUser + +class IsUserPriviligedcUnitTest(unittest.TestCase): + + url_path = '/user/is_privileged' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def test_no_headers_returns_unauthorized(self): + expected = {'message':'Missing Authorization!','code':440} + + response = self.client.get(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_access_token_headers_returns_unauthorized(self): + expected = {'message':'Invalid Authorization!','code':441} + + response = self.client.get(self.url_path, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_expired_access_token_headers_returns_unauthorized(self): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=950, + refresh_expires_at=1050, + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + response = self.client.get(self.url_path, headers={'Authorization': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_sending_non_saved_user_error_is_shown(self): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_priviliged_user_sending_correct_data_result_is_shown(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'is_privileged': True} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_non_priviliged_user_sending_correct_data_result_is_shown(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = False + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'is_privileged': False} + + header = {'Authorization': 'token'} + response = self.client.get(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_login.py b/server/flask/application/test/test_login.py new file mode 100644 index 0000000..55d3024 --- /dev/null +++ b/server/flask/application/test/test_login.py @@ -0,0 +1,129 @@ +import os +import unittest +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data.data_models import RegisteringUser + +class LoginUnitTest(unittest.TestCase): + + url_path = '/login' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def test_without_username_error_is_shown(self): + expected = {'message':'Username cannot be empty!','code':410} + + response = self.client.post(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_without_password_error_is_shown(self): + expected = {'message':'Password cannot be empty!','code':420} + + data = {'username': 'myname'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_sending_non_saved_user_error_is_shown(self): + expected = {'message':'User cannot be found!','code':412} + + data = {'username': 'myname', 'password': 'mypass', 'otp': '124'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_sending_non_found_name_by_name_error_is_shown(self): + password = 'mypass' + user = RegisteringUser( + name = "myname", + password = password, + otp_secret = "base32secret3232" + ) + self.insert_user(user) + expected = {'message':'User cannot be found!','code':412} + + data = {'username': 'myname2', 'password': password, 'otp': '124'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_sending_non_found_user_by_password_error_is_shown(self): + username = 'myname' + user = RegisteringUser( + name = username, + password = 'pass', + otp_secret = "base32secret3232" + ) + self.insert_user(user) + expected = {'message':'User cannot be found!','code':412} + + data = {'username': username, 'password': 'pass2', 'otp': '124'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_without_otp_verified_then_secret_is_returned(self): + user = RegisteringUser( + name = "myname", + password = "mypass", + otp_secret = "base32secret3232", + was_otp_verified = False + ) + self.insert_user(user) + expected_keys = {'otp_secret'} + expected_format = r'^otpauth://totp/FnivesVOD:myname\?secret=[^&]*&issuer=FnivesVOD$' + + data = {'username': 'myname', 'password': 'mypass'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(200, response.status_code) + self.assertEqual(expected_keys, set(actual_response_json.keys())) + self.assertRegex(actual_response_json['otp_secret'], expected_format) + + def test_with_otp_verified_success_is_returned(self): + user = RegisteringUser( + name = "myname", + password = "mypass", + otp_secret = "base32secret3232", + was_otp_verified = True + ) + self.insert_user(user) + expected = {'message':'User found!','code':200} + + data = {'username': 'myname', 'password': 'mypass'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_logout.py b/server/flask/application/test/test_logout.py new file mode 100644 index 0000000..c7b09b3 --- /dev/null +++ b/server/flask/application/test/test_logout.py @@ -0,0 +1,71 @@ +import os +import unittest +import unittest.mock +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_session +from backend.data.data_models import Session +from backend.data.data_models import RegisteringUser + +class LogoutUnitTest(unittest.TestCase): + + url_path = '/logout' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def assertSession(self, access_token, user_id): + with self.app.app_context(): + actual_user_id = dao_session.get_user_for_token(access_token) + self.assertEqual(actual_user_id, user_id) + + def test_no_headers_returns_ok(self): + expected = '' + response = self.client.post(self.url_path) + actual_response = response.data.decode() + + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response) + + def test_given_invalid_session_returns_ok(self): + expected = '' + response = self.client.post(self.url_path, headers={'Authorization': 'invalid_token'}) + actual_response = response.data.decode() + + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response) + + @unittest.mock.patch('time.time', return_value=1000) + def test_given_valid_session_returns_and_session_is_invalidated_ok(self, mock_time): + session = Session( + user_id=2, + access_token='access', + refresh_token='refresh', + access_expires_at=1010, + refresh_expires_at=1020 + ) + self.insert_session(session = session) + self.assertSession(access_token = 'access', user_id = 2) + expected = '' + response = self.client.post(self.url_path, headers={'Authorization': 'access'}) + actual_response = response.data.decode() + + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response) + self.assertSession(access_token = 'access', user_id = None) + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_otp_verification.py b/server/flask/application/test/test_otp_verification.py new file mode 100644 index 0000000..4bc582f --- /dev/null +++ b/server/flask/application/test/test_otp_verification.py @@ -0,0 +1,165 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_session +from backend.data.data_models import Session +from backend.data.data_models import RegisteringUser + +class OTP_VerificationUnitTest(unittest.TestCase): + + url_path = '/otp_verification' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def assertHasSession(self, access_token, user_id): + with self.app.app_context(): + actual = dao_session.get_user_for_token(access_token) + self.assertEqual(user_id, actual) + + def assertUserOTPIs(self, user_id, was_otp_verified): + with self.app.app_context(): + actual = dao_users.get_user_by_id(user_id) + self.assertEqual(was_otp_verified, actual.was_otp_verified) + + def test_without_username_error_is_shown(self): + expected = {'message':'Username cannot be empty!','code':410} + + response = self.client.post(self.url_path) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_without_password_error_is_shown(self): + expected = {'message':'Password cannot be empty!','code':420} + + data = {'username': 'myname'} + response = self.client.post(self.url_path, data=data) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_sending_non_saved_user_error_is_shown(self): + expected = {'message':'User cannot be found!','code':412} + + data = {'username': 'myname', 'password': 'mypass', 'otp': '124'} + response = self.client.post(self.url_path, data=data) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_sending_non_found_user_by_name_error_is_shown(self): + password = 'mypass' + user = RegisteringUser( + name = "myname", + password = password, + otp_secret = "base32secret3232" + ) + user_id = self.insert_user(user) + expected = {'message':'User cannot be found!','code':412} + + data = {'username': 'myname2', 'password': password, 'otp': '124'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + self.assertUserOTPIs(user_id = user_id, was_otp_verified = False) + + def test_sending_non_found_user_by_password_error_is_shown(self): + username = 'myname' + user = RegisteringUser( + name = username, + password = 'pass', + otp_secret = "base32secret3232" + ) + user_id = self.insert_user(user) + expected = {'message':'User cannot be found!','code':412} + + data = {'username': username, 'password': 'pass2', 'otp': '124'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + self.assertUserOTPIs(user_id = user_id, was_otp_verified = False) + + def test_without_otp_error_is_shown(self): + user = RegisteringUser( + name = "myname", + password = "mypass", + otp_secret = "base32secret3232" + ) + user_id = self.insert_user(user) + expected = {'message':'Invalid Token!','code':431} + + data = {'username': 'myname', 'password': 'mypass'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + self.assertUserOTPIs(user_id = user_id, was_otp_verified = False) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_invalid_otp_token_then_error_is_shown(self, mock_time): + user = RegisteringUser( + name = "myname", + password = "mypass", + otp_secret = "base32secret3232" + ) + user_id = self.insert_user(user) + invalid_code = 1 + expected = {'message':'Invalid Token!','code':431} + + data = {'username': 'myname', 'password': 'mypass', 'otp': '{}'.format(invalid_code)} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + self.assertUserOTPIs(user_id = user_id, was_otp_verified = False) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_proper_otp_token_then_session_is_returned(self, mock_time): + user = RegisteringUser( + name = "myname", + password = "mypass", + otp_secret = "base32secret3232" + ) + user_id = self.insert_user(user) + correct_code = 585501 #for 1000 and base32secret3232 + expected_keys = {'access_token', 'refresh_token', 'expires_at'} + + data = {'username': 'myname', 'password': 'mypass', 'otp': '{}'.format(correct_code)} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(200, response.status_code) + self.assertEqual(expected_keys, set(actual_response_json.keys())) + self.assertEqual(actual_response_json['expires_at'], 1000+20000) + self.assertHasSession(access_token = actual_response_json['access_token'], user_id = user_id) + self.assertUserOTPIs(user_id = user_id, was_otp_verified = True) + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_refresh_token.py b/server/flask/application/test/test_refresh_token.py new file mode 100644 index 0000000..3800fa6 --- /dev/null +++ b/server/flask/application/test/test_refresh_token.py @@ -0,0 +1,118 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_session +from backend.data.data_models import RegisteringUser +from backend.data.data_models import Session + +class RefreshTokenUnitTest(unittest.TestCase): + + url_path = '/refresh/token' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def assertHasSession(self, access_token, user_id): + with self.app.app_context(): + actual = dao_session.get_user_for_token(access_token) + self.assertEqual(user_id, actual) + + def test_no_refresh_token_returns_error(self): + expected = {'message':'Invalid Refresh Token!','code':450} + + response = self.client.post(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_refresh_token_returns_error(self): + expected = {'message':'Invalid Refresh Token!','code':450} + + response = self.client.post(self.url_path, data={'refresh_token': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_if_token_is_exists_2_times_then_invalid(self, mock_time): + session1 = Session( + user_id = 1, + access_token = "a", + refresh_token = "token", + access_expires_at = 5000, + refresh_expires_at = 5000, + ) + session2 = Session( + user_id = 2, + access_token = "b", + refresh_token = "token", + access_expires_at = 6000, + refresh_expires_at = 6000, + ) + self.insert_session(session1) + self.insert_session(session2) + expected = {'message':'Invalid Refresh Token!','code':450} + + response = self.client.post(self.url_path, data={'refresh_token': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_expired_refresh_token_then_invalid(self, mock_time): + session = Session( + user_id = 1, + access_token = "a", + refresh_token = "token", + access_expires_at = 6000, + refresh_expires_at = 900, + ) + self.insert_session(session) + expected = {'message':'Invalid Refresh Token!','code':450} + + response = self.client.post(self.url_path, data={'refresh_token': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_expired_access_token_but_non_expires_refresh_token_then_new_is_returned(self, mock_time): + session = Session( + user_id = 1, + access_token = "a", + refresh_token = "token", + access_expires_at = 900, + refresh_expires_at = 6000, + ) + self.insert_session(session) + expected_keys = {'access_token', 'refresh_token', 'expires_at'} + + response = self.client.post(self.url_path, data={'refresh_token': 'token'}) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(200, response.status_code) + self.assertEqual(expected_keys, set(actual_response_json.keys())) + self.assertEqual(actual_response_json['expires_at'], 1000+20000) + self.assertHasSession(access_token = actual_response_json['access_token'], user_id = 1) + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_registration.py b/server/flask/application/test/test_registration.py new file mode 100644 index 0000000..fa50196 --- /dev/null +++ b/server/flask/application/test/test_registration.py @@ -0,0 +1,136 @@ +import os,re +import unittest +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_registration_tokens + +class RegistrationUnitTest(unittest.TestCase): + + url_path = '/register' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def save_token(self, token): + with self.app.app_context(): + dao_registration_tokens.insert_token(token) + + def test_without_username_error_is_shown(self): + expected = {'message':'Username cannot be empty!','code':410} + + response = self.client.post(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_without_password_error_is_shown(self): + expected = {'message':'Password cannot be empty!','code':420} + + data = {'username': 'myname'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_without_otp_error_is_shown(self): + expected = {'message':'Invalid Token!','code':430} + + data = {'username': 'myname', 'password': 'mypass'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_sending_non_saved_otp_token_then_error_is_shown(self): + expected = {'message':'Invalid Token!','code':430} + + data = {'username': 'myname', 'password': 'mypass', 'otp': '124'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_sending_saved_token_with_nonexistent_username_then_otp_secret_is_revealed(self): + token = '124' + name = 'myname' + self.save_token(token) + expected_keys = {'otp_secret'} + expected_format = r'^otpauth://totp/FnivesVOD:[^?]*\?secret='+re.escape(name)+r'&issuer=FnivesVOD$' + + data = {'username': 'myname', 'password': 'mypass', 'otp': token} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(200, response.status_code) + self.assertEqual(expected_keys, set(actual_response_json.keys())) + self.assertRegex(actual_response_json['otp_secret'], expected_format) + + def test_reusing_token_doesnt_work(self): + token = '124' + self.save_token(token) + first_user_data = {'username': 'myname', 'password': 'mypass', 'otp': token} + self.client.post(self.url_path, data=first_user_data) + expected = {'message':'Invalid Token!','code':430} + + data = {'username': 'myname2', 'password': 'mypass', 'otp': token} + response = self.client.post(self.url_path, data=data) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_sending_saved_otp_token_but_existing_username_then_error_is_shown(self): + first_user_token = '124' + second_user_token = '125' + same_user_name = 'myname' + self.save_token(first_user_token) + self.save_token(second_user_token) + first_user_data = {'username': same_user_name, 'password': 'mypass', 'otp': first_user_token} + self.client.post(self.url_path, data=first_user_data) + expected = {'message':'Username is already taken!','code':411} + + data = {'username': same_user_name, 'password': 'mypass', 'otp': second_user_token} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_reusing_token_after_existing_user_error_works(self): + first_user_token = '124' + second_user_token = '125' + same_user_name = 'myname' + different_user_name = 'myname2' + self.save_token(first_user_token) + self.save_token(second_user_token) + first_user_data = {'username': same_user_name, 'password': 'mypass', 'otp': first_user_token} + self.client.post(self.url_path, data=first_user_data) + second_user_data = {'username': same_user_name, 'password': 'mypass', 'otp': second_user_token} + self.client.post(self.url_path, data=second_user_data) + expected_keys = {'otp_secret'} + expected_format = r'^otpauth://totp/FnivesVOD:[^?]*\?secret='+re.escape(different_user_name)+r'&issuer=FnivesVOD$' + + data = {'username': different_user_name, 'password': 'mypass', 'otp': second_user_token} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(200, response.status_code) + self.assertEqual(expected_keys, set(actual_response_json.keys())) + self.assertRegex(actual_response_json['otp_secret'], expected_format) + + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_reset_password.py b/server/flask/application/test/test_reset_password.py new file mode 100644 index 0000000..d59123c --- /dev/null +++ b/server/flask/application/test/test_reset_password.py @@ -0,0 +1,204 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_reset_password_tokens +from backend.data import dao_users +from backend.data.data_models import RegisteringUser + +class ResetPasswordUnitTest(unittest.TestCase): + + url_path = '/reset/password' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def save_token(self, token, username, expires_at): + with self.app.app_context(): + dao_reset_password_tokens.insert_token(token=token,username=username,expires_at=expires_at) + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def test_without_username_error_is_shown(self): + expected = {'message':'Username cannot be empty!','code':410} + + response = self.client.post(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_without_password_error_is_shown(self): + expected = {'message':'Password cannot be empty!','code':420} + + data = {'username': 'myname'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_without_reset_password_token_error_is_shown(self): + expected = {'message': 'Invalid Reset Password Token given!','code':461} + + data = {'username': 'myname', 'password': 'mypass'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_without_valid_reset_password_token_error_is_shown(self): + expected = {'message': 'Invalid Reset Password Token given!','code':461} + + data = {'username': 'myname', 'password': 'mypass', 'reset_password_token': 'alma'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_without_saved_reset_password_token_error_is_shown(self): + expected = {'message': 'Invalid Reset Password Token given!','code':461} + + data = {'username': 'myname', 'password': 'mypass', 'reset_password_token': 'alma'} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_with_expired_reset_password_token_error_is_shown(self, mock_time): + expected = {'message': 'Invalid Reset Password Token given!','code':461} + token = 'alma' + username = 'myname' + self.save_token(token = token, username = username, expires_at = 10) + + data = {'username': username, 'password': 'mypass', 'reset_password_token': token} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_with_valid_registration_token_but_no_user_error_is_shown(self, mock_time): + expected = {'message': 'User cannot be found!','code':412} + token = 'alma' + username = 'myname' + self.save_token(token = token, username = username, expires_at = 1100) + + data = {'username': username, 'password': 'mypass', 'reset_password_token': token} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_with_valid_token_and_user_success_is_shown(self, mock_time): + expected = {'message': 'Password was Saved!','code':201} + token = 'alma' + username = 'myname' + self.save_token(token = token, username = username, expires_at = 1100) + user = RegisteringUser( + name = username, + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + + data = {'username': username, 'password': 'mypass', 'reset_password_token': token} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(200, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_reset_password_can_be_used(self, mock_time): + expected_keys = {'otp_secret'} + expected_format = r'^otpauth://totp/FnivesVOD:myname\?secret=[^&]*&issuer=FnivesVOD$' + token = 'alma' + username = 'myname' + self.save_token(token = token, username = username, expires_at = 1100) + user = RegisteringUser( + name = username, + password = 'citrom', + otp_secret = 'base32secret3232' + ) + self.insert_user(user) + data = {'username': username, 'password': 'mypass', 'reset_password_token': token} + self.client.post(self.url_path, data=data) + + login_data = {'username': username, 'password': 'mypass'} + response = self.client.post('/login', data=login_data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(200, response.status_code) + self.assertEqual(expected_keys, set(actual_response_json.keys())) + self.assertRegex(actual_response_json['otp_secret'], expected_format) + + @unittest.mock.patch('time.time', return_value=1000) + def test_same_token_cannot_be_reused(self, mock_time): + expected = {'message': 'Invalid Reset Password Token given!','code':461} + token = 'alma' + username = 'myname' + self.save_token(token = token, username = username, expires_at = 1100) + user = RegisteringUser( + name = username, + password = 'citrom', + otp_secret = 'base32secret3232' + ) + self.insert_user(user) + data = {'username': username, 'password': 'mypass', 'reset_password_token': token} + self.client.post(self.url_path, data=data) + + data = {'username': username, 'password': 'mypass', 'reset_password_token': token} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_no_previous_token_can_be_reused_after_reset(self, mock_time): + expected = {'message': 'Invalid Reset Password Token given!','code':461} + token = 'alma' + token2 = 'alma2' + username = 'myname' + self.save_token(token = token, username = username, expires_at = 1100) + self.save_token(token = token2, username = username, expires_at = 1100) + user = RegisteringUser( + name = username, + password = 'citrom', + otp_secret = 'base32secret3232' + ) + self.insert_user(user) + data = {'username': username, 'password': 'mypass', 'reset_password_token': token} + self.client.post(self.url_path, data=data) + + data = {'username': username, 'password': 'mypass', 'reset_password_token': token2} + response = self.client.post(self.url_path, data=data) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/application/test/test_reset_user_otp_verification.py b/server/flask/application/test/test_reset_user_otp_verification.py new file mode 100644 index 0000000..5a81235 --- /dev/null +++ b/server/flask/application/test/test_reset_user_otp_verification.py @@ -0,0 +1,243 @@ +import os +import unittest +import unittest.mock +import json +from .context import create_app, default_test_config +from backend.data import db +from backend.data import dao_users +from backend.data import dao_registration_tokens +from backend.data import dao_session +from backend.data.data_models import RegisteringUser +from backend.data.data_models import Session + +class ResetUserOTPVerificationTest(unittest.TestCase): + + url_path = '/admin/reset_otp_verification' + app = create_app(default_test_config) + client = app.test_client() + + def setUp(self): + with self.app.app_context(): + db.init_db() + + def tearDown(self): + with self.app.app_context(): + db.close_db() + os.remove("testdb") + + def insert_user(self, user: RegisteringUser): + with self.app.app_context(): + user_id = dao_users.insert_user(user) + return user_id + + def insert_session(self, session: Session): + with self.app.app_context(): + dao_session.insert_user_session(session) + + def inser_registration_token(self, token: str): + with self.app.app_context(): + dao_registration_tokens.insert_token(token) + + def test_no_headers_returns_unauthorized(self): + expected = {'message':'Missing Authorization!','code':440} + + response = self.client.post(self.url_path) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + def test_not_saved_access_token_headers_returns_unauthorized(self): + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_expired_access_token_headers_returns_unauthorized(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=950, + refresh_expires_at=1050, + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':441} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers=header) + actual_response_json = json.loads(response.data.decode()) + + self.assertEqual(401, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_non_saved_user_error_is_shown(self, mock_time): + session = Session( + user_id=2, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'message':'Invalid Authorization!','code':442} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_correct_authentication_but_no_otp_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232' + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + expected = {'code': 431, 'message': 'Invalid Token!'} + + header = {'Authorization': 'token'} + response = self.client.post(self.url_path, headers = header) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_correct_authentication_and_otp_but_not_priviliged_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = False + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'code': 460, 'message': 'Not Authorized!'} + + header = {'Authorization': 'token'} + data = {'otp': '{}'.format(correct_code)} + response = self.client.post(self.url_path, headers = header, data = data) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_correct_privilidged_authentication_and_otp_without_usertoreset_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'code': 413, 'message': 'username_to_reset cannot be empty!'} + + header = {'Authorization': 'token'} + data = {'otp': '{}'.format(correct_code)} + response = self.client.post(self.url_path, headers = header, data = data) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_correct_privilidged_authentication_and_otp_but_nonexistent_usertoreset_shows_error(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'message':'User cannot be found!','code':412} + + header = {'Authorization': 'token'} + data = {'username_to_reset': 'alma', 'otp': '{}'.format(correct_code)} + response = self.client.post(self.url_path, headers = header, data = data) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(400, response.status_code) + self.assertEqual(expected, actual_response_json) + + @unittest.mock.patch('time.time', return_value=1000) + def test_sending_correct_data_otp_verification_is_updated(self, mock_time): + user = RegisteringUser( + name = 'banan', + password = 'citrom', + otp_secret = 'base32secret3232', + privileged = True + ) + user_id = self.insert_user(user) + user_to_reset = RegisteringUser( + name = 'alma', + password = '', + otp_secret = '', + was_otp_verified = True + ) + self.insert_user(user_to_reset) + session = Session( + user_id=user_id, + access_token='token', + refresh_token='', + access_expires_at=1050, + refresh_expires_at=2000 + ) + self.insert_session(session) + correct_code = 585501 #for 1000 and base32secret3232 + expected = {'code': 207, 'message': 'OTP Verification Reset!'} + + header = {'Authorization': 'token'} + data = {'username_to_reset': '{}'.format(user_to_reset.name), 'otp': '{}'.format(correct_code)} + response = self.client.post(self.url_path, headers = header, data = data) + + actual_response_json = json.loads(response.data.decode()) + self.assertEqual(expected, actual_response_json) + self.assertEqual(200, response.status_code) + +if __name__ == '__main__': + unittest.main(verbosity=2) \ No newline at end of file diff --git a/server/flask/setup.sh b/server/flask/setup.sh new file mode 100755 index 0000000..c9b7f9e --- /dev/null +++ b/server/flask/setup.sh @@ -0,0 +1,69 @@ +#!/bin/bash + +if [ -f "certificate/cert.pem" ] && [ -f "certificate/key.pem" ]; then + echo "Certificates found!" +else + echo "Certificates not found! Make sure you have cert.pem and key.pem ready in folder $(pwd)/certificate/" >&2 + echo "To generate certificate use following command: \`openssl req -x509 -newkey rsa:4096 -keyout key.pem -nodes -out cert.pem -sha256 -days 365 -addext \"subjectAltName = IP:\"\`" >&2 + echo "If -addext option doesn't work and you use libressl (to verify use openssl version) checkout https://github.com/libressl/portable/issues/544" + exit 1 +fi + +while getopts ":u:p:m:" opt; do + case $opt in + u) admin_username="$OPTARG" + ;; + p) password="$OPTARG" + ;; + m) mount_folder="$OPTARG" + ;; + \?) echo "Invalid option -$OPTARG" >&2 + exit 1 + ;; + esac +done + +if [ -z "$admin_username" ]; then + echo "-u Admin Username is required! ">&2 + exit 1 +fi +if [ -z "$password" ]; then + echo "-p Admin Password is required! ">&2 + exit 1 +fi +if [ -z "$mount_folder" ]; then + echo "-p Media Folder absolute path is required! ">&2 + exit 1 +fi + +echo "Admin username of backend: $admin_username" +echo "Admin password of backend: $password" +echo "Media folder mounted: $mount_folder" + +IMG_NAME="home-vod-server:1.0" +CONTAINER_NAME="home-vod-server" + +# build docker image if doesn't exists already +if [[ "$(docker images -q $IMG_NAME 2> /dev/null)" == "" ]]; then + echo "Building Docker Image..." + docker build -t $IMG_NAME . +else + echo "Docker image $IMG_NAME already exists, skipping build." +fi + +# delete container if it exists +echo "Deleting existing container($CONTAINER_NAME)..." +docker stop $CONTAINER_NAME 2> /dev/null +docker rm $CONTAINER_NAME 2> /dev/null + +# start container binding the source and media files +echo "Creating container($CONTAINER_NAME)..." +docker run -d --name "$CONTAINER_NAME" -p443:443 --restart=always --mount type=bind,source="$(pwd)"/application,target=/server --mount type=bind,source="$mount_folder",target="/media-data/media",readonly $IMG_NAME + +# initialize database with admin credentials +echo "Initializing database with adming user..." +docker exec $CONTAINER_NAME python backend/data/db.py -u "$admin_username" -p "$password" + +echo "Docker container should be ready to use." +echo "To test server, you may use \`curl -k -d \"username=$admin_username&password=$password\" https://localhost:443/login\`" +echo "To tests use \`docker exec home-vod-server python -m unittest discover -v -s .\`." \ No newline at end of file