From 79d7a45e32c29585c954c7e5a4bf769db9bd33dd Mon Sep 17 00:00:00 2001 From: Jonathan Zernik Date: Sat, 24 Oct 2020 23:13:16 -0400 Subject: [PATCH] Use authentication for web app (#321) * Require authentication for web app index page * Got template working for login page * Got fake login working * Add logout endpoint * Remove old comment * Got login working * Remove log lines --- docker/config.ini | 2 + requirements.txt | 2 + squeakserver/admin/forms.py | 12 ++++ .../admin/squeak_admin_web_service.py | 61 ++++++++++++++++--- squeakserver/admin/squeak_admin_web_user.py | 34 +++++++++++ squeakserver/admin/templates/base.html | 31 ++++++++++ squeakserver/admin/templates/hello.html | 7 +++ squeakserver/admin/templates/login.html | 24 ++++++++ squeakserver/main.py | 2 + squeakserver/node/squeak_block_verifier.py | 2 +- squeakserver/node/squeak_sync_status.py | 2 +- 11 files changed, 170 insertions(+), 9 deletions(-) create mode 100644 squeakserver/admin/forms.py create mode 100644 squeakserver/admin/squeak_admin_web_user.py create mode 100644 squeakserver/admin/templates/base.html create mode 100644 squeakserver/admin/templates/hello.html create mode 100644 squeakserver/admin/templates/login.html diff --git a/docker/config.ini b/docker/config.ini index 577b585b..c5c7008c 100644 --- a/docker/config.ini +++ b/docker/config.ini @@ -32,6 +32,8 @@ rpc_port=8994 enabled=true host=0.0.0.0 port=12994 +username=devuser +password=devpass use_ssl=false [postgresql] diff --git a/requirements.txt b/requirements.txt index 6d90706f..0eb72fd2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,3 +9,5 @@ requests SQLAlchemy Flask protobuf +flask-login +Flask-WTF diff --git a/squeakserver/admin/forms.py b/squeakserver/admin/forms.py new file mode 100644 index 00000000..e58edf88 --- /dev/null +++ b/squeakserver/admin/forms.py @@ -0,0 +1,12 @@ +from flask_wtf import FlaskForm +from wtforms import StringField, PasswordField, BooleanField, SubmitField +from wtforms.validators import ValidationError, DataRequired, EqualTo + +from squeakserver.admin.squeak_admin_web_user import User + + +class LoginForm(FlaskForm): + username = StringField('Username', validators=[DataRequired()]) + password = PasswordField('Password', validators=[DataRequired()]) + remember_me = BooleanField('Remember Me') + submit = SubmitField('Sign In') diff --git a/squeakserver/admin/squeak_admin_web_service.py b/squeakserver/admin/squeak_admin_web_service.py index 8aa485db..9ed2430f 100644 --- a/squeakserver/admin/squeak_admin_web_service.py +++ b/squeakserver/admin/squeak_admin_web_service.py @@ -3,6 +3,14 @@ import logging from flask import Flask from flask import request +from flask import redirect, url_for +from flask import render_template +from flask import flash + +from flask_login import LoginManager +from flask_login import current_user, login_user +from flask_login import login_required +from flask_login import logout_user from google.protobuf import json_format from google.protobuf import message @@ -10,17 +18,38 @@ from google.protobuf import message from proto import squeak_admin_pb2, squeak_admin_pb2_grpc from proto import lnd_pb2, lnd_pb2_grpc +from squeakserver.admin.squeak_admin_web_user import User +from squeakserver.admin.forms import LoginForm logger = logging.getLogger(__name__) -def create_app(handler): +def create_app(handler, username, password): # create and configure the app logger.info("Starting flask app from directory: {}".format(os.getcwd())) - app = Flask(__name__, static_folder='/app/static/build', static_url_path='/') + app = Flask( + __name__, + static_folder='/app/static/build', + static_url_path='/', + template_folder='/app/squeakserver/admin/templates', + ) app.config.from_mapping( SECRET_KEY='dev', ) + login = LoginManager(app) + valid_user = User( + username, + password, + ) + + + @login.user_loader + def load_user(id): + return valid_user.get_user_by_username(id) + + @login.unauthorized_handler + def unauthorized_callback(): + return redirect('/login') def handle_request(request_message, handle_rpc_request): data = request.get_data() @@ -28,11 +57,30 @@ def create_app(handler): reply = handle_rpc_request(request_message) return reply.SerializeToString(reply) + @app.route('/login', methods=['GET', 'POST']) + def login(): + logger.info("Trying to login") + if current_user.is_authenticated: + return redirect(url_for('index')) + form = LoginForm() + if form.validate_on_submit(): + user = valid_user.get_user_by_username(form.username.data) + if user is None or not user.check_password(form.password.data): + flash('Invalid username or password') + return redirect(url_for('login')) + login_user(user, remember=form.remember_me.data) + return redirect(url_for('index')) + return render_template('login.html', title='Sign In', form=form) + + @app.route('/logout') + def logout(): + logout_user() + return redirect(url_for('index')) + @app.route('/') + @login_required def index(): logger.info("Getting index route.") - logger.info("os.getcwd(): {}".format(os.getcwd())) - logger.info("os.listdir(os.getcwd()): {}".format(os.listdir(os.getcwd()))) return app.send_static_file('index.html') @app.route('/hello') @@ -297,12 +345,11 @@ def create_app(handler): class SqueakAdminWebServer(): - def __init__(self, host, port, use_ssl, handler): + def __init__(self, host, port, username, password, use_ssl, handler): self.host = host self.port = port self.use_ssl = use_ssl - self.handler = handler - self.app = create_app(handler) + self.app = create_app(handler, username, password) def serve(self): self.app.run( diff --git a/squeakserver/admin/squeak_admin_web_user.py b/squeakserver/admin/squeak_admin_web_user.py new file mode 100644 index 00000000..9e8cbc63 --- /dev/null +++ b/squeakserver/admin/squeak_admin_web_user.py @@ -0,0 +1,34 @@ +import logging + +from flask_login import UserMixin + +from werkzeug.security import generate_password_hash, check_password_hash + + +logger = logging.getLogger(__name__) + + +class User(UserMixin): + + def __init__(self, username, password): + self.username = username + self.password_hash = generate_password_hash(password) + + def is_authenticated(self): + return True + + def is_active(self): + return True + + def is_anonymous(self): + return False + + def get_id(self): + return self.username + + def check_password(self, password): + return check_password_hash(self.password_hash, password) + + def get_user_by_username(self, username): + if self.username == username: + return self diff --git a/squeakserver/admin/templates/base.html b/squeakserver/admin/templates/base.html new file mode 100644 index 00000000..775a3cef --- /dev/null +++ b/squeakserver/admin/templates/base.html @@ -0,0 +1,31 @@ + + + {% if title %} + {{ title }} - Microblog + {% else %} + Welcome to Microblog + {% endif %} + + +
+ Microblog: + Home + {% if current_user.is_anonymous %} + Login + {% else %} + Logout + {% endif %} +
+
+ {% with messages = get_flashed_messages() %} + {% if messages %} + + {% endif %} + {% endwith %} + {% block content %}{% endblock %} + + diff --git a/squeakserver/admin/templates/hello.html b/squeakserver/admin/templates/hello.html new file mode 100644 index 00000000..90c13fea --- /dev/null +++ b/squeakserver/admin/templates/hello.html @@ -0,0 +1,7 @@ + +Hello from Flask +{% if name %} +

Hello {{ name }}!

+{% else %} +

Hello, World!

+{% endif %} diff --git a/squeakserver/admin/templates/login.html b/squeakserver/admin/templates/login.html new file mode 100644 index 00000000..806e09a6 --- /dev/null +++ b/squeakserver/admin/templates/login.html @@ -0,0 +1,24 @@ +{% extends "base.html" %} + +{% block content %} +

Sign In

+
+ {{ form.hidden_tag() }} +

+ {{ form.username.label }}
+ {{ form.username(size=32) }}
+ {% for error in form.username.errors %} + [{{ error }}] + {% endfor %} +

+

+ {{ form.password.label }}
+ {{ form.password(size=32) }}
+ {% for error in form.password.errors %} + [{{ error }}] + {% endfor %} +

+

{{ form.remember_me() }} {{ form.remember_me.label }}

+

{{ form.submit() }}

+
+{% endblock %} diff --git a/squeakserver/main.py b/squeakserver/main.py index 4a490924..09329756 100644 --- a/squeakserver/main.py +++ b/squeakserver/main.py @@ -65,6 +65,8 @@ def load_admin_web_server(config, handler) -> SqueakAdminWebServer: return SqueakAdminWebServer( config["webadmin"]["host"], config["webadmin"]["port"], + config["webadmin"]["username"], + config["webadmin"]["password"], config["webadmin"].getboolean("use_ssl"), handler, ) diff --git a/squeakserver/node/squeak_block_verifier.py b/squeakserver/node/squeak_block_verifier.py index bfa89bad..e18c1b85 100644 --- a/squeakserver/node/squeak_block_verifier.py +++ b/squeakserver/node/squeak_block_verifier.py @@ -18,7 +18,7 @@ class SqueakBlockVerifier: block_info = self._get_block_info_for_height(squeak.nBlockHeight) except Exception as e: logger.error( - "Failed to sync because unable to get blockchain info.", exc_info=True + "Failed to sync because unable to get blockchain info.", exc_info=False ) return diff --git a/squeakserver/node/squeak_sync_status.py b/squeakserver/node/squeak_sync_status.py index d5845972..81514026 100644 --- a/squeakserver/node/squeak_sync_status.py +++ b/squeakserver/node/squeak_sync_status.py @@ -54,7 +54,7 @@ class SqueakSyncController: block_height = block_info.block_height except Exception as e: logger.error( - "Failed to sync because unable to get blockchain info.", exc_info=True + "Failed to sync because unable to get blockchain info.", exc_info=False ) return self._download_from_peers(peers, block_height)