Commit 22493f04 authored by Robin Sonnabend's avatar Robin Sonnabend
Browse files

Implemented logging in and out

parent 92928cdc
import ldap
import hmac, hashlib
class User:
def __init__(self, username, groups):
self.username = username
self.groups = groups
def summarize(self):
return "{}:{}".format(self.username, ",".join(self.groups))
@staticmethod
def from_summary(summary):
name, groupstring = summary.split(":", 1)
groups = groupstring.split(",")
return User(name, groups)
@staticmethod
def from_hashstring(secure_string):
summary, hash = secure_string.split("=", 1)
return User.from_summary(summary)
class LdapManager:
def __init__(self, url, base):
self.connection = ldap.initialize(url)
self.base = base
def login(self, username, password):
if not self.authenticate(username, password):
return None
groups = list(map(lambda g: g.decode("utf-8"), self.groups(username)))
print(groups)
return User(username, groups)
def authenticate(self, username, password):
try:
self.connection.simple_bind_s("uid={},ou=users,{}".format(username, self.base), password)
return True
except ldap.INVALID_CREDENTIALS:
return False
return False
def groups(self, username):
result = []
for _, result_dict in self.connection.search_s(self.base, ldap.SCOPE_SUBTREE, "(memberUid={})".format(username), ["cn"]):
result.append(result_dict["cn"][0])
return result
class SecurityManager:
def __init__(self, key):
self.maccer = hmac.new(key.encode("utf-8"), digestmod=hashlib.sha512)
def hash_user(self, user):
maccer = self.maccer.copy()
summary = user.summarize()
maccer.update(summary.encode("utf-8"))
return "{}={}".format(summary, maccer.hexdigest())
def check_user(self, string):
parts = string.split("=", 1)
if len(parts) != 2:
# wrong format, expecting summary:hash
return False
summary, hash = map(lambda s: s.encode("utf-8"), parts)
maccer = self.maccer.copy()
maccer.update(summary)
return hmac.compare_digest(maccer.hexdigest().encode("utf-8"), hash)
...@@ -7,10 +7,12 @@ from flask_script import Manager, prompt ...@@ -7,10 +7,12 @@ from flask_script import Manager, prompt
from flask_migrate import Migrate, MigrateCommand from flask_migrate import Migrate, MigrateCommand
#from flask_socketio import SocketIO #from flask_socketio import SocketIO
from celery import Celery from celery import Celery
from functools import wraps
import config import config
from shared import db, date_filter, datetime_filter from shared import db, date_filter, datetime_filter, ldap_manager, security_manager
from utils import is_past, mail_manager, url_manager from utils import is_past, mail_manager, url_manager
from views.forms import LoginForm
app = Flask(__name__) app = Flask(__name__)
app.config.from_object(config) app.config.from_object(config)
...@@ -35,26 +37,63 @@ app.jinja_env.lstrip_blocks = True ...@@ -35,26 +37,63 @@ app.jinja_env.lstrip_blocks = True
app.jinja_env.filters["datify"] = date_filter app.jinja_env.filters["datify"] = date_filter
app.jinja_env.filters["datetimify"] = datetime_filter app.jinja_env.filters["datetimify"] = datetime_filter
app.jinja_env.filters["url_complete"] = url_manager.complete app.jinja_env.filters["url_complete"] = url_manager.complete
app.jinja_env.tests["auth_valid"] = security_manager.check_user
import tasks import tasks
from auth import User
def check_login():
return "auth" in session and security_manager.check_user(session["auth"])
def current_user():
if not check_login():
return None
return User.from_hashstring(session["auth"])
def login_required(function):
@wraps(function)
def decorated_function(*args, **kwargs):
if check_login():
return function(*args, **kwargs)
else:
return redirect(url_for("login", next=request.url))
return decorated_function
app.jinja_env.globals.update(check_login=check_login)
app.jinja_env.globals.update(current_user=current_user)
# blueprints here # blueprints here
@app.route("/") @app.route("/")
@login_required
def index(): def index():
return render_template("index.html") return render_template("index.html")
@app.route("/imprint/") @app.route("/login", methods=["GET", "POST"])
def imprint(): def login():
return render_template("imprint.html") if "auth" in session:
flash("You are already logged in.", "alert-success")
return redirect(url_for(request.args.get("next") or "index"))
form = LoginForm()
if form.validate_on_submit():
user = ldap_manager.login(form.username.data, form.password.data)
if user is not None:
session["auth"] = security_manager.hash_user(user)
flash("Login successful, {}!".format(user.username), "alert-success")
return redirect(request.args.get("next") or url_for("index"))
else:
flash("Wrong login data. Try again.", "alert-error")
return render_template("login.html", form=form)
@app.route("/contact/") @app.route("/logout")
def contact(): @login_required
return render_template("contact.html") def logout():
if "auth" in session:
session.pop("auth")
else:
flash("You are not logged in.", "alert-error")
return redirect(url_for(".index"))
@app.route("/privacy/")
def privacy():
return render_template("privacy.html")
if __name__ == "__main__": if __name__ == "__main__":
manager.run() manager.run()
...@@ -71,3 +71,7 @@ def date_filter_long(date): ...@@ -71,3 +71,7 @@ def date_filter_long(date):
return date.strftime("%A, %d.%m.%Y, Kalenderwoche %W") return date.strftime("%A, %d.%m.%Y, Kalenderwoche %W")
def time_filter(time): def time_filter(time):
return time.strftime("%H:%m") return time.strftime("%H:%m")
from auth import LdapManager, SecurityManager
ldap_manager = LdapManager(config.LDAP_PROVIDER_URL, config.LDAP_BASE)
security_manager = SecurityManager(config.SECURITY_KEY)
...@@ -29,6 +29,13 @@ ...@@ -29,6 +29,13 @@
<li><a href="{{url_for("index")}}">Zuhause</a></li> <li><a href="{{url_for("index")}}">Zuhause</a></li>
{# todo: add more links #} {# todo: add more links #}
</ul> </ul>
<ul class="nav navbar-nav navbar-right">
{% if check_login() %}
<li><a href="{{url_for("logout")}}">Logout</a></li>
{% else %}
<li><a href="{{url_for("login")}}">Login</a></li>
{% endif %}
</ul>
</div> </div>
</div> </div>
</nav> </nav>
......
{% extends "layout.html" %}
{% from "macros.html" import render_form %}
{% block title %}Login{% endblock %}
{% block content %}
{{render_form(form)}}
{% endblock %}
<!-- Taken from https://gist.github.com/bearz/7394681 and modified
to not render a label for the CRSFTokenField -->
{# Renders field for bootstrap 3 standards.
Params:
field - WTForm field
kwargs - pass any arguments you want in order to put them into the html attributes.
There are few exceptions: for - for_, class - class_, class__ - class_
Example usage:
{{ macros.render_field(form.email, placeholder='Input email', type='email') }}
#}
{% macro render_field(field, label_visible=true) -%}
<div class="form-group {% if field.errors %}has-error{% endif %} {{ kwargs.pop('class_', '') }}">
{% if field.type != 'HiddenField' and field.type !='CSRFTokenField' and label_visible %}
<label for="{{ field.id }}" class="control-label">{{ field.label }}</label>
<!--<span onclick="el=document.getElementById('{{field.id}}-description');el.style.display=(el.style.display=='none'?'flex':'none')" class="field-description-questionmark">?</span>-->
{% endif %}
{{ field(title=field.description, class_='form-control', **kwargs) }}
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{% endif %}
</div>
<div id="{{field.id}}-description" style="display:none" class="field-description">{{field.description}}</div>
{%- endmacro %}
{# Renders checkbox fields since they are represented differently in bootstrap
Params:
field - WTForm field (there are no check, but you should put here only BooleanField.
kwargs - pass any arguments you want in order to put them into the html attributes.
There are few exceptions: for - for_, class - class_, class__ - class_
Example usage:
{{ macros.render_checkbox_field(form.remember_me) }}
#}
{% macro render_checkbox_field(field) -%}
<div class="checkbox {% if field.errors %}has-error{% endif %}">
<label>
{{ field(type='checkbox', **kwargs) }} {{ field.label }}
</label>
<span onclick="el=document.getElementById('{{field.id}}-description');el.style.display=(el.style.display=='none'?'flex':'none')" class="field-description-questionmark">?</span>
{% if field.errors %}
{% for e in field.errors %}
<p class="help-block">{{ e }}</p>
{% endfor %}
{% endif %}
</div>
<div id="{{field.id}}-description" style="display:none" class="field-description">{{field.description}}</div>
{%- endmacro %}
{# Renders radio field
Params:
field - WTForm field (there are no check, but you should put here only BooleanField.
kwargs - pass any arguments you want in order to put them into the html attributes.
There are few exceptions: for - for_, class - class_, class__ - class_
Example usage:
{{ macros.render_radio_field(form.answers) }}
#}
{% macro render_radio_field(field) -%}
{% for value, label, _ in field.iter_choices() %}
<div class="radio">
<label>
<input type="radio" name="{{ field.id }}" id="{{ field.id }}" value="{{ value }}">{{ label }}
</label>
</div>
{% endfor %}
{%- endmacro %}
{# Renders WTForm in bootstrap way. There are two ways to call function:
- as macros: it will render all field forms using cycle to iterate over them
- as call: it will insert form fields as you specify:
e.g. {% call macros.render_form(form, action_url=url_for('login_view'), action_text='Login',
class_='login-form') %}
{{ macros.render_field(form.email, placeholder='Input email', type='email') }}
{{ macros.render_field(form.password, placeholder='Input password', type='password') }}
{{ macros.render_checkbox_field(form.remember_me, type='checkbox') }}
{% endcall %}
Params:
form - WTForm class
action_url - url where to submit this form
action_text - text of submit button
class_ - sets a class for form
#}
{% macro render_form(form, action_url='', action_text='Submit', class_='', btn_class='btn btn-default') -%}
<form method="POST" action="{{ action_url }}" role="form" class="{{ class_ }}">
{{ form.hidden_tag() if form.hidden_tag }}
{% if caller %}
{{ caller() }}
{% else %}
{% for f in form %}
{% if f.type == 'BooleanField' %}
{{ render_checkbox_field(f) }}
{% elif f.type == 'RadioField' %}
{{ render_radio_field(f) }}
{% else %}
{{ render_field(f) }}
{% endif %}
{% endfor %}
{% endif %}
<button type="submit" class="{{ btn_class }}">{{ action_text }} </button>
</form>
{%- endmacro %}
from flask_wtf import FlaskForm
from wtforms import StringField, PasswordField
from wtforms.validators import InputRequired
class LoginForm(FlaskForm):
username = StringField("User", validators=[InputRequired("Please input the username.")])
password = PasswordField("Password", validators=[InputRequired("Please input the password.")])
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment