Add Admin and security

master
Raoul Snyman 2021-11-05 19:34:06 -07:00
parent f0ad281f31
commit daf343535e
No known key found for this signature in database
GPG Key ID: F55BCED79626AE9C
12 changed files with 299 additions and 18 deletions

View File

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

View File

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

View File

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

View File

@ -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 '<Page {}>'.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 '<Role {}>'.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 '<User {}>'.format(self.email)

View File

@ -0,0 +1,30 @@
{% extends 'admin/master.html' %}
{% block body %}
{{ super() }}
<div class="container">
<div class="row">
<div class="col-sm-10 col-sm-offset-1">
<h1>Flask-Admin example</h1>
<p class="lead">
Authentication
</p>
<p>
This example shows how you can use <a href="https://pythonhosted.org/Flask-Security/index.html" target="_blank">Flask-Security</a> for authentication.
</p>
{% if not current_user.is_authenticated %}
<p>You can register as a regular user, or log in as a superuser with the following credentials:
<ul>
<li>email: <b>admin</b></li>
<li>password: <b>admin</b></li>
</ul>
<p>
<a class="btn btn-primary" href="{{ url_for('security.login') }}">login</a> <a class="btn btn-default" href="{{ url_for('security.register') }}">register</a>
</p>
{% endif %}
<p>
<a class="btn btn-primary" href="/"><i class="glyphicon glyphicon-chevron-left"></i> Back</a>
</p>
</div>
</div>
</div>
{% endblock body %}

View File

@ -0,0 +1,18 @@
{% extends 'admin/base.html' %}
{% block access_control %}
{% if current_user.is_authenticated %}
<div class="navbar-text btn-group pull-right">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" role="button" aria-expanded="false">
<i class="glyphicon glyphicon-user"></i>
{% if current_user.first_name -%}
{{ current_user.first_name }}
{% else -%}
{{ current_user.email }}
{%- endif %}<span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="{{ url_for('security.logout') }}">Log out</a></li>
</ul>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,27 @@
{% macro render_field_with_errors(field) %}
<div class="form-group">
{{ field.label }} {{ field(class_='form-control', **kwargs)|safe }}
{% if field.errors %}
<ul>
{% for error in field.errors %}
<li>{{ error }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endmacro %}
{% macro render_field(field) %}
<p>{{ field(class_='form-control', **kwargs)|safe }}</p>
{% endmacro %}
{% macro render_checkbox_field(field) -%}
<div class="form-group">
<div class="checkbox">
<label>
{{ field(type='checkbox', **kwargs) }} {{ field.label }}
</label>
</div>
</div>
{%- endmacro %}

View File

@ -0,0 +1,15 @@
{% if security.registerable or security.recoverable or security.confirmable %}
<h2>Menu</h2>
<ul>
<li><a href="{{ url_for_security('login') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">Login</a></li>
{% if security.registerable %}
<li><a href="{{ url_for_security('register') }}{% if 'next' in request.args %}?next={{ request.args.next|urlencode }}{% endif %}">Register</a><br/></li>
{% endif %}
{% if security.recoverable %}
<li><a href="{{ url_for_security('forgot_password') }}">Forgot password</a><br/></li>
{% endif %}
{% if security.confirmable %}
<li><a href="{{ url_for_security('send_confirmation') }}">Confirm account</a></li>
{% endif %}
</ul>
{% endif %}

View File

@ -0,0 +1,9 @@
{%- with messages = get_flashed_messages(with_categories=true) -%}
{% if messages %}
<ul class="flashes">
{% for category, message in messages %}
<li class="{{ category }}">{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{%- endwith %}

View File

@ -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() }}
<div class="row-fluid">
<div class="col-sm-8 col-sm-offset-2">
<h1>Login</h1>
<div class="well">
<form action="{{ url_for_security('login') }}" method="POST" name="login_user_form">
{{ 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") }}
</form>
<p>Not yet signed up? Please <a href="{{ url_for('security.register') }}">register for an account</a>.</p>
</div>
</div>
</div>
{% endblock body %}

View File

@ -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() }}
<div class="row-fluid">
<div class="col-sm-8 col-sm-offset-2">
<h1>Register</h1>
<div class="well">
<form action="{{ url_for_security('register') }}" method="POST" name="register_user_form">
{{ 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") }}
</form>
<p>Already signed up? Please <a href="{{ url_for('security.login') }}">log in</a>.</p>
</div>
</div>
</div>
{% endblock body %}

View File

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