feat: implement authentication
This commit is contained in:
parent
8d73839de7
commit
d9ef64d983
@ -11,6 +11,7 @@ services:
|
|||||||
- ./lumi2/__init__.py:/app/lumi2/__init__.py:ro
|
- ./lumi2/__init__.py:/app/lumi2/__init__.py:ro
|
||||||
- ./lumi2/exceptions.py:/app/lumi2/exceptions.py:ro
|
- ./lumi2/exceptions.py:/app/lumi2/exceptions.py:ro
|
||||||
- ./lumi2/ldap.py:/app/lumi2/ldap.py:ro
|
- ./lumi2/ldap.py:/app/lumi2/ldap.py:ro
|
||||||
|
- ./lumi2/auth.py:/app/lumi2/auth.py:ro
|
||||||
- ./lumi2/usermodel.py:/app/lumi2/usermodel.py:ro
|
- ./lumi2/usermodel.py:/app/lumi2/usermodel.py:ro
|
||||||
- ./lumi2/webapi.py:/app/lumi2/webapi.py:ro
|
- ./lumi2/webapi.py:/app/lumi2/webapi.py:ro
|
||||||
- ./lumi2/usermanager.py:/app/lumi2/usermanager.py:ro
|
- ./lumi2/usermanager.py:/app/lumi2/usermanager.py:ro
|
||||||
|
@ -16,6 +16,7 @@ def create_app(test_config=None):
|
|||||||
app = Flask(__name__, instance_relative_config=True)
|
app = Flask(__name__, instance_relative_config=True)
|
||||||
app.config.from_mapping(
|
app.config.from_mapping(
|
||||||
SECRET_KEY='ChangeMeInProduction',
|
SECRET_KEY='ChangeMeInProduction',
|
||||||
|
ADMIN_PASSWORD='pbkdf2:sha256:260000$J9yKJOAvWfvaO9Op$f959d88402f67a5143808a00e35d17e636546f1caf5a85c1b6ab1165d1780448',
|
||||||
SITE_URL='https://www.example.com/',
|
SITE_URL='https://www.example.com/',
|
||||||
SITE_TITLE='LUMI 2',
|
SITE_TITLE='LUMI 2',
|
||||||
SITE_AUTHOR='LUMI 2 Development Team',
|
SITE_AUTHOR='LUMI 2 Development Team',
|
||||||
@ -44,6 +45,9 @@ def create_app(test_config=None):
|
|||||||
except OSError:
|
except OSError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
from . import auth
|
||||||
|
app.register_blueprint(auth.bp)
|
||||||
|
|
||||||
from . import usermanager
|
from . import usermanager
|
||||||
app.register_blueprint(usermanager.bp)
|
app.register_blueprint(usermanager.bp)
|
||||||
app.add_url_rule('/', endpoint='index')
|
app.add_url_rule('/', endpoint='index')
|
||||||
|
68
lumi2/auth.py
Normal file
68
lumi2/auth.py
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import functools
|
||||||
|
|
||||||
|
from flask import (
|
||||||
|
Blueprint, current_app, g, flash, redirect, url_for, session,
|
||||||
|
render_template
|
||||||
|
)
|
||||||
|
from werkzeug.security import check_password_hash
|
||||||
|
|
||||||
|
from flask_wtf import FlaskForm
|
||||||
|
from wtforms import ValidationError, PasswordField, SubmitField
|
||||||
|
from wtforms.validators import InputRequired
|
||||||
|
|
||||||
|
|
||||||
|
bp = Blueprint('auth', __name__)
|
||||||
|
|
||||||
|
class LoginForm(FlaskForm):
|
||||||
|
@staticmethod
|
||||||
|
def validate_password(form, field) -> None:
|
||||||
|
if not field.data:
|
||||||
|
raise ValidationError("Please enter a password.")
|
||||||
|
if not check_password_hash(current_app.config['ADMIN_PASSWORD'], field.data):
|
||||||
|
raise ValidationError("Invalid password.")
|
||||||
|
|
||||||
|
password = PasswordField(
|
||||||
|
'Password',
|
||||||
|
[InputRequired('Please enter a password.'), validate_password],
|
||||||
|
)
|
||||||
|
|
||||||
|
submit = SubmitField(
|
||||||
|
'Log In',
|
||||||
|
)
|
||||||
|
|
||||||
|
@bp.route("/login", methods=("GET", "POST"))
|
||||||
|
def login():
|
||||||
|
form = LoginForm()
|
||||||
|
if form.validate_on_submit():
|
||||||
|
session.clear()
|
||||||
|
session['is_authenticated'] = True
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
return render_template('auth/login.html', form=form)
|
||||||
|
|
||||||
|
|
||||||
|
@bp.before_app_request
|
||||||
|
def load_logged_in_user():
|
||||||
|
authentication_status = session.get('is_authenticated')
|
||||||
|
if authentication_status:
|
||||||
|
g.is_authenticated = authentication_status
|
||||||
|
else:
|
||||||
|
g.is_authenticated = False
|
||||||
|
|
||||||
|
|
||||||
|
@bp.route('/logout')
|
||||||
|
def logout():
|
||||||
|
session.clear()
|
||||||
|
flash("You were logged out.")
|
||||||
|
return redirect(url_for('index'))
|
||||||
|
|
||||||
|
|
||||||
|
def login_required(view):
|
||||||
|
@functools.wraps(view)
|
||||||
|
def wrapped_view(**kwargs):
|
||||||
|
if not g.is_authenticated:
|
||||||
|
return redirect(url_for('auth.login'))
|
||||||
|
|
||||||
|
return view(**kwargs)
|
||||||
|
|
||||||
|
return wrapped_view
|
20
lumi2/templates/auth/login.html
Normal file
20
lumi2/templates/auth/login.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
<h1 class="text-center">Log In</h1>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form method="post" enctype="multipart/form-data">
|
||||||
|
{{ form.csrf_token }}
|
||||||
|
<div class="container row align-items-center border rounded m-2">
|
||||||
|
{{ form.password.label(class="col-sm-2 col-form-label text-center") }}
|
||||||
|
{{ form.password(class="col-sm form-control m-2" + (" is-invalid" if form.password.errors else "")) }}
|
||||||
|
{{ form.submit(class_="col-sm-2 btn btn-primary m-2") }}
|
||||||
|
{% if form.password.errors %}
|
||||||
|
<div class="text-center invalid-feedback">{{ form.password.errors[0] }}</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{% endblock content %}
|
@ -45,11 +45,19 @@
|
|||||||
<a class="nav-link" href="{{ url_for('usermanager.group_list') }}">Groups</a>
|
<a class="nav-link" href="{{ url_for('usermanager.group_list') }}">Groups</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
{% if g.is_authenticated %}
|
||||||
|
<div class="d-flex"">
|
||||||
|
<a class="btn btn-outline-primary"
|
||||||
|
href="{{ url_for('auth.logout') }}"
|
||||||
|
role="button">Log Out</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
<div class="d-flex"">
|
<div class="d-flex"">
|
||||||
<a class="btn btn-primary"
|
<a class="btn btn-primary"
|
||||||
href="#"
|
href="{{ url_for('auth.login') }}"
|
||||||
role="button">Log In</a>
|
role="button">Log In</a>
|
||||||
</div>
|
</div>
|
||||||
|
{% endif%}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -11,7 +11,6 @@
|
|||||||
<input type="text" class="col-sm form-control m-2" id="groupNameInput">
|
<input type="text" class="col-sm form-control m-2" id="groupNameInput">
|
||||||
<a class="col-sm-2 btn btn-secondary m-2" href="{{ url_for('usermanager.group_list') }}" role="button">Cancel</a>
|
<a class="col-sm-2 btn btn-secondary m-2" href="{{ url_for('usermanager.group_list') }}" role="button">Cancel</a>
|
||||||
<button class="col-sm-2 btn btn-primary m-2" type="button" id="createGroupButton">Create Group</button>
|
<button class="col-sm-2 btn btn-primary m-2" type="button" id="createGroupButton">Create Group</button>
|
||||||
<p class="col-sm-12 text-danger">
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row border rounded m-2">
|
<div class="row border rounded m-2">
|
||||||
<table class="table table-hover align-middle" id="tableMembers">
|
<table class="table table-hover align-middle" id="tableMembers">
|
||||||
|
@ -10,6 +10,6 @@
|
|||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
<div class="row justify-content-md-center text-center">
|
<div class="row justify-content-md-center text-center">
|
||||||
<p class="fs-3 text-secondary">This site is still under construction.</p>
|
<p class="fs-3 text-secondary"><i class="bi-cone-striped"></i> This site is still under construction.</p>
|
||||||
</div>
|
</div>
|
||||||
{% endblock content %}
|
{% endblock content %}
|
||||||
|
@ -16,6 +16,7 @@ from wtforms import (
|
|||||||
)
|
)
|
||||||
from wtforms.validators import InputRequired, Email, EqualTo
|
from wtforms.validators import InputRequired, Email, EqualTo
|
||||||
|
|
||||||
|
from lumi2.auth import login_required
|
||||||
import lumi2.ldap as ldap
|
import lumi2.ldap as ldap
|
||||||
from lumi2.usermodel import User, Group
|
from lumi2.usermodel import User, Group
|
||||||
from lumi2.exceptions import InvalidStringFormatException, InvalidImageException
|
from lumi2.exceptions import InvalidStringFormatException, InvalidImageException
|
||||||
@ -59,6 +60,7 @@ def index():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route("/users/view/<string:username>")
|
@bp.route("/users/view/<string:username>")
|
||||||
|
@login_required
|
||||||
def user_view(username: str):
|
def user_view(username: str):
|
||||||
"""Detail view for a specific User.
|
"""Detail view for a specific User.
|
||||||
|
|
||||||
@ -81,6 +83,7 @@ def user_view(username: str):
|
|||||||
|
|
||||||
|
|
||||||
@bp.route("/users/list")
|
@bp.route("/users/list")
|
||||||
|
@login_required
|
||||||
def user_list():
|
def user_list():
|
||||||
"""Displays a list of all users."""
|
"""Displays a list of all users."""
|
||||||
|
|
||||||
@ -198,6 +201,7 @@ class UserCreationForm(UserUpdateForm):
|
|||||||
|
|
||||||
|
|
||||||
@bp.route("/users/create", methods=("GET", "POST"))
|
@bp.route("/users/create", methods=("GET", "POST"))
|
||||||
|
@login_required
|
||||||
def user_create():
|
def user_create():
|
||||||
"""Creation view for a new User.
|
"""Creation view for a new User.
|
||||||
|
|
||||||
@ -236,6 +240,7 @@ def user_create():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route("/users/update/<string:username>", methods=("GET", "POST"))
|
@bp.route("/users/update/<string:username>", methods=("GET", "POST"))
|
||||||
|
@login_required
|
||||||
def user_update(username: str):
|
def user_update(username: str):
|
||||||
"""Update view for a specific User.
|
"""Update view for a specific User.
|
||||||
|
|
||||||
@ -285,6 +290,7 @@ def user_update(username: str):
|
|||||||
|
|
||||||
|
|
||||||
@bp.route("/users/delete/<string:username>", methods=("GET", "POST"))
|
@bp.route("/users/delete/<string:username>", methods=("GET", "POST"))
|
||||||
|
@login_required
|
||||||
def user_delete(username: str):
|
def user_delete(username: str):
|
||||||
"""Deletion view for a specific User.
|
"""Deletion view for a specific User.
|
||||||
|
|
||||||
@ -327,6 +333,7 @@ def user_delete(username: str):
|
|||||||
|
|
||||||
|
|
||||||
@bp.route("/groups/list")
|
@bp.route("/groups/list")
|
||||||
|
@login_required
|
||||||
def group_list():
|
def group_list():
|
||||||
"""Displays a list of all groups."""
|
"""Displays a list of all groups."""
|
||||||
|
|
||||||
@ -344,6 +351,7 @@ def group_list():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route("/groups/create")
|
@bp.route("/groups/create")
|
||||||
|
@login_required
|
||||||
def group_create():
|
def group_create():
|
||||||
"""Creation view for a new group.
|
"""Creation view for a new group.
|
||||||
|
|
||||||
@ -365,6 +373,7 @@ def group_create():
|
|||||||
|
|
||||||
|
|
||||||
@bp.route("/groups/update/<string:groupname>")
|
@bp.route("/groups/update/<string:groupname>")
|
||||||
|
@login_required
|
||||||
def group_update(groupname: str):
|
def group_update(groupname: str):
|
||||||
"""Detail and Update view for a group.
|
"""Detail and Update view for a group.
|
||||||
|
|
||||||
@ -395,6 +404,7 @@ def group_update(groupname: str):
|
|||||||
|
|
||||||
|
|
||||||
@bp.route("/groups/delete/<string:groupname>", methods=("GET", "POST"))
|
@bp.route("/groups/delete/<string:groupname>", methods=("GET", "POST"))
|
||||||
|
@login_required
|
||||||
def group_delete(groupname: str):
|
def group_delete(groupname: str):
|
||||||
"""Deletion view for a specific Group.
|
"""Deletion view for a specific Group.
|
||||||
|
|
||||||
|
@ -1,13 +1,18 @@
|
|||||||
from json import JSONEncoder, JSONDecoder, loads, dumps, JSONDecodeError
|
from json import JSONEncoder, JSONDecoder, loads, dumps, JSONDecodeError
|
||||||
|
|
||||||
from flask import Blueprint, request
|
from flask import request, g
|
||||||
from flask_restful import Resource
|
from flask_restful import Resource, abort
|
||||||
|
|
||||||
import lumi2.ldap as ldap
|
import lumi2.ldap as ldap
|
||||||
from lumi2.usermodel import User, Group
|
from lumi2.usermodel import User, Group
|
||||||
from lumi2.exceptions import InvalidStringFormatException
|
from lumi2.exceptions import InvalidStringFormatException
|
||||||
|
|
||||||
|
|
||||||
|
def _assert_is_authenticated():
|
||||||
|
if not g.is_authenticated:
|
||||||
|
abort(401, message="You are not logged in.")
|
||||||
|
|
||||||
|
|
||||||
class UserEncoder(JSONEncoder):
|
class UserEncoder(JSONEncoder):
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
if isinstance(obj, User):
|
if isinstance(obj, User):
|
||||||
@ -38,7 +43,9 @@ class UserResource(Resource):
|
|||||||
"""The UserResource is used for API access to users."""
|
"""The UserResource is used for API access to users."""
|
||||||
|
|
||||||
def get(self, username):
|
def get(self, username):
|
||||||
"""Returns the specified in JSON format."""
|
"""Returns the specified user in JSON format."""
|
||||||
|
|
||||||
|
_assert_is_authenticated()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = ldap.get_connection()
|
conn = ldap.get_connection()
|
||||||
@ -77,6 +84,8 @@ class GroupResource(Resource):
|
|||||||
A JSON string and HTTP status code.
|
A JSON string and HTTP status code.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_assert_is_authenticated()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = ldap.get_connection()
|
conn = ldap.get_connection()
|
||||||
except:
|
except:
|
||||||
@ -109,6 +118,8 @@ class GroupResource(Resource):
|
|||||||
json : str , status : int
|
json : str , status : int
|
||||||
A JSON string and HTTP status code.
|
A JSON string and HTTP status code.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_assert_is_authenticated()
|
||||||
|
|
||||||
group_dict = request.get_json()
|
group_dict = request.get_json()
|
||||||
if not isinstance(group_dict, dict):
|
if not isinstance(group_dict, dict):
|
||||||
@ -172,6 +183,8 @@ class GroupResource(Resource):
|
|||||||
A JSON string and HTTP status code.
|
A JSON string and HTTP status code.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_assert_is_authenticated()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = ldap.get_connection()
|
conn = ldap.get_connection()
|
||||||
except:
|
except:
|
||||||
@ -212,6 +225,8 @@ class GroupMemberResource(Resource):
|
|||||||
error message and HTTP error code are returned.
|
error message and HTTP error code are returned.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_assert_is_authenticated()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = ldap.get_connection()
|
conn = ldap.get_connection()
|
||||||
except:
|
except:
|
||||||
@ -262,6 +277,8 @@ class GroupMemberResource(Resource):
|
|||||||
error message and HTTP error code are returned.
|
error message and HTTP error code are returned.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
_assert_is_authenticated()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn = ldap.get_connection()
|
conn = ldap.get_connection()
|
||||||
except:
|
except:
|
||||||
|
Loading…
Reference in New Issue
Block a user