initial commit

This commit is contained in:
Franzi 2021-12-21 08:30:36 +01:00
commit 602127cbdc
Signed by: kunsi
GPG key ID: 12E3D2136B818350
7 changed files with 353 additions and 0 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.venv/
*.pyc
config.json

84
ldap_frontend/__init__.py Normal file
View file

@ -0,0 +1,84 @@
from json import load
from os import environ
from flask import Flask, flash, redirect, request, session, url_for
from ldap3.core.exceptions import LDAPException
from .helpers.ldap import login_required, try_auth, get_user, template, update_user
app = Flask(__name__)
app.secret_key = environ.get("FLASK_SECRET_KEY", default="test")
with open(environ["APP_CONFIG"]) as f:
APP_CONFIG = load(f)
@app.route("/")
def slash():
return redirect(url_for("login"))
@app.route("/login", methods=["GET", "POST"])
def login():
session["is_logged_in"] = False
if request.method == "POST":
if try_auth(
request.form["username"],
request.form["password"],
):
session["is_logged_in"] = True
session["username"] = request.form["username"]
session["password"] = request.form["password"]
flash("logged in")
return redirect(url_for("selfservice"))
else:
flash("username or password is wrong")
return template(None, "login.html")
@app.route("/logout")
def logout():
session["is_logged_in"] = False
session["username"] = ""
session["password"] = ""
flash("logged out")
return redirect(url_for("login"))
@app.route("/selfservice", methods=["GET", "POST"])
@login_required
def selfservice(ldap):
if request.method == "POST":
try:
update_user(
ldap,
session["username"],
{
"givenName": request.form["givenName"],
"sn": request.form["sn"],
"cn": "{} {}".format(
request.form["givenName"],
request.form["sn"],
),
"mail": request.form["mail"]
},
)
flash("data updated")
return redirect(url_for("selfservice"))
except LDAPException as e:
app.logger.error(
"Updating {} failed: {}\n{}".format(
APP_CONFIG["template"]["user_dn"].format(session["username"]),
repr(e),
repr(request.form),
),
)
flash(e)
return template(ldap, "selfservice.html")

View file

@ -0,0 +1,117 @@
from functools import wraps
from json import load
from os import environ
from flask import redirect, session, url_for, render_template
from ldap3 import ALL, Connection, Server
from ldap3 import ALL_ATTRIBUTES, MODIFY_REPLACE
from ldap3.core.exceptions import LDAPException
with open(environ["APP_CONFIG"]) as f:
APP_CONFIG = load(f)
def login_required(func):
@wraps(func)
def wrapper(*args, **kwargs):
if session["is_logged_in"]:
if try_auth(
session["username"],
session["password"],
):
ldap = connect()
return func(ldap, *args, **kwargs)
else:
return redirect(url_for("login"))
else:
return redirect(url_for("login"))
return wrapper
def admin_required(func):
@wraps(func)
@login_required
def wrapper(*args, **kwargs):
if session["is_logged_in"]:
if try_auth(
session["username"],
session["password"],
):
ldap = connect()
return func(ldap, *args, **kwargs)
else:
return redirect(url_for("login"))
else:
return redirect(url_for("login"))
return wrapper
def try_auth(user, password):
try:
connect(
user=APP_CONFIG["template"]["user_dn"].format(user),
password=password,
)
return True
except LDAPException:
return False
def connect(user=None, password=None):
server = Server(APP_CONFIG["ldap"]["server"])
if not user and not password:
user = APP_CONFIG["ldap"]["username"]
password = APP_CONFIG["ldap"]["password"]
conn = Connection(
server,
user=user,
password=password,
)
conn.bind()
return conn
def get_user(ldap, username):
ldap.search(
APP_CONFIG["ldap"]["user_base"],
APP_CONFIG["template"]["user_search"].format(username),
attributes=ALL_ATTRIBUTES,
)
if len(ldap.entries) == 1:
return ldap.entries[0]
else:
raise UserNotFoundException(username)
def update_user(ldap, username, settings):
attrs = {}
for attr, value in settings.items():
attrs[attr] = [(MODIFY_REPLACE, value)]
return ldap.modify(
APP_CONFIG["template"]["user_dn"].format(username),
attrs,
)
def template(ldap, name, **kwargs):
user = None
if ldap:
user = get_user(ldap, session["username"])
return render_template(
name,
APP_CONFIG=APP_CONFIG,
CURRENT_USER=user,
**kwargs,
)
class UserNotFoundException(Exception):
pass

View file

@ -0,0 +1,44 @@
<!DOCTYPE html>
<html>
<head>
<title>{% block title %}LDAP{% endblock %} | {{ APP_CONFIG["title"] }}</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- FIXME deploy CSS/JS locally -->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-1BmE4kWBq78iYhFldvKuhfTAU6auU8tT94WrHftjDbrCEXSU1oBoqyl2QvZ6jIW3" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ka7Sk0Gln4gmtz2MlQnikT1wXgYsOg+OMhuP+IlRH9sENBO0LRn5q+8nbTov4+1p" crossorigin="anonymous"></script>
</head>
<body>
{% if session.is_logged_in %}
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<div class="container-fluid">
<span class="navbar-brand">{{ APP_CONFIG["title"] }}</span>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<li class="nav-item">
<a class="nav-link" href="{{ url_for("selfservice") }}">self service</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">groups</a>
</li>
</ul>
<span class="navbar-text navbar-right">Signed in as <em>{{ CURRENT_USER["uid"] }}</em> - <a href="{{ url_for("logout") }}">logout</a></span>
</div>
</div>
</nav>
{% endif %}
<div class="container-md p-3">
{% for message in get_flashed_messages() %}
<div class="alert alert-primary" role="alert">
{{ message }}
</div>
{% endfor %}
{% block content %}
{% endblock %}
</div>
</body>
</html>

View file

@ -0,0 +1,20 @@
{% extends "layout/default.html" %}
{% block content %}
<form action="{{ url_for("login") }}" method="post">
<fieldset>
<legend>Login</legend>
<div class="mb-3">
<label for="username" class="form-label">Username</label>
<input type="text" name="username" id="username" required class="form-control">
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input type="password" name="password" id="password" required class="form-control">
</div>
<input type="submit" value="Login" class="btn btn-primary mb-3">
</fieldset>
</form>
{% endblock %}

View file

@ -0,0 +1,77 @@
{% extends "layout/default.html" %}
{% block title %}self service{% endblock %}
{% block content %}
<form action="{{ url_for("selfservice") }}" method="post" class="row g-3 needs-validation">
<fieldset>
<legend>user data</legend>
<div class="row mb-3">
<label for="uid" class="form-label col-sm-2">uid</label>
<div class="col-sm-10">
<input type="text" name="uid" id="uid" value="{{ CURRENT_USER["uid"]|e }}" disabled readonly class="form-control" aria-describedby="uidHelp">
<div id="uidHelp" class="form-text">contact an administrator if you want to change this</div>
</div>
</div>
<div class="row mb-3">
<label for="cn" class="form-label col-sm-2">common name</label>
<div class="col-sm-10">
<input type="text" name="cn" id="cn" value="{{ CURRENT_USER["cn"]|e }}" disabled readonly class="form-control" aria-describedby="cnHelp">
<div id="cnHelp" class="form-text">gets adjusted automatically</div>
</div>
</div>
<div class="row mb-3">
<label for="sn" class="form-label col-sm-2">surname</label>
<div class="col-sm-10">
<input type="text" name="sn" id="sn" value="{{ CURRENT_USER["sn"]|e }}" required class="form-control">
</div>
</div>
<div class="row mb-3">
<label for="givenName" class="form-label col-sm-2">given name</label>
<div class="col-sm-10">
<input type="text" name="givenName" id="givenName" value="{{ CURRENT_USER["givenName"]|e }}" required class="form-control">
</div>
</div>
<div class="row mb-3">
<label for="mail" class="form-label col-sm-2">email address</label>
<div class="col-sm-10">
<input type="email" name="mail" id="mail" value="{{ CURRENT_USER["mail"]|e }}" required class="form-control">
</div>
</div>
<input type="submit" value="update" class="btn btn-primary mb-3"><br>
</fieldset>
</form>
<form action="{{ url_for("selfservice") }}" method="post">
<fieldset>
<legend>password</legend>
<div class="row mb-3">
<label for="current" class="form-label col-sm-2">current password</label>
<div class="col-sm-10">
<input type="password" name="current" id="current" value="" class="form-control">
</div>
</div>
<div class="row mb-3">
<label for="new" class="form-label col-sm-2">new password</label>
<div class="col-sm-10">
<input type="password" name="new" id="new" value="" class="form-control">
</div>
</div>
<div class="row mb-3">
<label for="repeat" class="form-label col-sm-2">repeat new password</label>
<div class="col-sm-10">
<input type="password" name="repeat" id="repeat" value="" class="form-control">
</div>
</div>
<input type="submit" value="change password" class="btn btn-primary mb-3"><br>
</fieldset>
</form>
{% endblock %}

8
requirements.txt Normal file
View file

@ -0,0 +1,8 @@
click==8.0.3
Flask==2.0.2
itsdangerous==2.0.1
Jinja2==3.0.3
ldap3==2.9.1
MarkupSafe==2.0.1
pyasn1==0.4.8
Werkzeug==2.0.2