initial commit
This commit is contained in:
commit
602127cbdc
7 changed files with 353 additions and 0 deletions
3
.gitignore
vendored
Normal file
3
.gitignore
vendored
Normal file
|
@ -0,0 +1,3 @@
|
|||
.venv/
|
||||
*.pyc
|
||||
config.json
|
84
ldap_frontend/__init__.py
Normal file
84
ldap_frontend/__init__.py
Normal 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")
|
117
ldap_frontend/helpers/ldap.py
Normal file
117
ldap_frontend/helpers/ldap.py
Normal 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
|
44
ldap_frontend/templates/layout/default.html
Normal file
44
ldap_frontend/templates/layout/default.html
Normal 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>
|
20
ldap_frontend/templates/login.html
Normal file
20
ldap_frontend/templates/login.html
Normal 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 %}
|
77
ldap_frontend/templates/selfservice.html
Normal file
77
ldap_frontend/templates/selfservice.html
Normal 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
8
requirements.txt
Normal 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
|
Loading…
Reference in a new issue