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/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
|
||||
|
@ -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
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>
|
||||
</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>
|
||||
|
@ -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">
|
||||
|
@ -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 %}
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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:
|
||||
|
Loading…
Reference in New Issue
Block a user