flask server

This commit is contained in:
Gergely Hegedus 2023-02-01 23:15:18 +02:00
parent 5acb2992ce
commit 0a71a6c840
54 changed files with 5876 additions and 0 deletions

View file

@ -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

View file

@ -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

View file

@ -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"
}

View file

@ -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)

View file

@ -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)

View file

@ -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))

View file

@ -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()

View file

@ -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()

View file

@ -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()

View file

@ -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

View file

@ -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()

View file

@ -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
);

View file

@ -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')

View file

@ -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

View file

@ -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')

View file

@ -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