Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,20 @@ This is a fork of [TunnelVision](https://github.com/wilsonmcdade/tunnelvision)
* `[podman or docker] compose up` (this starts up the database and minio for S3)
* `uv run python3 app.py` (this runs the app in development mode)

## Configuring Auth

1. Create an auth0 tenant
2. create an application (type: Regular Web Application)
3. ensure the domain, client ID, and client secret are in the environment variables (see `sample.env` for the names to store these in)
4. generate a random secret value and store it in the `CPACCESS_SECRET_KEY` variable
5. Set up your callback and logout urls in the application settings of auth0 (default endpoints are `<your domain>/callback` and `<your domain>/logout`)
6. on the "API's" tab enable the auth0 management API
7. drop down the management API and ensure at least `read:users` and `read:roles` are selected
8. Run the app
9. visit the `/login` page. When prompted, sign up with whatever method you choose



## Database Schema
This project uses SQLAlchemy to access a PostgresQL database. The DB schema is defined in `db.py`

Expand Down
135 changes: 115 additions & 20 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import subprocess
from dateutil import parser
from enum import Enum
from flask import Flask, render_template, request, redirect, abort, url_for, make_response
from flask import Flask, render_template, request, redirect, abort, url_for, make_response, session
import logging
from werkzeug.utils import secure_filename
from werkzeug.exceptions import HTTPException
Expand Down Expand Up @@ -51,7 +51,9 @@
import json_log_formatter
from pathlib import Path
from dotenv import load_dotenv
from helpers import floor_to_integer, RoomNumber, integer_to_floor, MapLocation, ServiceNowStatus, ServiceNowUpdateType
from helpers import floor_to_integer, RoomNumber, integer_to_floor, MapLocation, ServiceNowStatus, ServiceNowUpdateType, save_user_details, check_for_admin_role, get_logged_in_user_id, get_logged_in_user
from urllib.parse import quote_plus, urlencode
from authlib.integrations.flask_client import OAuth


app = Flask(__name__)
Expand Down Expand Up @@ -85,6 +87,36 @@
git_cmd = ["git", "rev-parse", "--short", "HEAD"]
app.config["GIT_REVISION"] = subprocess.check_output(git_cmd).decode("utf-8").rstrip()

auth_configured = not None in [
os.environ.get("AUTH0_DOMAIN"),
os.environ.get("CPACCESS_SECRET_KEY"),
os.environ.get("AUTH0_CLIENT_ID"),
os.environ.get("AUTH0_CLIENT_SECRET")
]

if auth_configured:
# Auth Setup
app.secret_key = os.environ.get("CPACCESS_SECRET_KEY")

oauth = OAuth(app)

oauth.register(
"auth0",
client_id=os.environ.get("AUTH0_CLIENT_ID"),
client_secret=os.environ.get("AUTH0_CLIENT_SECRET"),
client_kwargs={
"scope": "openid profile email",
},
server_metadata_url=f'https://{os.environ.get("AUTH0_DOMAIN")}/.well-known/openid-configuration',
)

logging.info("Auth Initialized")

else:
logging.info("Auth configuration not available due to missing variables. Ensure all of AUTH0_DOMAIN, CPACCESS_SECRET_KEY, AUTH0_CLIENT_ID, AUTH0_CLIENT_SECRET are present")



logging.info(f"Connecting to S3 Bucket {app.config['BUCKET_NAME']}")

s3_bucket = S3Bucket(
Expand Down Expand Up @@ -817,6 +849,7 @@ def catalog():
if page is None:
return render_template(
"catalog.html",
authsession=get_logged_in_user(),
q=query,
page=1,
accessPoints=getAccessPointsPaginated(0),
Expand All @@ -825,13 +858,15 @@ def catalog():
else:
page = int(page)
return render_template(
"paginated.html",
"paginated.html",
authsession=get_logged_in_user(),
page=(page+1),
murals=getAccessPointsPaginated(page)
)
else:
return render_template(
"filtered.html",
authsession=get_logged_in_user(),
pageTitle=f"Query - {query}",
subHeading="Search Query",
q=query,
Expand Down Expand Up @@ -863,11 +898,53 @@ def tags():
def access_point(id):
if checkAccessPointExists(id):
return render_template(
"access_point.html", accessPointDetails=getAccessPoint(id)
"access_point.html",
authsession=get_logged_in_user(),
is_admin=check_for_admin_role(get_logged_in_user_id()),
accessPointDetails=getAccessPoint(id)
)
else:
return render_template("404.html"), 404


########################
#
# region Auth
#
########################

if auth_configured:
@app.route("/callback", methods=["GET", "POST"])
def callback():
token = oauth.auth0.authorize_access_token()
save_user_details(token)
return redirect("/")


@app.route("/login")
def login():
return oauth.auth0.authorize_redirect(
redirect_uri=url_for("callback", _external=True)
)


@app.route("/logout")
def logout():
session.clear()
return redirect(
"https://"
+ os.environ.get("AUTH0_DOMAIN")
+ "/v2/logout?"
+ urlencode(
{
"returnTo": url_for("home", _external=True),
"client_id": os.environ.get("AUTH0_CLIENT_ID"),
},
quote_via=quote_plus,
)
)


"""
Generic error handler
"""
Expand Down Expand Up @@ -896,6 +973,24 @@ def wrapped(**kwargs):
return wrapped


def requires_admin(f):
"""Determines if the user has the correct admin permissions for an action
"""
@wraps(f)
def decorated(*args, **kwargs):
user = get_logged_in_user_id()
is_admin = check_for_admin_role(user)

if user is None:
raise Exception("There must be a user signed in to perform this action",
400)
elif not is_admin:
raise Exception("Authorizing user does not have the correct role to perform this action",
401)
else:
return f(*args, **kwargs)
return decorated


########################
#
Expand Down Expand Up @@ -994,7 +1089,7 @@ def email_webhook():


@app.route("/add_ticket/<item_id>", methods=["POST"])
@debug_only
@requires_admin
def add_ticket(item_id):
if not checkAccessPointExists(item_id):
return "Not found", 404
Expand Down Expand Up @@ -1298,7 +1393,7 @@ def uploadImageResize(file, access_point_id, count, is_thumbnail=False):


@app.route("/edit/<id>")
@debug_only
@requires_admin
def edit(id):

if checkAccessPointExists(id):
Expand All @@ -1318,7 +1413,7 @@ def edit(id):


@app.route("/admin")
@debug_only
@requires_admin
def admin():
return render_template(
"admin.html",
Expand Down Expand Up @@ -1396,7 +1491,7 @@ def submit_suggestion():


@app.route("/deleteTag/<name>", methods=["POST"])
@debug_only
@requires_admin
def deleteTag(name):
deleteTagGivenName(name)
return redirect("/admin")
Expand All @@ -1408,7 +1503,7 @@ def deleteTag(name):


@app.route("/delete/<id>", methods=["POST"])
@debug_only
@requires_admin
def delete(id):
if checkAccessPointExists(id):
deleteAccessPointEntry(id)
Expand All @@ -1424,7 +1519,7 @@ def delete(id):


@app.route("/editaccesspoint/<id>", methods=["POST"])
@debug_only
@requires_admin
def editAccessPoint(id):
m = db.session.execute(
db.select(AccessPoint).where(AccessPoint.id == id)
Expand Down Expand Up @@ -1479,7 +1574,7 @@ def editAccessPoint(id):


@app.route("/editTag/<name>", methods=["POST"])
@debug_only
@requires_admin
def edit_tag(name):
t = db.session.execute(db.select(Tag).where(Tag.name == name)).scalar_one()
t.description = request.form["description"]
Expand All @@ -1494,7 +1589,7 @@ def edit_tag(name):


@app.route("/edittitle/<id>", methods=["POST"])
@debug_only
@requires_admin
def editTitle(id):
m = db.session.execute(
db.select(AccessPoint).where(AccessPoint.id == id)
Expand All @@ -1511,7 +1606,7 @@ def editTitle(id):


@app.route("/editimage/<id>", methods=["POST"])
@debug_only
@requires_admin
def editImage(id):
image = db.session.execute(db.select(Image).where(Image.id == id)).scalar_one()

Expand All @@ -1533,7 +1628,7 @@ def editImage(id):


@app.route("/makethumbnail", methods=["POST"])
@debug_only
@requires_admin
def makeThumbnail():
access_point_id = request.args.get("accesspointid", None)
image_id = request.args.get("imageid", None)
Expand Down Expand Up @@ -1563,7 +1658,7 @@ def makeThumbnail():


@app.route("/detachimage/<image_id>/from/<item_id>", methods=["POST"])
@debug_only
@requires_admin
def detachImageEndpoint(image_id, item_id):

detachImageByID(image_id, item_id)
Expand All @@ -1579,7 +1674,7 @@ def detachImageEndpoint(image_id, item_id):


@app.route("/export", methods=["POST"])
@debug_only
@requires_admin
def export_data():
public = bool(int(request.args.get("p")))
now = datetime.now()
Expand All @@ -1600,7 +1695,7 @@ def export_data():


@app.route("/import", methods=["POST"])
@debug_only
@requires_admin
def import_data():
return ("", 501)

Expand All @@ -1611,7 +1706,7 @@ def import_data():


@app.route("/addTag", methods=["POST"])
@debug_only
@requires_admin
def add_tag():
tag = Tag(name=request.form["name"], description="")

Expand All @@ -1627,7 +1722,7 @@ def add_tag():


@app.route("/uploadimage/<id>", methods=["POST"])
@debug_only
@requires_admin
def uploadNewImage(id):
count = db.session.execute(
db.select(func.count()).where(ImageAccessPointRelation.access_point_id == id)
Expand All @@ -1646,7 +1741,7 @@ def uploadNewImage(id):


@app.route("/upload/elevator", methods=["POST"])
@debug_only
@requires_admin
def upload():

# Step 1: Find the building by its number
Expand Down
Loading