From 371e2af64be2c7f695a906213d8d6f6522e7e92e Mon Sep 17 00:00:00 2001 From: Damien De Paoli Date: Sat, 26 Jun 2021 09:20:11 +1000 Subject: [PATCH] added python ldap / login pages --- Dockerfile | 2 +- README | 3 ++ ai.py | 2 + files.py | 15 ++++++++ job.py | 5 +++ main.py | 89 ++++++++++++++++++++++++++++++++++++++++++++- person.py | 4 ++ refimg.py | 4 ++ requirements.txt | 2 + settings.py | 2 + templates/jobs.html | 11 ++++-- 11 files changed, 134 insertions(+), 5 deletions(-) diff --git a/Dockerfile b/Dockerfile index bf8db83..ec845f5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -15,7 +15,7 @@ RUN truncate -s0 /tmp/preseed.cfg && \ apt-get install -y tzdata ## cleanup of files from setup RUN rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -RUN apt-get update && apt-get -y install python3-pip python3-psycopg2 libpq-dev gunicorn mediainfo cmake libgl1-mesa-glx libglib2.0-0 +RUN apt-get update && apt-get -y install python3-pip python3-psycopg2 libpq-dev gunicorn mediainfo cmake libgl1-mesa-glx libglib2.0-0 python3-ldap COPY requirements.txt requirements.txt RUN pip3 install -r requirements.txt RUN pip3 install --upgrade pillow --user diff --git a/README b/README index 31855e3..ca64e5a 100644 --- a/README +++ b/README @@ -13,6 +13,9 @@ pip packages: * datetime * pytz * face_recognition + * flask-login + * flask_login + * flask-ldap3-login #### dlib (might need to install this before face_recognitioin, but it might not be needed, cmake clearly was) diff --git a/ai.py b/ai.py index 1db4e17..29b3fe3 100644 --- a/ai.py +++ b/ai.py @@ -8,6 +8,7 @@ from status import st, Status from files import Entry, File, FileRefimgLink from person import Person, PersonRefimgLink from refimg import Refimg +from flask_login import login_required, current_user # pylint: disable=no-member @@ -15,6 +16,7 @@ from refimg import Refimg # /aistats -> placholder for some sort of stats ################################################################################ @app.route("/aistats", methods=["GET", "POST"]) +@login_required def aistats(): tmp=db.session.query(Entry,Person).join(File).join(FileRefimgLink).join(Refimg).join(PersonRefimgLink).join(Person).filter(FileRefimgLink.matched==True).all() entries=[] diff --git a/files.py b/files.py index 544c941..e6bdf90 100644 --- a/files.py +++ b/files.py @@ -16,6 +16,7 @@ import numpy import cv2 import time import re +from flask_login import login_required, current_user ################################################################################ # Local Class imports @@ -225,6 +226,7 @@ def GetEntriesInFolderView( cwd, prefix, noo, offset, how_many ): # /file_list -> show detailed file list of files from import_path(s) ################################################################################ @app.route("/file_list_ip", methods=["GET","POST"]) +@login_required def file_list_ip(): noo, grouping, how_many, offset, size, folders, cwd, root = ViewingOptions( request ) entries=[] @@ -240,6 +242,7 @@ def file_list_ip(): # /files -> show thumbnail view of files from import_path(s) ################################################################################ @app.route("/files_ip", methods=["GET", "POST"]) +@login_required def files_ip(): noo, grouping, how_many, offset, size, folders, cwd, root = ViewingOptions( request ) @@ -261,6 +264,7 @@ def files_ip(): # /files -> show thumbnail view of files from storage_path ################################################################################ @app.route("/files_sp", methods=["GET", "POST"]) +@login_required def files_sp(): noo, grouping, how_many, offset, size, folders, cwd, root = ViewingOptions( request ) entries=[] @@ -282,6 +286,7 @@ def files_sp(): # /files -> show thumbnail view of files from storage_path ################################################################################ @app.route("/files_rbp", methods=["GET", "POST"]) +@login_required def files_rbp(): noo, grouping, how_many, offset, size, folders, cwd, root = ViewingOptions( request ) entries=[] @@ -303,6 +308,7 @@ def files_rbp(): # /search -> show thumbnail view of files from import_path(s) ################################################################################ @app.route("/search", methods=["GET","POST"]) +@login_required def search(): noo, grouping, how_many, offset, size, folders, cwd, root = ViewingOptions( request ) @@ -321,6 +327,7 @@ def search(): # /files/scannow -> allows us to force a check for new files ################################################################################ @app.route("/files/scannow", methods=["GET"]) +@login_required def scannow(): job=NewJob("scannow" ) st.SetAlert("success") @@ -331,6 +338,7 @@ def scannow(): # /files/forcescan -> deletes old data in DB, and does a brand new scan ################################################################################ @app.route("/files/forcescan", methods=["GET"]) +@login_required def forcescan(): job=NewJob("forcescan" ) st.SetAlert("success") @@ -341,6 +349,7 @@ def forcescan(): # /files/scan_sp -> allows us to force a check for new files ################################################################################ @app.route("/files/scan_sp", methods=["GET"]) +@login_required def scan_sp(): job=NewJob("scan_sp" ) st.SetAlert("success") @@ -349,6 +358,7 @@ def scan_sp(): @app.route("/fix_dups", methods=["POST"]) +@login_required def fix_dups(): rows = db.engine.execute( "select e1.id as id1, f1.hash, d1.rel_path as rel_path1, d1.eid as did1, e1.name as fname1, p1.id as path1, p1.type_id as path_type1, e2.id as id2, d2.rel_path as rel_path2, d2.eid as did2, e2.name as fname2, p2.id as path2, p2.type_id as path_type2 from entry e1, file f1, dir d1, entry_dir_link edl1, path_dir_link pdl1, path p1, entry e2, file f2, dir d2, entry_dir_link edl2, path_dir_link pdl2, path p2 where e1.id = f1.eid and e2.id = f2.eid and d1.eid = edl1.dir_eid and edl1.entry_id = e1.id and edl2.dir_eid = d2.eid and edl2.entry_id = e2.id and p1.type_id != (select id from path_type where name = 'Bin') and p1.id = pdl1.path_id and pdl1.dir_eid = d1.eid and p2.type_id != (select id from path_type where name = 'Bin') and p2.id = pdl2.path_id and pdl2.dir_eid = d2.eid and f1.hash = f2.hash and e1.id != e2.id and f1.size_mb = f2.size_mb order by path1, rel_path1, fname1"); @@ -374,6 +384,7 @@ def fix_dups(): return render_template("dups.html", DD=DD, pagesize=pagesize ) @app.route("/rm_dups", methods=["POST"]) +@login_required def rm_dups(): jex=[] @@ -398,6 +409,7 @@ def rm_dups(): return render_template("base.html") @app.route("/restore_files", methods=["POST"]) +@login_required def restore_files(): jex=[] for el in request.form: @@ -409,6 +421,7 @@ def restore_files(): return render_template("base.html") @app.route("/delete_files", methods=["POST"]) +@login_required def delete_files(): jex=[] for el in request.form: @@ -420,6 +433,7 @@ def delete_files(): return render_template("base.html") @app.route("/move_files", methods=["POST"]) +@login_required def move_files(): jex=[] for el in request.form: @@ -435,6 +449,7 @@ def move_files(): # we create/use symlinks in static/ to reference the images to show ################################################################################ @app.route("/static/") +@login_required def custom_static(filename): return send_from_directory("static/", filename) diff --git a/job.py b/job.py index 136869d..2978f1b 100644 --- a/job.py +++ b/job.py @@ -6,9 +6,11 @@ from sqlalchemy import Sequence from sqlalchemy.exc import SQLAlchemyError from status import st, Status from datetime import datetime, timedelta +from flask_login import login_required, current_user import pytz import socket from shared import PA_JOB_MANAGER_HOST, PA_JOB_MANAGER_PORT +from flask_login import login_required, current_user # pylint: disable=no-member @@ -93,6 +95,7 @@ def NewJob(name, num_files="0", wait_for=None, jex=None ): # /jobs -> show current settings ################################################################################ @app.route("/jobs", methods=["GET"]) +@login_required def jobs(): page_title='Job list' jobs = Job.query.order_by(Job.id.desc()).all() @@ -103,6 +106,7 @@ def jobs(): # /job/ -> GET -> shows status/history of jobs ################################################################################ @app.route("/job/", methods=["GET"]) +@login_required def joblog(id): page_title='Show Job Details' joblog = Job.query.get(id) @@ -118,6 +122,7 @@ def joblog(id): # /job/ -> GET -> shows status/history of jobs ################################################################################ @app.route("/wakeup", methods=["GET"]) +@login_required def wakeup(): WakePAJobManager() return render_template("base.html") diff --git a/main.py b/main.py index e4eb3f2..6bcfe6f 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,4 @@ -from flask import Flask, render_template, request, redirect, jsonify +from flask import Flask, render_template, request, redirect, jsonify, url_for, render_template_string from flask_sqlalchemy import SQLAlchemy from sqlalchemy.exc import SQLAlchemyError from flask_marshmallow import Marshmallow @@ -7,6 +7,13 @@ from wtforms import SubmitField, StringField, HiddenField, SelectField, IntegerF from flask_wtf import FlaskForm from status import st, Status from shared import CreateSelect, CreateFoldersSelect, LocationIcon, DB_URL +from flask_login import login_required, current_user + + +# for ldap auth +from flask_ldap3_login import LDAP3LoginManager +from flask_login import LoginManager, login_user, UserMixin, current_user +from flask_ldap3_login.forms import LDAPLoginForm import re import socket @@ -23,9 +30,30 @@ app = Flask(__name__) app.config['SQLALCHEMY_DATABASE_URI'] = DB_URL app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config.from_mapping( SECRET_KEY=b'\xd6\x04\xbdj\xfe\xed$c\x1e@\xad\x0f\x13,@G') + +# ldap config vars: (the last one is required, or python ldap freaks out) +app.config['LDAP_HOST'] = 'mara.ddp.net' +app.config['LDAP_BASE_DN'] = 'dc=depaoli,dc=id,dc=au' +app.config['LDAP_USER_DN'] = 'ou=users' +app.config['LDAP_GROUP_DN'] = 'ou=groups' +app.config['LDAP_USER_RDN_ATTR'] = 'cn' +app.config['LDAP_USER_LOGIN_ATTR'] = 'uid' +app.config['LDAP_BIND_USER_DN'] = None +app.config['LDAP_BIND_USER_PASSWORD'] = None +app.config['LDAP_GROUP_OBJECT_FILTER'] = '(objectclass=posixGroup)' + db = SQLAlchemy(app) ma = Marshmallow(app) Bootstrap(app) +login_manager = LoginManager(app) # Setup a Flask-Login Manager +ldap_manager = LDAP3LoginManager(app) # Setup a LDAP3 Login Manager. +login_manager.login_view = "login" # default login route, failed with url_for, so hard-coded + +# Create a dictionary to store the users in when they authenticate +# This example stores users in memory. +users = {} + + ################################# Now, import non-book classes ################################### from settings import Settings @@ -50,11 +78,70 @@ app.jinja_env.globals['CreateFoldersSelect'] = CreateFoldersSelect app.jinja_env.globals['LocationIcon'] = LocationIcon app.jinja_env.globals['StoragePathNames'] = StoragePathNames + +# Declare an Object Model for the user, and make it comply with the +# flask-login UserMixin mixin. +class User(UserMixin): + def __init__(self, dn, username, data): + self.dn = dn + self.username = username + self.data = data + + def __repr__(self): + return self.dn + + def get_id(self): + return self.dn + +# Declare a User Loader for Flask-Login. +# Simply returns the User if it exists in our 'database', otherwise +# returns None. +@login_manager.user_loader +def load_user(id): + if id in users: + return users[id] + return None + +# Declare The User Saver for Flask-Ldap3-Login +# This method is called whenever a LDAPLoginForm() successfully validates. +# Here you have to save the user, and return it so it can be used in the +# login controller. +@ldap_manager.save_user +def save_user(dn, username, data, memberships): + user = User(dn, username, data) + users[dn] = user + return user + # default page, just the navbar @app.route("/", methods=["GET"]) +@login_required def main_page(): + # Redirect users who are not logged in. + if not current_user or current_user.is_anonymous: + return redirect(url_for('login')) + return render_template("base.html") +@app.route('/login', methods=['GET', 'POST']) +def login(): + # Instantiate a LDAPLoginForm which has a validator to check if the user + # exists in LDAP. + form = LDAPLoginForm() + form.submit.label.text="Login" + + if form.validate_on_submit(): + # Successfully logged in, We can now access the saved user object + # via form.user. + login_user(form.user, remember=True) # Tell flask-login to log them in. + next = request.args.get("next") + if next: + return redirect(next) # Send them back where they came from + else: + return redirect('/') + + return render_template("login.html", form=form) + + if __name__ == "__main__": if hostname == PROD_HOST: app.run(ssl_context=('/etc/letsencrypt/live/book.depaoli.id.au/cert.pem', '/etc/letsencrypt/live/book.depaoli.id.au/privkey.pem'), host="0.0.0.0", debug=False) diff --git a/person.py b/person.py index e549f8d..7d00668 100644 --- a/person.py +++ b/person.py @@ -6,6 +6,7 @@ from sqlalchemy import Sequence from sqlalchemy.exc import SQLAlchemyError from status import st, Status from refimg import Refimg +from flask_login import login_required, current_user # pylint: disable=no-member @@ -56,6 +57,7 @@ class PersonForm(FlaskForm): # /persons -> GET only -> prints out list of all persons ################################################################################ @app.route("/persons", methods=["GET"]) +@login_required def persons(): persons = Person.query.all() return render_template("persons.html", persons=persons) @@ -65,6 +67,7 @@ def persons(): # /person -> GET/POST -> creates a new person type and when created, takes you back to /persons ################################################################################ @app.route("/person", methods=["GET", "POST"]) +@login_required def new_person(): form = PersonForm(request.form) page_title='Create new Person' @@ -90,6 +93,7 @@ def new_person(): # person ################################################################################ @app.route("/person/", methods=["GET", "POST"]) +@login_required def person(id): form = PersonForm(request.form) page_title='Edit Person' diff --git a/refimg.py b/refimg.py index c258e5f..9379bcd 100644 --- a/refimg.py +++ b/refimg.py @@ -6,6 +6,7 @@ from sqlalchemy import Sequence from sqlalchemy.exc import SQLAlchemyError from status import st, Status import os +from flask_login import login_required, current_user # pylint: disable=no-member @@ -51,6 +52,7 @@ class RefimgForm(FlaskForm): # /refimgs -> GET only -> prints out list of all refimgs ################################################################################ @app.route("/refimgs", methods=["GET"]) +@login_required def refimgs(): refimgs = Refimg.query.all() return render_template("refimgs.html", refimgs=refimgs) @@ -59,6 +61,7 @@ def refimgs(): # /refimg -> GET/POST -> creates a new refimg type and when created, takes you back to /refimgs ################################################################################ @app.route("/refimg", methods=["GET", "POST"]) +@login_required def new_refimg(): form = RefimgForm(request.form) page_title='Create new Reference Image' @@ -88,6 +91,7 @@ def new_refimg(): # refimg ################################################################################ @app.route("/refimg/", methods=["GET", "POST"]) +@login_required def refimg(id): form = RefimgForm(request.form) page_title='Edit Reference Image' diff --git a/requirements.txt b/requirements.txt index 65ef784..0b26d25 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,6 @@ flask +flask_login +flask-ldap3-login sqlalchemy flask-sqlalchemy SQLAlchemy-serializer diff --git a/settings.py b/settings.py index a82e812..08d59f1 100644 --- a/settings.py +++ b/settings.py @@ -5,6 +5,7 @@ from main import db, app, ma from sqlalchemy import Sequence from sqlalchemy.exc import SQLAlchemyError from status import st, Status +from flask_login import login_required, current_user # pylint: disable=no-member @@ -45,6 +46,7 @@ class SettingsForm(FlaskForm): # /settings -> show current settings ################################################################################ @app.route("/settings", methods=["GET", "POST"]) +@login_required def settings(): form = SettingsForm(request.form) page_title='Settings' diff --git a/templates/jobs.html b/templates/jobs.html index 7983b16..33a3edb 100644 --- a/templates/jobs.html +++ b/templates/jobs.html @@ -8,9 +8,9 @@ {% for job in jobs %} {% if job.state == "Failed" %} - row='Job #{{job.id}} - {{job.name}}' + row='Job #{{job.id}} - {{job.name}}' {% elif job.state == "Withdrawn" %} - row='Job #{{job.id}} - {{job.name}}' + row='Job #{{job.id}} - {{job.name}}' {% else %} row='Job #{{job.id}} - {{job.name}}' {% endif %} @@ -21,7 +21,12 @@ {% endif %} {% endfor %} {% endif %} - row+= '{{job.start_time}}' + + {% if job.state == "Withdrawn" %} + row+= '{{job.start_time}}' + {% else %} + row+= '{{job.start_time}}' + {% endif %} {% if job.pa_job_state != "Completed" %} {% if job.num_files and job.num_files > 0 %} {% set prog=(job.current_file_num/job.num_files*100)|round|int %}