diff --git a/docker-compose.yml b/docker-compose.yml
index c0e5858..c8af98d 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -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
diff --git a/lumi2/__init__.py b/lumi2/__init__.py
index c2508a6..eef5011 100644
--- a/lumi2/__init__.py
+++ b/lumi2/__init__.py
@@ -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')
diff --git a/lumi2/auth.py b/lumi2/auth.py
new file mode 100644
index 0000000..22a80f5
--- /dev/null
+++ b/lumi2/auth.py
@@ -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
diff --git a/lumi2/templates/auth/login.html b/lumi2/templates/auth/login.html
new file mode 100644
index 0000000..03b57a7
--- /dev/null
+++ b/lumi2/templates/auth/login.html
@@ -0,0 +1,20 @@
+{% extends 'base.html' %}
+
+{% block content %}
+
+
+{% endblock content %}
diff --git a/lumi2/templates/base.html b/lumi2/templates/base.html
index 3c6f8fa..f401c45 100644
--- a/lumi2/templates/base.html
+++ b/lumi2/templates/base.html
@@ -45,11 +45,19 @@
Groups
+ {% if g.is_authenticated %}
+
+ {% else %}
+ {% endif%}
diff --git a/lumi2/templates/usermanager/group_create.html b/lumi2/templates/usermanager/group_create.html
index f3d4cce..cc42d17 100644
--- a/lumi2/templates/usermanager/group_create.html
+++ b/lumi2/templates/usermanager/group_create.html
@@ -11,7 +11,6 @@
Cancel
-
diff --git a/lumi2/templates/usermanager/index.html b/lumi2/templates/usermanager/index.html
index c873a0f..10914d4 100644
--- a/lumi2/templates/usermanager/index.html
+++ b/lumi2/templates/usermanager/index.html
@@ -10,6 +10,6 @@
>
-
This site is still under construction.
+
This site is still under construction.
{% endblock content %}
diff --git a/lumi2/usermanager.py b/lumi2/usermanager.py
index 127d9a8..9203a96 100644
--- a/lumi2/usermanager.py
+++ b/lumi2/usermanager.py
@@ -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/")
+@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/", 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/", 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/")
+@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/", methods=("GET", "POST"))
+@login_required
def group_delete(groupname: str):
"""Deletion view for a specific Group.
diff --git a/lumi2/webapi.py b/lumi2/webapi.py
index 8c0e50e..77101f3 100644
--- a/lumi2/webapi.py
+++ b/lumi2/webapi.py
@@ -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: