feat: implement authentication

This commit is contained in:
Julian Lobbes 2022-11-30 21:06:34 +01:00
parent 8d73839de7
commit d9ef64d983
9 changed files with 133 additions and 6 deletions

View File

@ -11,6 +11,7 @@ services:
- ./lumi2/__init__.py:/app/lumi2/__init__.py:ro
- ./lumi2/exceptions.py:/app/lumi2/exceptions.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/webapi.py:/app/lumi2/webapi.py:ro
- ./lumi2/usermanager.py:/app/lumi2/usermanager.py:ro

View File

@ -16,6 +16,7 @@ def create_app(test_config=None):
app = Flask(__name__, instance_relative_config=True)
app.config.from_mapping(
SECRET_KEY='ChangeMeInProduction',
ADMIN_PASSWORD='pbkdf2:sha256:260000$J9yKJOAvWfvaO9Op$f959d88402f67a5143808a00e35d17e636546f1caf5a85c1b6ab1165d1780448',
SITE_URL='https://www.example.com/',
SITE_TITLE='LUMI 2',
SITE_AUTHOR='LUMI 2 Development Team',
@ -44,6 +45,9 @@ def create_app(test_config=None):
except OSError:
pass
from . import auth
app.register_blueprint(auth.bp)
from . import usermanager
app.register_blueprint(usermanager.bp)
app.add_url_rule('/', endpoint='index')

68
lumi2/auth.py Normal file
View 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

View 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 %}

View File

@ -45,11 +45,19 @@
<a class="nav-link" href="{{ url_for('usermanager.group_list') }}">Groups</a>
</li>
</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"">
<a class="btn btn-primary"
href="#"
href="{{ url_for('auth.login') }}"
role="button">Log In</a>
</div>
{% endif%}
</div>
</div>
</nav>

View File

@ -11,7 +11,6 @@
<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>
<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 class="row border rounded m-2">
<table class="table table-hover align-middle" id="tableMembers">

View File

@ -10,6 +10,6 @@
>
</div>
<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>
{% endblock content %}

View File

@ -16,6 +16,7 @@ from wtforms import (
)
from wtforms.validators import InputRequired, Email, EqualTo
from lumi2.auth import login_required
import lumi2.ldap as ldap
from lumi2.usermodel import User, Group
from lumi2.exceptions import InvalidStringFormatException, InvalidImageException
@ -59,6 +60,7 @@ def index():
@bp.route("/users/view/<string:username>")
@login_required
def user_view(username: str):
"""Detail view for a specific User.
@ -81,6 +83,7 @@ def user_view(username: str):
@bp.route("/users/list")
@login_required
def user_list():
"""Displays a list of all users."""
@ -198,6 +201,7 @@ class UserCreationForm(UserUpdateForm):
@bp.route("/users/create", methods=("GET", "POST"))
@login_required
def user_create():
"""Creation view for a new User.
@ -236,6 +240,7 @@ def user_create():
@bp.route("/users/update/<string:username>", methods=("GET", "POST"))
@login_required
def user_update(username: str):
"""Update view for a specific User.
@ -285,6 +290,7 @@ def user_update(username: str):
@bp.route("/users/delete/<string:username>", methods=("GET", "POST"))
@login_required
def user_delete(username: str):
"""Deletion view for a specific User.
@ -327,6 +333,7 @@ def user_delete(username: str):
@bp.route("/groups/list")
@login_required
def group_list():
"""Displays a list of all groups."""
@ -344,6 +351,7 @@ def group_list():
@bp.route("/groups/create")
@login_required
def group_create():
"""Creation view for a new group.
@ -365,6 +373,7 @@ def group_create():
@bp.route("/groups/update/<string:groupname>")
@login_required
def group_update(groupname: str):
"""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"))
@login_required
def group_delete(groupname: str):
"""Deletion view for a specific Group.

View File

@ -1,13 +1,18 @@
from json import JSONEncoder, JSONDecoder, loads, dumps, JSONDecodeError
from flask import Blueprint, request
from flask_restful import Resource
from flask import request, g
from flask_restful import Resource, abort
import lumi2.ldap as ldap
from lumi2.usermodel import User, Group
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):
def default(self, obj):
if isinstance(obj, User):
@ -38,7 +43,9 @@ class UserResource(Resource):
"""The UserResource is used for API access to users."""
def get(self, username):
"""Returns the specified in JSON format."""
"""Returns the specified user in JSON format."""
_assert_is_authenticated()
try:
conn = ldap.get_connection()
@ -77,6 +84,8 @@ class GroupResource(Resource):
A JSON string and HTTP status code.
"""
_assert_is_authenticated()
try:
conn = ldap.get_connection()
except:
@ -109,6 +118,8 @@ class GroupResource(Resource):
json : str , status : int
A JSON string and HTTP status code.
"""
_assert_is_authenticated()
group_dict = request.get_json()
if not isinstance(group_dict, dict):
@ -172,6 +183,8 @@ class GroupResource(Resource):
A JSON string and HTTP status code.
"""
_assert_is_authenticated()
try:
conn = ldap.get_connection()
except:
@ -212,6 +225,8 @@ class GroupMemberResource(Resource):
error message and HTTP error code are returned.
"""
_assert_is_authenticated()
try:
conn = ldap.get_connection()
except:
@ -262,6 +277,8 @@ class GroupMemberResource(Resource):
error message and HTTP error code are returned.
"""
_assert_is_authenticated()
try:
conn = ldap.get_connection()
except: