Commit a4a01261 authored by Alex's avatar Alex
Browse files

Updated template with more features

parent 0ac30217
**/__pycache__/**
web: gunicorn app:app --log-file=- web: gunicorn app:app --log-file=-
\ No newline at end of file
## Imperial PaaS Template: Python Flask # Python3 Flask Template
Get started with this Python Flask template. A started template for Flask web apps using **Python 3.8**. This started template includes:
- Dynamic frontend templates (not React)
- Database support
- Imperial LDAP user authentication
- Multiple environments (development and production)
This template is useful if you would like to create a backend REST API, optionally coupled with a simple dynamic frontend.
## Getting started
Once you have created your new app, take a few minutes to look through the files to familiarise yourself with the project structure.
- `app.py` : entry point to the Flask app
- `templates/` : contains the frontend dynamic HTML files
- `static/` : contains the static frontend assets (images, stylesheets and scripts)
- `blueprints/` : contains all the application routes
- `models/` : contains all the database models
- `database/` : contains the database creation
- `config/` : contains the app settings for the different environments
**The first change you should make** is to set the `APP_NAME` variable in `config/config.py` to whatever you app's name is.
To start the application locally, you can just run `python3 app.py` and this will launch the app on port 5000 (by default).
You will notice a message in the console saying:
`WARNING: Could not connect to the given database URL!`
To fix this, you should set the environment variable DATABASE_URL accordingly. If you have PostgreSQL running locally, you can use that. Alternatively, you could use SQLite which is much simpler and does not require installation.
If you do not want to use a database yet, you can ignore this warning and delete any routes that interact with the database.
If you navigate to `http://localhost:5000`, you will see the response created by the route defined in `blueprints/home.py`.
You will also notice the lines `Environment: production` and `Debug mode: off` when the Flask application starts in the console. To enable debug mode, you must set the environment variable `ENV` to `dev`, ie: `export ENV=dev` (see `config/config.py` for more details on different environments).
## Tutorial 1: Adding a new route
## Tutorial 2: Adding database interaction
## Tutorial 3: Configuring a test environment
\ No newline at end of file
from flask import Flask, render_template from flask import Flask, send_from_directory, url_for
from database.db import db
from config.config import APP_NAME, ENV, get_app_config, get_static_url
app = Flask(__name__) # Create and configure our Flask app
app = Flask(__name__, static_url_path=get_static_url())
app.url_map.strict_slashes = False
app.config.from_object(get_app_config())
db.init_app(app)
@app.route('/') # uri = os.getenv("DATABASE_URL") # or other relevant config var
def index(): # print(uri)
return render_template('index.html')
# # if uri and uri.startswith("postgres://"):
# # uri = uri.replace("postgres://", "postgresql://", 1)
# # app.config["SQLALCHEMY_DATABASE_URI"] = uri
# # db = SQLAlchemy(app)
# # db.create_all()
# # if db.session.query(User).filter_by(username="testuser").first() is not None:
# # db.session.add(User(username='testuser', email='admin@example.com'))
# # db.session.commit()
if __name__ == '__main__': app.run(debug=True) # # if db.session.query(Entity).filter_by(username="my entity").first() is not None:
\ No newline at end of file # # db.session.add(Entity(username='my entity', email='entity@example.com'))
# # db.session.commit()
# # else:
# # print("No database created/linked with this application")
# Serve all static assets for the frontend
@app.route('/static/<path:path>')
def serve_static_files(path):
return send_from_directory('static', path)
# Register all routes from the blueprints module
from blueprints.home import home_blueprint
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
app.jinja_env.globals.update(CUSTOM_TEMPLATE_FUNCTIONS)
if __name__ == '__main__':
app.run()
\ No newline at end of file
# 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
auth_blueprint = Blueprint('auth', __name__, url_prefix='/auth')
@auth_blueprint.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'GET':
return render_template('login.html')
# Handle post request
username = request.form.get('username')
password = request.form.get('password')
print(f'Got username={username} and password={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
from flask import Blueprint, render_template, request, redirect, url_for
from database.db import db
from models.user import Entity
home_blueprint = Blueprint('home', __name__, url_prefix='/')
@home_blueprint.route('')
def home():
return render_template('index.html')
@home_blueprint.route('/hello/<name>')
def hello(name: str):
return "Hello, " + name
# Example CRUD route
@home_blueprint.route('/entities', methods=['GET', 'POST'])
def entities():
if request.method == "GET":
try:
rows = ""
for e in db.session.query(Entity).all():
rows += str(e) + "<br>"
return rows
except:
return "App database error (have you setup a database for this app?)"
else:
username = request.form.get('username')
email = request.form.get('email')
db.session.add(Entity(username=username, email=email))
db.session.commit()
return redirect('/entities')
# Define any custom Jinja2 functions here
from flask import url_for
from .config import URL_PREFIX
# Get around the routing prefix issue inside the templates
def url_for2(endpoint: str, **kwargs):
return URL_PREFIX + str(url_for(endpoint, **kwargs))
CUSTOM_TEMPLATE_FUNCTIONS = {
"url": url_for2,
}
import os
# This should match exactly the name of the app you specified
APP_NAME = "dbtestapp2"
ENV = os.environ.get('ENV', 'prod').lower()
URL_PREFIX = f"/{APP_NAME}" if ENV == 'prod' else ""
# Get the static URL of the app (to get around the production path issue)
def get_static_url():
if ENV == 'prod':
return f'/{APP_NAME}/static'
else:
return '/static'
# Get the app configuration based on the ENV environment variable (default is prod)
def get_app_config():
if ENV == 'prod':
return ProductionConfig()
else:
return DevelopmentConfig()
# If you have created a database for this app, the connection string will be automatically
# accessible through the DATABASE_URL environment variable.
def get_db_url():
url = os.environ.get('DATABASE_URL')
if url is None:
print("WARNING: Could not connect to the given database URL!")
# For PostgreSQL databases, the conn string needs to start with "postgresql"
if url and url.startswith("postgres://"):
url = url.replace("postgres://", "postgresql://", 1)
return url
# Flask App settings for production environment
class ProductionConfig:
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:
DEBUG = True
SQLALCHEMY_DATABASE_URI = get_db_url()
SQLALCHEMY_TRACK_MODIFICATIONS = False
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
\ No newline at end of file
from database.db import db
# class User(db.Model):
# id = db.Column(db.Integer, primary_key=True)
# username = db.Column(db.String(80), unique=True, nullable=False)
# email = db.Column(db.String(120), unique=True, nullable=False)
# def __repr__(self):
# return f'User({self.id}, {self.username}, {self.email})'
class Entity(db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
def __repr__(self):
return f'Entity({self.id}, {self.username}, {self.email})'
\ No newline at end of file
...@@ -4,4 +4,8 @@ gunicorn ...@@ -4,4 +4,8 @@ gunicorn
itsdangerous itsdangerous
Jinja2 Jinja2
peewee peewee
Werkzeug Werkzeug
\ No newline at end of file Flask-SQLAlchemy
SQLAlchemy
psycopg2-binary
python_ldap
\ No newline at end of file
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>Heroku Flask Template</title> <title>Flask Template</title>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
</head> </head>
<body> <body>
<h1 id="tittle">Heroku Flask Template</h1> <h1 id="title">Imperial PaaS - Python Flask Template</h1>
<nav> <nav>
<ul> <ul>
<li><a href="index.html">HOME</a></li> <li><a href="{{ url('home.home') }}">HOME</a></li>
<li><a href="https://github.com/vetronus/heroku-flask-template">SOURCE CODE(GitHub)</a></li> <li><a href="{{ url('auth.login') }}">LOGIN</a></li>
<li><a href="http://parthsarthee.com/">PARTH SARTHEE(Author)</a></li>
<li><a href="http://aroxbit.com">AROXBIT(Awesome Indie Startup)</a></li>
</ul> </ul>
</nav> </nav>
<h2>What is Heroku Flask Template</h2>
<img src="{{ url('static', filename='img/python-logo.png') }}" width="100px" alt="python logo"/>
<h2>What is Flask Template</h2>
<p> <p>
Heroku 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 which can then easily be deployable on Heroku. 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> </p><br>
<h3>You can download this plugin, or modify its source code from <a href="https://github.com/vetronus/heroku-flask-template">GitHub</a></h3>
Example CRUD form:
<form method="post" action="{{ url('home.entities') }}">
Username:<br>
<input type="text" name="username">
<br>
Email:<br>
<input type="email" name="email">
<br>
<button type="submit">Create Entry</button>
</form>
<br>
<a href="{{ url('home.entities') }}">View entities</a>
</body> </body>
</html> </html>
\ 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