From daf343535eca54261efd627e272f80adda75632f Mon Sep 17 00:00:00 2001 From: Raoul Snyman Date: Fri, 5 Nov 2021 19:34:06 -0700 Subject: [PATCH] Add Admin and security --- libertywiki/admin.py | 45 +++++++++++ libertywiki/app.py | 74 ++++++++++++++++++- libertywiki/db.py | 1 + libertywiki/models.py | 47 ++++++++---- libertywiki/templates/admin/index.html | 30 ++++++++ libertywiki/templates/admin/my_master.html | 18 +++++ libertywiki/templates/security/_macros.html | 27 +++++++ libertywiki/templates/security/_menu.html | 15 ++++ libertywiki/templates/security/_messages.html | 9 +++ .../templates/security/login_user.html | 22 ++++++ .../templates/security/register_user.html | 23 ++++++ setup.cfg | 6 +- 12 files changed, 299 insertions(+), 18 deletions(-) create mode 100644 libertywiki/admin.py create mode 100644 libertywiki/templates/admin/index.html create mode 100644 libertywiki/templates/admin/my_master.html create mode 100644 libertywiki/templates/security/_macros.html create mode 100644 libertywiki/templates/security/_menu.html create mode 100644 libertywiki/templates/security/_messages.html create mode 100644 libertywiki/templates/security/login_user.html create mode 100644 libertywiki/templates/security/register_user.html diff --git a/libertywiki/admin.py b/libertywiki/admin.py new file mode 100644 index 0000000..3ecd5f0 --- /dev/null +++ b/libertywiki/admin.py @@ -0,0 +1,45 @@ +from flask_admin import Admin, helpers as admin_helpers +from flask_admin.contrib.sqla import ModelView +from flask_security import current_user, utils +from wtforms.fields import PasswordField + +from libertywiki.db import session +from libertywiki.models import User, Role + + +class UserAdmin(ModelView): + """A custom view for user administration""" + column_exclude_list = ('password',) + form_excluded_columns = ('password',) + column_auto_select_related = True + + def is_accessible(self): + """Prevent administration of users from users without the "admin" role""" + return current_user.has_role('admin') + + def scaffold_form(self): + """Remove the password field so that we can bypass it and set the password manually""" + form_class = super().scaffold_form() + form_class.password2 = PasswordField('New Password') + return form_class + + def on_model_change(self, form, model, is_created): + """Update the password when the user is saved""" + if len(model.password2): + model.password = utils.encrypt_password(model.password2) + + +class RoleAdmin(ModelView): + """A custom view for role administration""" + + def is_accessible(self): + """Prevent administration of roles from users without the "admin" role""" + return current_user.has_role('admin') + + +def setup_admin(app): + """Set up the admin interface""" + admin = Admin(app, 'LibertyWiki Admin', base_template='admin/my_master.html', template_mode='bootstrap4') + admin.add_view(UserAdmin(User, session)) + admin.add_view(RoleAdmin(Role, session)) + return admin, admin_helpers diff --git a/libertywiki/app.py b/libertywiki/app.py index 1834bb3..f9e40c4 100644 --- a/libertywiki/app.py +++ b/libertywiki/app.py @@ -1,26 +1,92 @@ -from flask import Flask +import json +import os +import secrets +from pathlib import Path +import yaml +from flask import Flask, url_for +from flask_security import Security, SQLAlchemyUserDatastore +from flask_security.utils import hash_password from rst2html import rst2html -from libertywiki.db import db +from libertywiki.admin import setup_admin +from libertywiki.db import db, session +from libertywiki.models import User, Role from libertywiki.views import wiki +DEFAULT_CONFIG = { + 'DEBUG': False, + 'SECRET_KEY': secrets.token_urlsafe(128), + 'SECURITY_PASSWORD_SALT': secrets.token_urlsafe(128), + 'SECURITY_USERNAME_ENABLE': False, + 'SECURITY_REGISTERABLE': True, + 'SECURITY_SEND_REGISTER_EMAIL': False, + 'SQLALCHEMY_DATABASE_URI': 'sqlite://', + 'SQLALCHEMY_ENGINE_OPTIONS': {'pool_pre_ping': True}, + 'SQLALCHEMY_TRACK_MODIFICATIONS': False +} +CONFIG_VARS = [ + 'LIBERTYWIKI_CONFIG', + 'LW_CONFIG', + 'WIKI_CONFIG' +] + def get_app(): """Create and configure the application object""" app = Flask(__name__) + + # Set app defaults + app.config.update(DEFAULT_CONFIG) + + # Load config from file + for config_var in CONFIG_VARS: + if os.environ.get(config_var): + config_fname = Path(os.environ[config_var]) + if config_fname.suffix == 'json': + loader = json.load + elif config_fname.suffix in ['yaml', 'yml']: + loader = yaml.safe_load + with Path(os.environ[config_var]).open() as conf_file: + app.config.update(**loader(conf_file)) + break + + # Load from environment variables + app.config.update(**{key: os.environ[key] for key in DEFAULT_CONFIG.keys() if os.environ.get(key)}) + db.init_app(app) + user_datastore = SQLAlchemyUserDatastore(db, User, Role) + security = Security(app, user_datastore) + admin, admin_helpers = setup_admin(app) with app.app_context(): + # Create all the tables in the database db.create_all() + # Create the basic roles + admin_role = user_datastore.find_or_create_role(name='admin', description='Administrator') + user_datastore.find_or_create_role(name='end-user', description='End user') + session.commit() + # Create an administrator + if not User.query.filter(User.roles.contains(admin_role)).first(): + user = user_datastore.create_user(name='Administrator', email='admin@example.com', + password=hash_password('password1234')) + user.roles.append(admin_role) + session.commit() @app.template_filter('rst2html') def rst2html_filter(text): html, warning = rst2html(text) - print(html) - print(warning) return html + @security.context_processor + def security_context_processor(): + return dict( + admin_base_template=admin.base_template, + admin_view=admin.index_view, + h=admin_helpers, + get_url=url_for + ) + app.register_blueprint(wiki) return app diff --git a/libertywiki/db.py b/libertywiki/db.py index 485568f..bb2b549 100644 --- a/libertywiki/db.py +++ b/libertywiki/db.py @@ -12,6 +12,7 @@ LargeBinary = db.LargeBinary Table = db.Table String = db.String Text = db.Text +backref = db.backref relationship = db.relationship inspect = db.inspect session = db.session diff --git a/libertywiki/models.py b/libertywiki/models.py index 802056c..5a22525 100644 --- a/libertywiki/models.py +++ b/libertywiki/models.py @@ -1,9 +1,16 @@ from datetime import datetime -from sqlalchemy.ext.hybrid import hybrid_property +from flask_security import UserMixin, RoleMixin -from libertywiki.db import Model, Column, ForeignKey, DateTime, Integer, String, Text -from libertywiki.utils import bcrypt +from libertywiki.db import Model, Table, Column, ForeignKey, Boolean, DateTime, Integer, String, Text, backref, \ + relationship + + +roles_users = Table( + 'roles_users', + Column('role_id', Integer, ForeignKey('roles.id')), + Column('user_id', Integer, ForeignKey('users.id')) +) class Page(Model): @@ -21,10 +28,24 @@ class Page(Model): modified = Column(DateTime, default=datetime.now()) def __str__(self): - return self.title + return ''.format(self.title) -class User(Model): +class Role(Model, RoleMixin): + """ + Role model + """ + __tablename__ = 'roles' + + id = Column(Integer, primary_key=True) + name = Column(String(255), unique=True, index=True, nullable=False) + description = Column(Text) + + def __str__(self): + return ''.format(self.name) + + +class User(Model, UserMixin): """ User model """ @@ -33,13 +54,13 @@ class User(Model): id = Column(Integer, primary_key=True) name = Column(String(255)) email = Column(String(255), nullable=False, index=True, unique=True) - _password = Column('password', String(255), nullable=False) - activation_code = Column(String(255)) + password = Column(String(255), nullable=False) + activation_code = Column(String(255), index=True) + active = Column('is_active', Boolean, index=True) + confirmed_at = Column(DateTime) + fs_uniquifier = Column(String(255), unique=True, nullable=False, index=True) - @hybrid_property - def password(self): - return self.password + roles = relationship('Role', secondary=roles_users, backref=backref('users', lazy='dynamic')) - @password.setter - def password(self, value): - self._password = bcrypt.generate_password_hash(value) + def __str__(self): + return ''.format(self.email) diff --git a/libertywiki/templates/admin/index.html b/libertywiki/templates/admin/index.html new file mode 100644 index 0000000..a31f068 --- /dev/null +++ b/libertywiki/templates/admin/index.html @@ -0,0 +1,30 @@ +{% extends 'admin/master.html' %} +{% block body %} +{{ super() }} +
+
+
+

Flask-Admin example

+

+ Authentication +

+

+ This example shows how you can use Flask-Security for authentication. +

+ {% if not current_user.is_authenticated %} +

You can register as a regular user, or log in as a superuser with the following credentials: +

    +
  • email: admin
  • +
  • password: admin
  • +
+

+ login register +

+ {% endif %} +

+ Back +

+
+
+
+{% endblock body %} diff --git a/libertywiki/templates/admin/my_master.html b/libertywiki/templates/admin/my_master.html new file mode 100644 index 0000000..6a7ba83 --- /dev/null +++ b/libertywiki/templates/admin/my_master.html @@ -0,0 +1,18 @@ +{% extends 'admin/base.html' %} + +{% block access_control %} +{% if current_user.is_authenticated %} + +{% endif %} +{% endblock %} diff --git a/libertywiki/templates/security/_macros.html b/libertywiki/templates/security/_macros.html new file mode 100644 index 0000000..a5d3b9f --- /dev/null +++ b/libertywiki/templates/security/_macros.html @@ -0,0 +1,27 @@ +{% macro render_field_with_errors(field) %} + +
+ {{ field.label }} {{ field(class_='form-control', **kwargs)|safe }} + {% if field.errors %} +
    + {% for error in field.errors %} +
  • {{ error }}
  • + {% endfor %} +
+ {% endif %} +
+{% endmacro %} + +{% macro render_field(field) %} +

{{ field(class_='form-control', **kwargs)|safe }}

+{% endmacro %} + +{% macro render_checkbox_field(field) -%} +
+
+ +
+
+{%- endmacro %} diff --git a/libertywiki/templates/security/_menu.html b/libertywiki/templates/security/_menu.html new file mode 100644 index 0000000..9e251b7 --- /dev/null +++ b/libertywiki/templates/security/_menu.html @@ -0,0 +1,15 @@ +{% if security.registerable or security.recoverable or security.confirmable %} +

Menu

+ +{% endif %} diff --git a/libertywiki/templates/security/_messages.html b/libertywiki/templates/security/_messages.html new file mode 100644 index 0000000..15beb21 --- /dev/null +++ b/libertywiki/templates/security/_messages.html @@ -0,0 +1,9 @@ +{%- with messages = get_flashed_messages(with_categories=true) -%} +{% if messages %} +
    + {% for category, message in messages %} +
  • {{ message }}
  • + {% endfor %} +
+{% endif %} +{%- endwith %} diff --git a/libertywiki/templates/security/login_user.html b/libertywiki/templates/security/login_user.html new file mode 100644 index 0000000..ce7e338 --- /dev/null +++ b/libertywiki/templates/security/login_user.html @@ -0,0 +1,22 @@ +{% extends 'admin/master.html' %} +{% from "security/_macros.html" import render_field, render_field_with_errors, render_checkbox_field %} +{% include "security/_messages.html" %} +{% block body %} +{{ super() }} +
+
+

Login

+
+
+ {{ login_user_form.hidden_tag() }} + {{ render_field_with_errors(login_user_form.email) }} + {{ render_field_with_errors(login_user_form.password) }} + {{ render_checkbox_field(login_user_form.remember) }} + {{ render_field(login_user_form.next) }} + {{ render_field(login_user_form.submit, class="btn btn-primary") }} +
+

Not yet signed up? Please register for an account.

+
+
+
+{% endblock body %} diff --git a/libertywiki/templates/security/register_user.html b/libertywiki/templates/security/register_user.html new file mode 100644 index 0000000..db0e719 --- /dev/null +++ b/libertywiki/templates/security/register_user.html @@ -0,0 +1,23 @@ +{% extends 'admin/master.html' %} +{% from "security/_macros.html" import render_field_with_errors, render_field %} +{% include "security/_messages.html" %} +{% block body %} +{{ super() }} +
+
+

Register

+
+
+ {{ register_user_form.hidden_tag() }} + {{ render_field_with_errors(register_user_form.email) }} + {{ render_field_with_errors(register_user_form.password) }} + {% if register_user_form.password_confirm %} + {{ render_field_with_errors(register_user_form.password_confirm) }} + {% endif %} + {{ render_field(register_user_form.submit, class="btn btn-primary") }} +
+

Already signed up? Please log in.

+
+
+
+{% endblock body %} diff --git a/setup.cfg b/setup.cfg index 679366a..6f3f16a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -20,9 +20,13 @@ classifiers = packages = libertywiki install_requires = Flask + Flask-Admin Flask-SQLAlchemy Flask-Bcrypt - Flask-Security + Flask-Security-Too + bcrypt + email_validator + pyyaml rst2html setup_requires = setuptools_scm