added python ldap / login pages

This commit is contained in:
2021-06-26 09:20:11 +10:00
parent d1ed80bd35
commit 371e2af64b
11 changed files with 134 additions and 5 deletions

View File

@@ -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

3
README
View File

@@ -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)

2
ai.py
View File

@@ -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=[]

View File

@@ -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/<filename>")
@login_required
def custom_static(filename):
return send_from_directory("static/", filename)

5
job.py
View File

@@ -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/<id> -> GET -> shows status/history of jobs
################################################################################
@app.route("/job/<id>", 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/<id> -> GET -> shows status/history of jobs
################################################################################
@app.route("/wakeup", methods=["GET"])
@login_required
def wakeup():
WakePAJobManager()
return render_template("base.html")

89
main.py
View File

@@ -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)

View File

@@ -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/<id>", methods=["GET", "POST"])
@login_required
def person(id):
form = PersonForm(request.form)
page_title='Edit Person'

View File

@@ -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/<id>", methods=["GET", "POST"])
@login_required
def refimg(id):
form = RefimgForm(request.form)
page_title='Edit Reference Image'

View File

@@ -1,4 +1,6 @@
flask
flask_login
flask-ldap3-login
sqlalchemy
flask-sqlalchemy
SQLAlchemy-serializer

View File

@@ -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'

View File

@@ -8,9 +8,9 @@
{% for job in jobs %}
{% if job.state == "Failed" %}
row='<tr><td class="bg-danger"><a class="text-white" href="{{url_for('joblog', id=job.id)}}">Job #{{job.id}} - {{job.name}}</a>'
row='<tr><td class="table-danger"><a href="{{url_for('joblog', id=job.id)}}">Job #{{job.id}} - {{job.name}}</a>'
{% elif job.state == "Withdrawn" %}
row='<tr><td class="bg-secondary"><a class="text-white" href="{{url_for('joblog', id=job.id)}}">Job #{{job.id}} - {{job.name}}</a>'
row='<tr><td class="table-secondary"><i><a href="{{url_for('joblog', id=job.id)}}">Job #{{job.id}} - {{job.name}}</a>'
{% else %}
row='<tr><td><a href="{{url_for('joblog', id=job.id)}}">Job #{{job.id}} - {{job.name}}</a>'
{% endif %}
@@ -21,7 +21,12 @@
{% endif %}
{% endfor %}
{% endif %}
{% if job.state == "Withdrawn" %}
row+= '</td><td>{{job.start_time}}</i></td><td>'
{% else %}
row+= '</td><td>{{job.start_time}}</td><td>'
{% 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 %}