From e02bf1aa62d74b5942af1d4e23b7120ecf73086a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torma=20Krist=C3=B3f?= Date: Tue, 24 Nov 2020 20:26:02 +0100 Subject: [PATCH] user handling done --- k8s/videon-backend.yml | 6 +-- requirements.txt | 5 ++- src/app.py | 17 ++++++++- src/config.py | 4 +- src/fbcrypt.py | 13 +++++++ src/jwtman.py | 14 +++++++ src/models.py | 31 +++++++++++++++ src/resources.py | 86 ++++++++++++++++++++++++++++++++++++++++++ src/schemas.py | 38 +++++++++++++++++++ 9 files changed, 207 insertions(+), 7 deletions(-) create mode 100644 src/fbcrypt.py create mode 100644 src/jwtman.py create mode 100644 src/models.py create mode 100644 src/resources.py create mode 100644 src/schemas.py diff --git a/k8s/videon-backend.yml b/k8s/videon-backend.yml index 60bebdf..6d4c3d1 100644 --- a/k8s/videon-backend.yml +++ b/k8s/videon-backend.yml @@ -2,10 +2,10 @@ apiVersion: v1 kind: ConfigMap metadata: - name: input-service + name: backend labels: - app: input-service - namespace: birbnetes + app: backend + namespace: videon data: SENTRY_DSN: https://58bd309272c642d884ff2332c336b977@sentry.kmlabz.com/21 RELEASE_ID: kmlabz-k8s diff --git a/requirements.txt b/requirements.txt index 98c8ffb..7d9b572 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,6 @@ flask sentry-sdk[flask] flask-restful -flask-security kubernetes gunicorn psycopg2-binary @@ -10,4 +9,6 @@ flask_sqlalchemy marshmallow marshmallow-sqlalchemy flask-marshmallow -py-healthcheck \ No newline at end of file +py-healthcheck +flask-jwt-extended +flask-bcrypt \ No newline at end of file diff --git a/src/app.py b/src/app.py index 7cd7bbc..4ee33b1 100644 --- a/src/app.py +++ b/src/app.py @@ -9,8 +9,11 @@ from healthcheck import HealthCheck from config import * from db import db +from jwtman import jwtman +from fbcrypt import bcrypt from marshm import ma from healthchecks import health_database_status +from resources import SignupApi, LoginApi """ Main Flask RESTful API @@ -35,15 +38,19 @@ if SENTRY_DSN: app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] =\ f"postgresql://{POSTGRES_USERNAME}:{POSTGRES_PASSWORD}@{POSTGRES_HOSTNAME}:5432/{POSTGRES_DB}" +app.config['JWT_SECRET_KEY'] = JWT_SECRET_KEY +app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False api = Api(app) health = HealthCheck(app, "/healthz") db.init_app(app) ma.init_app(app) +bcrypt.init_app(app) +jwtman.init_app(app) with app.app_context(): db.create_all() -"" + formatter = logging.Formatter( fmt="%(asctime)s - %(levelname)s - %(module)s - %(message)s" ) @@ -56,9 +63,17 @@ logger.setLevel(logging.DEBUG) logger.addHandler(handler) # api.add_resource(SampleResource, "/sample") +api.add_resource(SignupApi, '/api/auth/signup') +api.add_resource(LoginApi, '/api/auth/login') health.add_check(health_database_status) + +@app.errorhandler(404) +def page_not_found(e): + return {'status': 'error', 'message': 'page not found'}, 404 + + if __name__ == "__main__": app.run( debug=bool(DEBUG), diff --git a/src/config.py b/src/config.py index 90d931c..39785b9 100644 --- a/src/config.py +++ b/src/config.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import os - +from uuid import uuid4 """ Configuration @@ -25,3 +25,5 @@ POSTGRES_HOSTNAME = os.getenv("VIDEON_POSTGRES_HOSTNAME", "localhost") POSTGRES_USERNAME = os.getenv("VIDEON_POSTGRES_USERNAME", "videon") POSTGRES_PASSWORD = os.getenv("VIDEON_POSTGRES_PASSWORD", "videon") POSTGRES_DB = os.getenv("VIDEON_POSTGRES_DB", "videon") + +JWT_SECRET_KEY = os.getenv("VIDEON_POSTGRES_DB", str(uuid4())) diff --git a/src/fbcrypt.py b/src/fbcrypt.py new file mode 100644 index 0000000..68c2785 --- /dev/null +++ b/src/fbcrypt.py @@ -0,0 +1,13 @@ +#!/usr/bin/env python3 +from flask_bcrypt import Bcrypt + +""" +Bcrypt object +""" + +__author__ = '@tormakris' +__copyright__ = "Copyright 2020, videON Team" +__module_name__ = "fbrypt" +__version__text__ = "1" + +bcrypt = Bcrypt() diff --git a/src/jwtman.py b/src/jwtman.py new file mode 100644 index 0000000..a49cc15 --- /dev/null +++ b/src/jwtman.py @@ -0,0 +1,14 @@ +#!/usr/bin/env python3 + +from flask_jwt_extended import JWTManager + +""" +JWTManager +""" + +__author__ = '@tormakris' +__copyright__ = "Copyright 2020, videON Team" +__module_name__ = "jwtman" +__version__text__ = "1" + +jwtman = JWTManager() diff --git a/src/models.py b/src/models.py new file mode 100644 index 0000000..c1aeab1 --- /dev/null +++ b/src/models.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python3 +from sqlalchemy.sql import func +from flask_bcrypt import generate_password_hash, check_password_hash + +from db import db + +""" +Database models +""" + +__author__ = '@tormakris' +__copyright__ = "Copyright 2020, videON Team" +__module_name__ = "models" +__version__text__ = "1" + + +class User(db.Model): + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + + name = db.Column(db.String, nullable=False, unique=True) + password = db.Column(db.String, nullable=False) + + last_logon = db.Column(db.TIMESTAMP, nullable=False, server_default=func.now()) + + timestamp = db.Column(db.TIMESTAMP, nullable=False, server_default=func.now()) + + def hash_password(self): + self.password = generate_password_hash(self.password).decode('utf8') + + def check_password(self, password): + return check_password_hash(self.password, password) diff --git a/src/resources.py b/src/resources.py new file mode 100644 index 0000000..11335bb --- /dev/null +++ b/src/resources.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +import datetime + +from flask_jwt_extended import create_access_token, jwt_required +from flask_restful import Resource +from flask import request, current_app + +from db import db +from models import User +from schemas import UserSchema, UserMetadataSchema + + +""" +Flask Restful endpoints +""" + +__author__ = '@tormakris' +__copyright__ = "Copyright 2020, videON Team" +__module_name__ = "resources" +__version__text__ = "1" + + +class SignupApi(Resource): + """ + See: https://swagger.kmlabz.com/?urls.primaryName=videON%20Backend#/backend/createuser + """ + + userschema = UserSchema(many=False) + usermetadataschema = UserMetadataSchema(many=False) + + def post(self): + body = request.get_json() + + try: + userobj = self.userschema.load(body) + except Exception as e: + current_app.logger.exception(e) + return {'status': 'error', 'message': 'Input JSON schema invalid'}, 417 + + user = User(name=userobj['name'], password=userobj['password']) + try: + user.hash_password() + db.session.add(user) + db.session.commit() + except Exception as e: + db.session.rollback() + current_app.logger.exception(e) + return {'status': 'error', 'message': 'db transaction error'}, 503 + + return self.usermetadataschema.dump(user), 200 + + +class LoginApi(Resource): + """ + See: https://swagger.kmlabz.com/?urls.primaryName=videON%20Backend#/backend/logon + """ + + userschema = UserSchema(many=False) + usermetadataschema = UserMetadataSchema(many=False) + + def post(self): + body = request.get_json() + + try: + userobj = self.userschema.load(body) + except Exception as e: + current_app.logger.exception(e) + return {'status': 'error', 'message': 'Input JSON schema invalid'}, 417 + + user = User.query.filter_by(name=userobj['name']).first() + authorized = user.check_password(userobj['password']) + if not authorized: + return {'status': 'error', 'message': 'username or password invalid'}, 401 + + try: + user.last_logon = datetime.datetime.now() + db.session.add(user) + db.session.commit() + except Exception as e: + db.session.rollback() + current_app.logger.exception(e) + return {'status': 'error', 'message': 'db transaction error'}, 503 + + expires = datetime.timedelta(days=7) + access_token = create_access_token(identity=str(user.name), expires_delta=expires) + return {'token': access_token}, 200 diff --git a/src/schemas.py b/src/schemas.py new file mode 100644 index 0000000..224eb0a --- /dev/null +++ b/src/schemas.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +from flask_marshmallow.sqla import auto_field + +from models import User +from marshm import ma +from marshmallow import fields + + +""" +Marshmallow schemas +""" + + +__author__ = "@tormakris" +__copyright__ = "Copyright 2020, videON Team" +__module_name__ = "schemas" +__version__text__ = "1" + + +class UserSchema(ma.Schema): + """ + Parameters: + - name (string) + - passowrd (string) + """ + + name = fields.String(required=True) + password = fields.String(required=True) + + +class UserMetadataSchema(ma.SQLAlchemyAutoSchema): + """ + Marshmallow schema generated + """ + class Meta: + model = User + exclude = ('timestamp', 'password',) + creation_date = auto_field("timestamp", dump_only=False)