diff --git a/website/README.md b/website/README.md index ae3140d..77e194d 100644 --- a/website/README.md +++ b/website/README.md @@ -31,8 +31,18 @@ Edit `config.py` - `MISP_MODULE`: url and port where misp-module is running +- `ADMIN_USER`: If True, config page will not be accessible + +- `ADMIN_PASSWORD`: Password for Admin user if `ADMIN_USER` is True + ## Launch ```bash ./launch.sh -l ``` + + + +## Admin user + +If admin user is active, type `/login` in url to access a login page and type the password wrote in `config.py` in `ADMIN_PASSOWRD`. diff --git a/website/app/__init__.py b/website/app/__init__.py index d8c3f96..173fb3e 100644 --- a/website/app/__init__.py +++ b/website/app/__init__.py @@ -3,6 +3,7 @@ from flask_sqlalchemy import SQLAlchemy from flask_wtf import CSRFProtect from flask_migrate import Migrate from flask_session import Session +from flask_login import LoginManager from config import config as Config import os @@ -11,7 +12,8 @@ import os db = SQLAlchemy() csrf = CSRFProtect() migrate = Migrate() -sess = Session() +session = Session() +login_manager = LoginManager() def create_app(): app = Flask(__name__) @@ -25,12 +27,16 @@ def create_app(): csrf.init_app(app) migrate.init_app(app, db, render_as_batch=True) app.config["SESSION_SQLALCHEMY"] = db - sess.init_app(app) + session.init_app(app) + login_manager.login_view = "account.login" + login_manager.init_app(app) from .home import home_blueprint from .history.history import history_blueprint + from .account.account import account_blueprint app.register_blueprint(home_blueprint, url_prefix="/") app.register_blueprint(history_blueprint, url_prefix="/") + app.register_blueprint(account_blueprint, url_prefix="/") return app diff --git a/website/app/account/account.py b/website/app/account/account.py new file mode 100644 index 0000000..7950e0e --- /dev/null +++ b/website/app/account/account.py @@ -0,0 +1,45 @@ +from ..db_class.db import User +from flask import Blueprint, render_template, redirect, url_for, request, flash +from .form import LoginForm +from flask_login import ( + login_required, + login_user, + logout_user, + current_user +) +from ..utils.utils import admin_password +from ..db_class.db import User +from .. import db + +account_blueprint = Blueprint( + 'account', + __name__, + template_folder='templates', + static_folder='static' +) + +@account_blueprint.route('/login', methods=['GET', 'POST']) +def login(): + """Log in an existing user.""" + form = LoginForm() + if form.validate_on_submit(): + if form.password.data == str(admin_password()): + user = User(email="admin@admin.admin") + db.session.add(user) + db.session.commit() + login_user(user, form.remember_me.data) + flash('You are now logged in. Welcome back!', 'success') + return redirect(request.args.get('next') or "/") + else: + flash('Invalid password.', 'error') + return render_template('account/login.html', form=form) + +@account_blueprint.route('/logout') +@login_required +def logout(): + User.query.filter_by(id=current_user.id).delete() + logout_user() + + flash('You have been logged out.', 'info') + return redirect(url_for('home.home')) + diff --git a/website/app/account/form.py b/website/app/account/form.py new file mode 100644 index 0000000..5c53a71 --- /dev/null +++ b/website/app/account/form.py @@ -0,0 +1,13 @@ +from flask_wtf import FlaskForm +from wtforms.fields import ( + BooleanField, + PasswordField, + SubmitField +) +from wtforms.validators import InputRequired + + +class LoginForm(FlaskForm): + password = PasswordField('Password', validators=[InputRequired()]) + remember_me = BooleanField('Keep me logged in') + submit = SubmitField('Log in') diff --git a/website/app/db_class/db.py b/website/app/db_class/db.py index 727382b..924b0fc 100644 --- a/website/app/db_class/db.py +++ b/website/app/db_class/db.py @@ -1,5 +1,6 @@ import json -from .. import db +from .. import db, login_manager +from flask_login import UserMixin, AnonymousUserMixin class Module(db.Model): @@ -76,3 +77,30 @@ class Module_Config(db.Model): config_id = db.Column(db.Integer, index=True) value = db.Column(db.String, index=True) + +class User(UserMixin, db.Model): + id = db.Column(db.Integer, primary_key=True, autoincrement=True) + first_name = db.Column(db.String(64), index=True) + last_name = db.Column(db.String(64), index=True) + email = db.Column(db.String(64), unique=True, index=True) + + def to_json(self): + return { + "id": self.id, + "first_name": self.first_name, + "last_name": self.last_name, + "email": self.email + } + +class AnonymousUser(AnonymousUserMixin): + def is_admin(self): + return False + + def read_only(self): + return True + +login_manager.anonymous_user = AnonymousUser + +@login_manager.user_loader +def load_user(user_id): + return User.query.get(int(user_id)) \ No newline at end of file diff --git a/website/app/history/history.py b/website/app/history/history.py index f5e91f3..7444b31 100644 --- a/website/app/history/history.py +++ b/website/app/history/history.py @@ -1,6 +1,7 @@ import json -from flask import Flask, Blueprint, render_template, request, jsonify +from flask import Flask, Blueprint, render_template, request, jsonify, session as sess from . import history_core as HistoryModel +from ..utils.utils import admin_user_active history_blueprint = Blueprint( 'history', @@ -13,6 +14,7 @@ history_blueprint = Blueprint( @history_blueprint.route("/history", methods=["GET"]) def history(): """View all history""" + sess["admin_user"] = admin_user_active() return render_template("history.html") @history_blueprint.route("/get_history", methods=["GET"]) @@ -25,6 +27,7 @@ def get_history(): @history_blueprint.route("/history_session", methods=["GET"]) def history_session(): """View all history""" + sess["admin_user"] = admin_user_active() return render_template("history_session.html", tree_view=False) @history_blueprint.route("/get_history_session", methods=["GET"]) @@ -49,6 +52,7 @@ def save_history(sid): @history_blueprint.route("/history_tree", methods=["GET"]) def history_tree(): """View all history""" + sess["admin_user"] = admin_user_active() return render_template("history_session.html", tree_view=True) @history_blueprint.route("/get_history_tree", methods=["GET"]) diff --git a/website/app/home.py b/website/app/home.py index a35f08f..e86ba9e 100644 --- a/website/app/home.py +++ b/website/app/home.py @@ -1,7 +1,9 @@ import json -from flask import Flask, Blueprint, render_template, request, jsonify +from flask import Blueprint, render_template, request, jsonify, session as sess +from flask_login import current_user +from . import session_class as SessionModel from . import home_core as HomeModel -from . import session as SessionModel +from .utils.utils import admin_user_active home_blueprint = Blueprint( 'home', @@ -13,12 +15,14 @@ home_blueprint = Blueprint( @home_blueprint.route("/") def home(): + sess["admin_user"] = admin_user_active() if "query" in request.args: return render_template("home.html", query=request.args.get("query")) return render_template("home.html") @home_blueprint.route("/home/", methods=["GET", "POST"]) def home_query(sid): + sess["admin_user"] = admin_user_active() if "query" in request.args: query = request.args.get("query") return render_template("home.html", query=query, sid=sid) @@ -26,6 +30,7 @@ def home_query(sid): @home_blueprint.route("/query/") def query(sid): + sess["admin_user"] = admin_user_active() session = HomeModel.get_session(sid) flag=False if session: @@ -159,38 +164,50 @@ def download(sid): - @home_blueprint.route("/modules_config") def modules_config(): """List all modules for configuration""" - - return render_template("modules_config.html") + sess["admin_user"] = admin_user_active() + if sess.get("admin_user"): + if current_user.is_authenticated: + return render_template("modules_config.html") + return render_template("404.html") @home_blueprint.route("/modules_config_data") def modules_config_data(): """List all modules for configuration""" - - modules_config = HomeModel.get_modules_config() - return modules_config, 200 + sess["admin_user"] = admin_user_active() + if sess.get("admin_user"): + if current_user.is_authenticated: + modules_config = HomeModel.get_modules_config() + return modules_config, 200 + return {"message": "Permission denied"}, 403 @home_blueprint.route("/change_config", methods=["POST"]) def change_config(): """Change configuation for a module""" - if "module_name" in request.json["result_dict"]: - res = HomeModel.change_config_core(request.json["result_dict"]) - if res: - return {'message': 'Config changed', 'toast_class': "success-subtle"}, 200 - return {'message': 'Something went wrong', 'toast_class': "danger-subtle"}, 400 - return {'message': 'Need to pass "module_name"', 'toast_class': "warning-subtle"}, 400 + sess["admin_user"] = admin_user_active() + if sess.get("admin_user"): + if current_user.is_authenticated: + if "module_name" in request.json["result_dict"]: + res = HomeModel.change_config_core(request.json["result_dict"]) + if res: + return {'message': 'Config changed', 'toast_class': "success-subtle"}, 200 + return {'message': 'Something went wrong', 'toast_class': "danger-subtle"}, 400 + return {'message': 'Need to pass "module_name"', 'toast_class': "warning-subtle"}, 400 + return {'message': 'Permission denied', 'toast_class': "danger-subtle"}, 403 @home_blueprint.route("/change_status", methods=["GET"]) def change_status(): """Change the status of a module, active or unactive""" - if "module_id" in request.args: - res = HomeModel.change_status_core(request.args.get("module_id")) - if res: - return {'message': 'Module status changed', 'toast_class': "success-subtle"}, 200 - return {'message': 'Something went wrong', 'toast_class': "danger-subtle"}, 400 - return {'message': 'Need to pass "module_id"', 'toast_class': "warning-subtle"}, 400 - + sess["admin_user"] = admin_user_active() + if sess.get("admin_user"): + if current_user.is_authenticated: + if "module_id" in request.args: + res = HomeModel.change_status_core(request.args.get("module_id")) + if res: + return {'message': 'Module status changed', 'toast_class': "success-subtle"}, 200 + return {'message': 'Something went wrong', 'toast_class': "danger-subtle"}, 400 + return {'message': 'Need to pass "module_id"', 'toast_class': "warning-subtle"}, 400 + return {'message': 'Permission denied', 'toast_class': "danger-subtle"}, 403 diff --git a/website/app/home_core.py b/website/app/home_core.py index 9d08acc..0adc40b 100644 --- a/website/app/home_core.py +++ b/website/app/home_core.py @@ -1,5 +1,5 @@ import json -from .utils.utils import query_get_module, isUUID +from .utils.utils import query_get_module from . import db from .db_class.db import History, Module, Config, Module_Config, Session_db, History_Tree from flask import session as sess diff --git a/website/app/session.py b/website/app/session_class.py similarity index 100% rename from website/app/session.py rename to website/app/session_class.py diff --git a/website/app/templates/account/login.html b/website/app/templates/account/login.html new file mode 100644 index 0000000..4881727 --- /dev/null +++ b/website/app/templates/account/login.html @@ -0,0 +1,126 @@ + +{% import 'macros/form_macros.html' as f %} + + + + + + Flowintel-cm + + + + + + + + + + + + + + + +
+ {% include 'macros/_flashes.html' %} + {% set flashes = { + 'error': get_flashed_messages(category_filter=['form-error']), + 'warning': get_flashed_messages(category_filter=['form-check-email']), + 'info': get_flashed_messages(category_filter=['form-info']), + 'success': get_flashed_messages(category_filter=['form-success']) + } %} +
+ {{ form.hidden_tag() }} +
+ {{form.password.label}}: + {{form.password(class_="form-control")}} +
+
+ {{form.remember_me.label}}: + {{form.remember_me}} +
+ {{ f.form_message(flashes['error'], header='Something went wrong.', class='error') }} + {{ f.form_message(flashes['warning'], header='Check your email.', class='warning') }} + {{ f.form_message(flashes['info'], header='Information', class='info') }} + {{ f.form_message(flashes['success'], header='Success!', class='success') }} + {{form.submit(class='btn btn-primary')}} +
+
+ + + {# Implement CSRF protection for site #} + {% if csrf_token()|safe %} +
+ +
+ {% endif %} + + + + \ No newline at end of file diff --git a/website/app/templates/sidebar.html b/website/app/templates/sidebar.html index bb4a5ac..bb7daa1 100644 --- a/website/app/templates/sidebar.html +++ b/website/app/templates/sidebar.html @@ -11,18 +11,34 @@ Home - - History - + {% if session.admin_user %} + {% if current_user.is_authenticated %} + + History + + {%endif%} + {%else%} + + History + + {%endif%} History Session History Tree - - Config - + {% if session.admin_user %} + {% if current_user.is_authenticated %} + + Config + + {%endif%} + {%else%} + + Config + + {%endif%} diff --git a/website/app/utils/utils.py b/website/app/utils/utils.py index 4da65c7..724f186 100644 --- a/website/app/utils/utils.py +++ b/website/app/utils/utils.py @@ -48,84 +48,12 @@ def get_object(obj_name): loc_json = json.load(read_json) return loc_json return False - -# def form_to_dict(form): -# loc_dict = dict() -# for field in form._fields: -# if field == "files_upload": -# loc_dict[field] = dict() -# loc_dict[field]["data"] = form._fields[field].data -# loc_dict[field]["name"] = form._fields[field].name -# elif not field == "submit" and not field == "csrf_token": -# loc_dict[field] = form._fields[field].data -# return loc_dict +def admin_user_active(): + return Config.ADMIN_USER -# def create_specific_dir(specific_dir): -# if not os.path.isdir(specific_dir): -# os.mkdir(specific_dir) +def admin_password(): + return Config.ADMIN_PASSWORD -# caseSchema = { -# "type": "object", -# "properties": { -# "title": {"type": "string"}, -# "description": {"type": "string"}, -# "uuid": {"type": "string"}, -# "deadline:": {"type": "string"}, -# "recurring_date:": {"type": "string"}, -# "recurring_type:": {"type": "string"}, -# "tasks": { -# "type": "array", -# "items": {"type": "object"}, -# }, -# "tags":{ -# "type": "array", -# "items": {"type": "string"}, -# }, -# "clusters":{ -# "type": "array", -# "items": {"type": "string"}, -# }, -# }, -# "required": ['title'] -# } - -# taskSchema = { -# "type": "object", -# "properties": { -# "title": {"type": "string"}, -# "description": {"type": "string"}, -# "uuid": {"type": "string"}, -# "deadline:": {"type": "string"}, -# "url:": {"type": "string"}, -# "notes:": {"type": "string"}, -# "tags":{ -# "type": "array", -# "items": {"type": "string"} -# }, -# "clusters":{ -# "type": "array", -# "items": {"type": "string"}, -# }, -# }, -# "required": ['title'] -# } - -# def validateCaseJson(json_data): -# try: -# jsonschema.validate(instance=json_data, schema=caseSchema) -# except jsonschema.exceptions.ValidationError as err: -# print(err) -# return False -# return True - -# def validateTaskJson(json_data): -# try: -# jsonschema.validate(instance=json_data, schema=taskSchema) -# except jsonschema.exceptions.ValidationError as err: -# print(err) -# return False -# return True - diff --git a/website/config.py b/website/config.py index bb49a8a..52ef0f5 100644 --- a/website/config.py +++ b/website/config.py @@ -4,6 +4,8 @@ class Config: FLASK_URL = '127.0.0.1' FLASK_PORT = 7008 MISP_MODULE = '127.0.0.1:6666' + ADMIN_USER = False + ADMIN_PASSWORD = Password1234 class DevelopmentConfig(Config): DEBUG = True diff --git a/website/requirements.txt b/website/requirements.txt index d88d6d5..d87ce1b 100644 --- a/website/requirements.txt +++ b/website/requirements.txt @@ -4,6 +4,7 @@ flask-session flask-sqlalchemy Flask-WTF Flask-Migrate +Flask-Login WTForms Werkzeug==2.3.8 flask-restx