Commit 222161dc authored by Alex's avatar Alex
Browse files

Reverted to template without migrations, with auth package

parent e10936db
......@@ -8,6 +8,11 @@ app = Flask(__name__, static_url_path=get_static_url())
app.url_map.strict_slashes = False
app.config.from_object(get_app_config())
# Setup authentication
import imperial_ldap
imperial_ldap.init_app(app)
imperial_ldap.set_auth_type(imperial_ldap.config.AuthType.SESSION, redirect_url='auth.login')
db.init_app(app)
# TODO Find a workaround for migrations/db.create_all()
......@@ -29,21 +34,6 @@ from blueprints.auth import auth_blueprint
app.register_blueprint(home_blueprint)
app.register_blueprint(auth_blueprint)
# @app.route('/test-db')
# def test_db():
# try:
# rows = ""
# for user in db.session.query(User).all():
# rows += str(user) + " "
# for e in db.session.query(Entity).all():
# rows += str(e) + " "
# print("All rows:", rows)
# return rows
# except:
# return "App database error"
# Hook any custom Jinja templating functions
from config import CUSTOM_TEMPLATE_FUNCTIONS
......
# LDAP server config
LDAP_URL = "ldaps://ldaps-vip.cc.ic.ac.uk:636"
LDAP_DN = "OU=Users,OU=Imperial College (London),DC=ic,DC=ac,DC=uk"
# Relevant IC LDAP attributes
TITLE = "extensionAttribute6"
NAME = "givenName"
SURNAME = "sn"
DN = "distinguishedName"
MEMBERSHIPS = "memberOf"
# List of attributes to be parsed into dictionaries
ATTRIBUTES_TO_SERIALISE = [DN, MEMBERSHIPS]
\ No newline at end of file
from .constants import *
from .ldap_handler import ldap_service
WHITE_LIST = ["ictsec"]
def ldap_login(username, password):
"""
Perform (a) LDAP authentication and (b) additional (app specific) verifications
before granting access and returning the user LDAP attributes 'name, surname, title and memberships'.
"""
ldap_attributes = ldap_service.ldap_login(
username, password, query_attrs=(TITLE, NAME, SURNAME, DN, MEMBERSHIPS)
)
return custom_authentication_checks(username, ldap_attributes)
def custom_authentication_checks(username, ldap_attributes):
# ADD HERE CUSTOM HIGHER-LEVEL CHECKS
# e.g.:
#
# if 'doc' not in dict_attrs[DN]['OU']: # is 'doc' in the organisation sub-attribute?
# if 'doc-all-students' not in dict_attrs[MEMBERSHIPS]['CN']: # is 'doc-all-students' among the memberships?
# raise ldap.INVALID_CREDENTIALS # raise INVALID_CREDENTIALS exception
return ldap_attributes
# To enforce a distinction between "student" and "staff", the `ldap_constant_TITLE` ldap attribute is
# requested (see above) and associated to the user model. The following decorator is then an example
# on how to leverage the title to implement title-based access (where DEFAULT_REDIRECTION is assigned
# a convenient application route).
# For inspiration on how to implement title-based access, refer to emarking's source code:
# https://gitlab.doc.ic.ac.uk/edtech/emarking
#
# def role_required(access_role, redirection_url=None):
# def decorator(f):
# @wraps(f)
# def decorated_function(*args, **kwargs):
# if current_user.title == access_role:
# return f(*args, **kwargs)
# return redirect(url_for(redirection_url or DEFAULT_REDIRECTION))
# return decorated_function
# return decorator
import itertools
import re
from collections import defaultdict
from .constants import *
import ldap
# Used to parse key-value LDAP attributes
KEY_VAL_ATT_REGEX = "([A-Za-z0-9]+)=([A-Za-z0-9-@]+)"
USERNAME_FILTER_TEMPLATE = "(&(objectClass=user)(sAMAccountName=%s))"
BINDING_TEMPLATE = "%s@IC.AC.UK"
class LdapConnectionHandler:
"""
Adapter for the python-LDAP library.
The class simplifies the interaction with python-LDAP
to initialise an LDAPObject and handle the retrieval of
relevant LDAP user attributes.
EXAMPLE USAGE FOR LOGIN PURPOSES:
1. An LDAP object is initialised with LDAP server URL and base distinct name
2. A new connection is established with connect()
3. The LDAP binding for a given username and password is performed with ldap_login()
4. Relevant attributes are queried with query_attributes().
"""
def __init__(self):
self.base_dn = LDAP_DN
self.server_url = LDAP_URL
def ldap_login(self, username, password, query_attrs):
"""
Performs basic LDAP authentication by binding on a fresh connection with `username` and `password`.
Throws INVALID_CREDENTIALS exception if authentication fails. On successful authentication,
retrieves the values stored on the LDAP server associated to `username` for the given `attributes`.
:param username: username credential
:param password: password credential
:param attributes: names of the attributes to filter for
:return: attr_name -> attr_value dict for given username
"""
connection = ldap.initialize(self.server_url)
connection.set_option(ldap.OPT_X_TLS_REQUIRE_CERT, ldap.OPT_X_TLS_ALLOW)
connection.set_option(ldap.OPT_X_TLS_NEWCTX, 0)
connection.simple_bind_s(BINDING_TEMPLATE % username, password)
attributes = parse_ldap_attributes(
self.raw_attributes(username, query_attrs, connection)
)
connection.unbind_s()
return attributes
def raw_attributes(self, username, attributes, connection):
ldap_filter = USERNAME_FILTER_TEMPLATE % username
raw_res = connection.search(
self.base_dn, ldap.SCOPE_SUBTREE, ldap_filter, attributes
)
res_type, res_data = connection.result(raw_res)
_, filtered_attributes = res_data[0]
return filtered_attributes.items()
###################################################################
# U T I L I T I E S #
###################################################################
def parse_ldap_attributes(attributes):
return {
k: ldap_attributes_to_dictionary(vs)
if k in ATTRIBUTES_TO_SERIALISE
else vs[0].decode("utf-8")
for k, vs in attributes
}
def ldap_attributes_to_dictionary(attr_values):
items = (
re.findall(KEY_VAL_ATT_REGEX, item.decode("utf-8").replace(",", " "))
for item in attr_values
)
d = defaultdict(set)
for k, v in itertools.chain.from_iterable(items):
d[k].add(v)
return d
ldap_service = LdapConnectionHandler()
from flask import Blueprint, render_template, request
from auth.ldap_auth import ldap_login
import ldap
from flask import Blueprint, render_template, request, redirect, flash
from imperial_ldap.auth import ldap_login, ldap_logout
from config import url_for2
auth_blueprint = Blueprint('auth', __name__, url_prefix='/auth')
auth_blueprint = Blueprint("auth", __name__, url_prefix="/auth")
@auth_blueprint.route('/login', methods=['GET', 'POST'])
@auth_blueprint.route("/login", methods=["GET", "POST"])
def login():
if request.method == 'GET':
return render_template('login.html')
if request.method == "GET":
return render_template("login.html")
# Handle post request
username = request.form.get('username')
password = request.form.get('password')
username = request.form.get("username")
password = request.form.get("password")
if username and password:
try:
r = ldap_login(username, password)
return f"Logged in! LDAP response: {r}"
except ldap.INVALID_CREDENTIALS:
return "Invalid credentials."
return "please provide a username and password"
\ No newline at end of file
user = ldap_login(username, password)
if user:
# we're logged in
return redirect(url_for2("home.dashboard"))
else:
# bad credentials
flash("Invalid login credentials.")
return redirect(url_for2("auth.login"))
return "Please provide a username and password"
@auth_blueprint.route("/logout", methods=["GET"])
def logout():
ldap_logout()
return redirect(url_for2("home.index"))
......@@ -2,22 +2,26 @@ from flask import Blueprint, render_template, request, redirect
from config import url_for2
from database.db import db
from models.entity import Entity
from imperial_ldap.auth import login_required, get_user_from_session
home_blueprint = Blueprint('home', __name__, url_prefix='/')
@home_blueprint.route('')
def home():
return render_template('index.html')
def index():
user = get_user_from_session() # If there is no user logged, this returns None
return render_template('index.html', user=user)
@home_blueprint.route('/hello/<name>')
def hello(name: str):
return "Hello, " + name
@home_blueprint.route('/dashboard')
@login_required
def dashboard(user):
return render_template('dashboard.html', user=user)
# Example CRUD route
@home_blueprint.route('/entities', methods=['GET', 'POST'])
def entities():
@login_required
def entities(user):
if request.method == "GET":
try:
rows = ""
......
......@@ -35,17 +35,23 @@ def get_db_url():
return url
class CommonConfig:
APP_NAME = APP_NAME
URL_PREFIX = URL_PREFIX
SECRET_KEY = "my-secrefasdfasdfasdfasdt-key"
SESSION_COOKIE_SECURE = True
SQLALCHEMY_DATABASE_URI = get_db_url()
SQLALCHEMY_TRACK_MODIFICATIONS = False
# Flask App settings for production environment
class ProductionConfig:
class ProductionConfig(CommonConfig):
DEBUG = False
APPLICATION_ROOT = f"/{APP_NAME}"
SQLALCHEMY_DATABASE_URI = get_db_url()
SQLALCHEMY_TRACK_MODIFICATIONS = False
# Flask App settings for local development enviroment
class DevelopmentConfig:
class DevelopmentConfig(CommonConfig):
DEBUG = True
SQLALCHEMY_DATABASE_URI = get_db_url()
SQLALCHEMY_TRACK_MODIFICATIONS = False
#content {
margin-left: 1%;
margin-right: 1%;
}
\ No newline at end of file
html,
body {
height: 100%;
}
body {
display: -ms-flexbox;
display: -webkit-box;
display: flex;
-ms-flex-align: center;
-ms-flex-pack: center;
-webkit-box-align: center;
align-items: center;
-webkit-box-pack: center;
justify-content: center;
padding-top: 40px;
padding-bottom: 40px;
background-color: #f5f5f5;
}
.form-signin {
width: 100%;
max-width: 330px;
padding: 15px;
margin: 0 auto;
}
.form-signin .checkbox {
font-weight: 400;
}
.form-signin .form-control {
position: relative;
box-sizing: border-box;
height: auto;
padding: 10px;
font-size: 16px;
}
.form-signin .form-control:focus {
z-index: 2;
}
.form-signin input[type="email"] {
margin-bottom: -1px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
margin-bottom: 10px;
border-top-left-radius: 0;
border-top-right-radius: 0;
}
<!doctype html>
<html>
<head>
{% block head %}
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" crossorigin="anonymous">
<link rel="stylesheet" href="{{ url('static', filename='css/base.css') }}" crossorigin="anonymous">
<title>{% block title %}{% endblock %}</title>
{% endblock %}
</head>
<body>
<div id="content">
<!-- Flashed alerts go here -->
{% with messages = get_flashed_messages() %}
{% if messages %}
<ul class=flashes>
{% for message in messages %}
<div class="alert alert-danger alert-dismissible fade show" role="alert">
<strong>{{ message }}</strong> Please try again with your Imperial username and password.
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
{% endfor %}
</ul>
{% endif %}
{% endwith %}
<!-- Body of the web page goes here -->
{% block content %}{% endblock %}
</div>
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" crossorigin="anonymous"></script>
</body>
</html>
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Dashboard{% endblock %}
{% block content %}
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="{{ url('home.index') }}">My Web App</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-item nav-link" href="{{ url('home.index') }}">Home <span class="sr-only">(current)</span></a>
<a class="nav-item nav-link active" href="{{ url('home.dashboard') }}">Dashboard</a>
<a class="nav-item nav-link" href="{{ url('auth.logout') }}">Logout</a>
</div>
</div>
</nav>
<h1>My Dashboard</h1>
<h3>Welcome back, {{ user.name }} {{ user.surname }}!</h3>
<p><strong>Username:</strong> {{ user.username }}</p>
<p><strong>User Type:</strong> {{ user.title }}</p>
<br>
<br>
<h6>Example CRUD form - Create a record in the database:</h6>
<form method="post" action="{{ url('home.entities') }}" style="width: 30%;">
Name:<br>
<input type="text" name="name" class="form-control">
<br>
Age:<br>
<input type="number" name="age" class="form-control">
<br>
<button class="btn btn-lg btn-primary btn-block" type="submit">Create Entry</button>
</form>
<br>
<a href="{{ url('home.entities') }}">View records</a>
{% endblock %}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<title>Flask Template</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1 id="title">Imperial PaaS - Python Flask Template</h1>
<nav>
<ul>
<li><a href="{{ url('home.home') }}">HOME</a></li>
<li><a href="{{ url('auth.login') }}">LOGIN</a></li>
</ul>
</nav>
<img src="{{ url('static', filename='img/python-logo.png') }}" width="100px" alt="python logo"/>
<h2>What is Flask Template</h2>
<p>
Flask Template is a simple web app programmed in Python-3 using flask micro-framework. It is created for begginers to understand the basics of creating a flask web app and deploying it on the Heroku. It can also be used as a template to create your new flask web apps.
</p><br>
<h3>Example CRUD form - Create a record in the database:</h3>
<form method="post" action="{{ url('home.entities') }}">
Name:<br>
<input type="text" name="name">
<br>
Age:<br>
<input type="number" name="age">
<br>
<button type="submit">Create Entry</button>
</form>
<br>
<a href="{{ url('home.entities') }}">View records</a>
</body>
</html>
\ No newline at end of file
{% extends "base.html" %}
{% block title %}Home{% endblock %}
{% block content %}
<nav class="navbar navbar-expand-lg navbar-light bg-light">
<a class="navbar-brand" href="{{ url('home.index') }}">My Web App</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarNavAltMarkup" aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-item nav-link active" href="{{ url('home.index') }}">Home <span class="sr-only">(current)</span></a>
<a class="nav-item nav-link" href="{{ url('home.dashboard') }}">Dashboard</a>
{% if user %}
<a class="nav-item nav-link" href="{{ url('auth.logout') }}">Logout</a>
{% else %}
<a class="nav-item nav-link" href="{{ url('auth.login') }}">Login</a>
{% endif %}
</div>
</div>
</nav>
<h1>Imperial PaaS - Python Flask Template</h1>
<img src="{{ url('static', filename='img/python-logo.png') }}" width="100px" alt="python logo"/>
<p>Python Flask template with Authentication</p><br>
{% if not user %}
<a href="{{ url('auth.login') }}">LOGIN HERE</a>
{% endif %}
{% endblock %}
\ No newline at end of file
<!DOCTYPE html>
<html lang="en">
<head>
<title>Flask Template</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1 id="title">Imperial PaaS - Python Flask Template</h1>
<nav>
<ul>
<li><a href="{{ url('home.home') }}">HOME</a></li>
<li><a href="{{ url('auth.login') }}">LOGIN</a></li>
</ul>
</nav>
<form method="post">
LDAP Username:<br>
<input type="text" name="username">
<br>
Password:<br>
<input type="password" name="password">
<br>
<button type="submit">Login</button>
{% extends "base.html" %}
{% block title %}Login{% endblock %}
{% block head %}
{{ super() }}
<link rel="stylesheet" href="{{ url('static', filename='css/login.css') }}">
{% endblock %}
{% block content %}
<div class="text-center">
<form class="form-signin" method="post">
<img class="mb-4" src="{{ url('static', filename='img/imperial-logo.png') }}" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal">Imperial LDAP Login</h1>
<label for="inputUsername" class="sr-only">LDAP Username</label>
<input type="text" name="username" id="inputUsername" class="form-control" placeholder="Username" required="" autofocus="">
<label for="inputPassword" class="sr-only">Password</label>
<input type="password" name="password" id="inputPassword" class="form-control" placeholder="Password" required="">
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
</form>
</body>
</html>
\ No newline at end of file
</div>
{% endblock %}
\ No newline at end of file
Supports Markdown
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