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/__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

View File

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

View File

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

View File

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

View File

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

View File

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