[PYTHON] Implementation of DB administrator screen by Flask-Admin and Flask-Login

Introduction

To implement something like Django's admin screen with Flask, it's convenient to use a library called Flask-Admin. However, if you just use Flask-Admin as it is, you can enter the administrator screen without typing a password (without logging in), which is very vulnerable in terms of security. In this article, we will use Flask-Admin and Flask-Login to ** implement a DB administrator screen with a login function **.

This article

Reference source

Since the amount of information is a little small, this is an article that I tried to increase the information of the explanation in Japanese. If you can speak English or are not good at verbose phrases, you can see the reference site from the link above.

About MVC

You need to know about the Model-View-Controller model, as it only needs a little bit. Because in this article I'll use words like model and controller. I think the article below will be helpful. About MVC model

Writer's environment

Ubuntu20.04LTS
MySQL 8.0.21
Python3.8.5

Flask==1.1.2
Flask-Admin==1.5.6
Flask-Login==0.5.0
Flask-SQLAlchemy==2.4.4
mysqlclient==2.0.1

Whole source code

Suddenly, I'll show you the whole final source code for those who don't have the time. Detailed explanation is below.

from flask import Flask, abort, jsonify, render_template, request, redirect, url_for
from wtforms import form, fields, validators
import flask_admin as admin
import flask_login as login
from flask_admin.contrib import sqla
from flask_admin import helpers, expose
from flask_admin.contrib.sqla import ModelView
from werkzeug.security import generate_password_hash, check_password_hash
from flask_sqlalchemy import SQLAlchemy

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://{user}:{password}@{host}/{db_name}".format(**{
  'user': os.environ['RDS_USER'],
  'password': os.environ['RDS_PASS'],
  'host': os.environ['RDS_HOST'],
  'db_name': os.environ['RDS_DB_NAME']
})
app.config['SECRET_KEY'] = os.environ['FLASK_SECRET_KEY']
db = SQLAlchemy(app)

class AdminUser(db.Model):
  id = db.Column(db.Integer, primary_key=True)
  login = db.Column(db.String(50), unique=True)
  password = db.Column(db.String(250))

  @property
  def is_authenticated(self):
    return True

  @property
  def is_active(self):
    return True

  @property
  def is_anonymous(self):
    return False

  def get_id(self):
    return self.id

  def __unicode__(self):
    return self.username


class LoginForm(form.Form):
  login = fields.StringField(validators=[validators.required()])
  password = fields.PasswordField(validators=[validators.required()])

  def validate_login(self, field):
    user = self.get_user()

    if user is None:
      raise validators.ValidationError('The user name or password is different.')

    if not check_password_hash(user.password, self.password.data):
      raise validators.ValidationError('The user name or password is different.')

  def get_user(self):
    return db.session.query(AdminUser).filter_by(login=self.login.data).first()


class RegistrationForm(form.Form):
  login = fields.StringField(validators=[validators.required()])
  password = fields.PasswordField(validators=[validators.required()])

  def validate_login(self, field):
    if db.session.query(AdminUser).filter_by(login=self.login.data).count() > 0:
      raise validators.ValidationError('The same user name exists.')


def init_login():
  login_manager = login.LoginManager()
  login_manager.init_app(app)

  @login_manager.user_loader
  def load_user(user_id):
    return db.session.query(AdminUser).get(user_id)


class MyModelView(sqla.ModelView):
  def is_accessible(self):
    return login.current_user.is_authenticated


class MyAdminIndexView(admin.AdminIndexView):
  @expose('/')
  def index(self):
    if not login.current_user.is_authenticated:
      return redirect(url_for('.login_view'))
    return super(MyAdminIndexView, self).index()

  @expose('/login/', methods=('GET', 'POST'))
  def login_view(self):
    form = LoginForm(request.form)
    if helpers.validate_form_on_submit(form):
      user = form.get_user()
      login.login_user(user)

    if login.current_user.is_authenticated:
      return redirect(url_for('.index'))
    link = '<p>For not creating an account<a href="' + url_for('.register_view') + '">click here</a></p>'
    self._template_args['form'] = form
    self._template_args['link'] = link
    return super(MyAdminIndexView, self).index()
  
  @expose('/register/', methods=('GET', 'POST'))
  def register_view(self):
    form = RegistrationForm(request.form)
    if helpers.validate_form_on_submit(form):
      user = AdminUser()

      form.populate_obj(user)
      user.password = generate_password_hash(form.password.data)
      db.session.add(user)
      db.session.commit()
      login.login_user(user)
      return redirect(url_for('.index'))
    link = '<p>If you already have an account<a href="' + url_for('.login_view') + '">Click here to log in</a></p>'
    self._template_args['form'] = form
    self._template_args['link'] = link
    return super(MyAdminIndexView, self).index()

  @expose('/logout/')
  def logout_view(self):
    login.logout_user()
    return redirect(url_for('.index'))


init_login()
admin = admin.Admin(app, 'Administrator screen', index_view=MyAdminIndexView(), base_template='my_master.html')
admin.add_view(MyModelView(AdminUser, db.session))


@app.route("/", methods=['GET'])
def index():
  return "Hello, World!"

if __name__ == "__main__":
  app.run()

Connect to DB

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = "mysql://{user}:{password}@{host}/{db_name}".format(**{
  'user': os.environ['RDS_USER'],
  'password': os.environ['RDS_PASS'],
  'host': os.environ['RDS_HOST'],
  'db_name': os.environ['RDS_DB_NAME']
})
app.config['SECRET_KEY'] = os.environ['FLASK_SECRET_KEY']
db = SQLAlchemy(app)

Since there are many articles in Japanese on the Internet, the explanation here is omitted.

Create a model for an administrator account

class AdminUser(db.Model):
  id = db.Column(db.Integer, primary_key=True)
  login = db.Column(db.String(50), unique=True)
  password = db.Column(db.String(250))

  @property
  def is_authenticated(self):
    return True

  @property
  def is_active(self):
    return True

  @property
  def is_anonymous(self):
    return False

  def get_id(self):
    return self.id

  def __unicode__(self):
    return self.username

login is your username. It defines the user name and password for logging in to the administrator screen. Each method has a property decorator so that when you write the login process later, you'll get information like whether you're logged in or not. If you want to know more about property decorators ↓ Properties

Creating a controller


class LoginForm(form.Form):
  login = fields.StringField(validators=[validators.required()])
  password = fields.PasswordField(validators=[validators.required()])

  def validate_login(self, field):
    user = self.get_user()

    if user is None:
      raise validators.ValidationError('The user name or password is different.')

    if not check_password_hash(user.password, self.password.data):
      raise validators.ValidationError('The user name or password is different.')

  def get_user(self):
    return db.session.query(AdminUser).filter_by(login=self.login.data).first()


class RegistrationForm(form.Form):
  login = fields.StringField(validators=[validators.required()])
  password = fields.PasswordField(validators=[validators.required()])

  def validate_login(self, field):
    if db.session.query(AdminUser).filter_by(login=self.login.data).count() > 0:
      raise validators.ValidationError('The same user name exists.')

It is a controller that describes the process when input is received from the form of the view (login screen or administrator account registration screen). What you should pay attention to here is the LoginForm class.

check_password_hash(user.password, self.password.data)

is. This is a convenient one that compares the real password that is hashed and saved and the hashed value entered from the login screen, and returns True if they match.

Although it is not recommended, if the password is saved in clear text in the DB, the conditional expression should be used.

if user.password != self.password.data:

I think you should change it to.

Create view


def init_login():
  login_manager = login.LoginManager()
  login_manager.init_app(app)

  @login_manager.user_loader
  def load_user(user_id):
    return db.session.query(AdminUser).get(user_id)


class MyModelView(sqla.ModelView):
  def is_accessible(self):
    return login.current_user.is_authenticated


class MyAdminIndexView(admin.AdminIndexView):
  @expose('/')
  def index(self):
    if not login.current_user.is_authenticated:
      return redirect(url_for('.login_view'))
    return super(MyAdminIndexView, self).index()

  @expose('/login/', methods=('GET', 'POST'))
  def login_view(self):
    form = LoginForm(request.form)
    if helpers.validate_form_on_submit(form):
      user = form.get_user()
      login.login_user(user)

    if login.current_user.is_authenticated:
      return redirect(url_for('.index'))
    link = '<p>For not creating an account<a href="' + url_for('.register_view') + '">click here</a></p>'
    self._template_args['form'] = form
    self._template_args['link'] = link
    return super(MyAdminIndexView, self).index()
  
  @expose('/register/', methods=('GET', 'POST'))
  def register_view(self):
    form = RegistrationForm(request.form)
    if helpers.validate_form_on_submit(form):
      user = AdminUser()

      form.populate_obj(user)
      user.password = generate_password_hash(form.password.data)
      db.session.add(user)
      db.session.commit()
      login.login_user(user)
      return redirect(url_for('.index'))
    link = '<p>If you already have an account<a href="' + url_for('.login_view') + '">Click here to log in</a></p>'
    self._template_args['form'] = form
    self._template_args['link'] = link
    return super(MyAdminIndexView, self).index()

  @expose('/logout/')
  def logout_view(self):
    login.logout_user()
    return redirect(url_for('.index'))

init_login()
admin = admin.Admin(app, 'Administrator screen', index_view=MyAdminIndexView(), base_template='my_master.html')
admin.add_view(MyModelView(AdminUser, db.session))

It's a bit like doing it with Flask normally.

What you should pay attention to here is the MyModelView class. MyModelView inherits from sqla.ModelView and overrides the is_accessible method. (I need to) The is_accessible method returns whether the user is already logged in. By simply overriding the is_accessible method, you will be able to define access control rules in a later view class (here the MyAdminIndexView class).

init_login()
admin = admin.Admin(app, 'Administrator screen', index_view=MyAdminIndexView(), base_template='my_master.html')
admin.add_view(MyModelView(AdminUser, db.session))

Defines which view class is actually used in which model.

Write html

It doesn't make sense without HTML. Create a templates directory in the project root directory, and create files and directories with the following structure.

templates/
    admin/
        index.html
    my_master.html
    index.html

my_master.html

{% extends 'admin/base.html' %}

{% block access_control %}
{% if current_user.is_authenticated %}
<div class="btn-group pull-right">
  <a class="btn dropdown-toggle" data-toggle="dropdown" href="#">
    <i class="icon-user"></i> {{ current_user.login }} <span class="caret"></span>
  </a>
  <ul class="dropdown-menu">
    <li><a href="{{ url_for('admin.logout_view') }}">Log out</a></li>
  </ul>
</div>
{% endif %}
{% endblock %}

On the screen after login, if you press the user ID, a dropdown button will appear.

templates/index.html

<html>
  <body>
    <div>
      <a href="{{ url_for('admin.index') }}">Go to admin!</a>
    </div>
  </body>
</html>

Any index page is fine. I will write it appropriately.

templates/admin/index.html

{% extends 'admin/master.html' %}
{% block body %}
{{ super() }}
<div class="row-fluid">

    <div>
        {% if current_user.is_authenticated %}
        <h1>Civichat admin screen</h1>
        <p class="lead">
Certified
        </p>
        <p>
You can manage the data from this screen. If you want to log out/admin/Please access logout.
        </p>
        {% else %}
        <form method="POST" action="">
            {{ form.hidden_tag() if form.hidden_tag }}
            {% for f in form if f.type != 'CSRFTokenField' %}
            <div>
            {{ f.label }}
            {{ f }}
            {% if f.errors %}
            <ul>
                {% for e in f.errors %}
                <li>{{ e }}</li>
                {% endfor %}
            </ul>
            {% endif %}
            </div>
            {% endfor %}
            <button class="btn" type="submit">Done</button>
        </form>
        {{ link | safe }}
        {% endif %}
    </div>

    <a class="btn btn-primary" href="/"><i class="icon-arrow-left icon-white"></i>Return</a>
</div>
{% endblock body %}

It's like an index page on the admin screen after login.

I want to restrict IP in addition to password authentication

In the first place, I think there is a need to restrict access by IP address before reaching the login form. I think the article below will be helpful.

IP restriction with Flask

Finally

If you have any mistakes, please let us know in the comments.

Recommended Posts

Implementation of DB administrator screen by Flask-Admin and Flask-Login
Explanation and implementation of SocialFoceModel
Introduction and Implementation of JoCoR-Loss (CVPR2020)
Explanation and implementation of ESIM algorithm
Introduction and implementation of activation function
Summary of basic implementation by PyTorch
Explanation and implementation of simple perceptron
[Python] Implementation of Nelder–Mead method and saving of GIF images by matplotlib
Derivation of multivariate t distribution and implementation of random number generation by python
Implement a model with state and behavior (3) --Example of implementation by decorator
Implementation and experiment of convex clustering method
Implementation of SVM by stochastic gradient descent
Explanation and implementation of Decomposable Attention algorithm
Summary of SQLAlchemy connection method by DB
[Python] Comparison of Principal Component Analysis Theory and Implementation by Python (PCA, Kernel PCA, 2DPCA)
Comparison of k-means implementation examples of scikit-learn and pyclustering
Low-rank approximation of images by HOSVD and HOOI
Calculation of technical indicators by TA-Lib and pandas
Implementation of TRIE tree with Python and LOUDS
Parallel learning of deep learning by Keras and Kubernetes
Deep learning learned by implementation (segmentation) ~ Implementation of SegNet ~
Explanation of edit distance and implementation in Python