Skip to content
Snippets Groups Projects
Commit 22493f04 authored by Robin Sonnabend's avatar Robin Sonnabend
Browse files

Implemented logging in and out

parent 92928cdc
No related branches found
No related tags found
No related merge requests found
auth.py 0 → 100644
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)
alembic alembic==0.8.10
blinker amqp==2.1.4
Flask appdirs==1.4.0
Flask-Migrate argh==0.26.2
Flask-Script billiard==3.5.0.2
Flask-SQLAlchemy blinker==1.4
Flask-WTF celery==4.0.2
Flask-SocketIO click==6.7
Jinja2 Flask==0.12
Mako Flask-Migrate==2.0.3
MarkupSafe Flask-Script==2.0.5
psycopg2 Flask-SocketIO==2.8.4
python-editor Flask-SQLAlchemy==2.1
SQLAlchemy Flask-WTF==0.14.2
Werkzeug itsdangerous==0.24
wheel Jinja2==2.9.5
WTForms kombu==4.0.2
celery[redis] Mako==1.0.6
regex MarkupSafe==0.23
watchdog packaging==16.8
pathtools==0.1.2
psycopg2==2.6.2
pyldap==2.4.28
pyparsing==2.1.10
python-editor==1.0.3
python-engineio==1.2.2
python-socketio==1.7.1
pytz==2016.10
PyYAML==3.12
redis==2.10.5
regex==2017.2.8
six==1.10.0
SQLAlchemy==1.1.5
vine==1.1.3
watchdog==0.8.3
Werkzeug==0.11.15
WTForms==2.1
...@@ -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.")])
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment