Skip to content

Commit 6bb337f

Browse files
authored
Merge pull request #15 from CampusPulse/auth
Auth for admin stuff
2 parents 71f2f12 + 4d60160 commit 6bb337f

File tree

8 files changed

+542
-24
lines changed

8 files changed

+542
-24
lines changed

README.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,20 @@ This is a fork of [TunnelVision](https://github.com/wilsonmcdade/tunnelvision)
1515
* `[podman or docker] compose up` (this starts up the database and minio for S3)
1616
* `uv run python3 app.py` (this runs the app in development mode)
1717

18+
## Configuring Auth
19+
20+
1. Create an auth0 tenant
21+
2. create an application (type: Regular Web Application)
22+
3. ensure the domain, client ID, and client secret are in the environment variables (see `sample.env` for the names to store these in)
23+
4. generate a random secret value and store it in the `CPACCESS_SECRET_KEY` variable
24+
5. Set up your callback and logout urls in the application settings of auth0 (default endpoints are `<your domain>/callback` and `<your domain>/logout`)
25+
6. on the "API's" tab enable the auth0 management API
26+
7. drop down the management API and ensure at least `read:users` and `read:roles` are selected
27+
8. Run the app
28+
9. visit the `/login` page. When prompted, sign up with whatever method you choose
29+
30+
31+
1832
## Database Schema
1933
This project uses SQLAlchemy to access a PostgresQL database. The DB schema is defined in `db.py`
2034

app.py

Lines changed: 115 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import subprocess
44
from dateutil import parser
55
from enum import Enum
6-
from flask import Flask, render_template, request, redirect, abort, url_for, make_response
6+
from flask import Flask, render_template, request, redirect, abort, url_for, make_response, session
77
import logging
88
from werkzeug.utils import secure_filename
99
from werkzeug.exceptions import HTTPException
@@ -51,7 +51,9 @@
5151
import json_log_formatter
5252
from pathlib import Path
5353
from dotenv import load_dotenv
54-
from helpers import floor_to_integer, RoomNumber, integer_to_floor, MapLocation, ServiceNowStatus, ServiceNowUpdateType
54+
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
55+
from urllib.parse import quote_plus, urlencode
56+
from authlib.integrations.flask_client import OAuth
5557

5658

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

90+
auth_configured = not None in [
91+
os.environ.get("AUTH0_DOMAIN"),
92+
os.environ.get("CPACCESS_SECRET_KEY"),
93+
os.environ.get("AUTH0_CLIENT_ID"),
94+
os.environ.get("AUTH0_CLIENT_SECRET")
95+
]
96+
97+
if auth_configured:
98+
# Auth Setup
99+
app.secret_key = os.environ.get("CPACCESS_SECRET_KEY")
100+
101+
oauth = OAuth(app)
102+
103+
oauth.register(
104+
"auth0",
105+
client_id=os.environ.get("AUTH0_CLIENT_ID"),
106+
client_secret=os.environ.get("AUTH0_CLIENT_SECRET"),
107+
client_kwargs={
108+
"scope": "openid profile email",
109+
},
110+
server_metadata_url=f'https://{os.environ.get("AUTH0_DOMAIN")}/.well-known/openid-configuration',
111+
)
112+
113+
logging.info("Auth Initialized")
114+
115+
else:
116+
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")
117+
118+
119+
88120
logging.info(f"Connecting to S3 Bucket {app.config['BUCKET_NAME']}")
89121

90122
s3_bucket = S3Bucket(
@@ -817,6 +849,7 @@ def catalog():
817849
if page is None:
818850
return render_template(
819851
"catalog.html",
852+
authsession=get_logged_in_user(),
820853
q=query,
821854
page=1,
822855
accessPoints=getAccessPointsPaginated(0),
@@ -825,13 +858,15 @@ def catalog():
825858
else:
826859
page = int(page)
827860
return render_template(
828-
"paginated.html",
861+
"paginated.html",
862+
authsession=get_logged_in_user(),
829863
page=(page+1),
830864
murals=getAccessPointsPaginated(page)
831865
)
832866
else:
833867
return render_template(
834868
"filtered.html",
869+
authsession=get_logged_in_user(),
835870
pageTitle=f"Query - {query}",
836871
subHeading="Search Query",
837872
q=query,
@@ -863,11 +898,53 @@ def tags():
863898
def access_point(id):
864899
if checkAccessPointExists(id):
865900
return render_template(
866-
"access_point.html", accessPointDetails=getAccessPoint(id)
901+
"access_point.html",
902+
authsession=get_logged_in_user(),
903+
is_admin=check_for_admin_role(get_logged_in_user_id()),
904+
accessPointDetails=getAccessPoint(id)
867905
)
868906
else:
869907
return render_template("404.html"), 404
870908

909+
910+
########################
911+
#
912+
# region Auth
913+
#
914+
########################
915+
916+
if auth_configured:
917+
@app.route("/callback", methods=["GET", "POST"])
918+
def callback():
919+
token = oauth.auth0.authorize_access_token()
920+
save_user_details(token)
921+
return redirect("/")
922+
923+
924+
@app.route("/login")
925+
def login():
926+
return oauth.auth0.authorize_redirect(
927+
redirect_uri=url_for("callback", _external=True)
928+
)
929+
930+
931+
@app.route("/logout")
932+
def logout():
933+
session.clear()
934+
return redirect(
935+
"https://"
936+
+ os.environ.get("AUTH0_DOMAIN")
937+
+ "/v2/logout?"
938+
+ urlencode(
939+
{
940+
"returnTo": url_for("home", _external=True),
941+
"client_id": os.environ.get("AUTH0_CLIENT_ID"),
942+
},
943+
quote_via=quote_plus,
944+
)
945+
)
946+
947+
871948
"""
872949
Generic error handler
873950
"""
@@ -896,6 +973,24 @@ def wrapped(**kwargs):
896973
return wrapped
897974

898975

976+
def requires_admin(f):
977+
"""Determines if the user has the correct admin permissions for an action
978+
"""
979+
@wraps(f)
980+
def decorated(*args, **kwargs):
981+
user = get_logged_in_user_id()
982+
is_admin = check_for_admin_role(user)
983+
984+
if user is None:
985+
raise Exception("There must be a user signed in to perform this action",
986+
400)
987+
elif not is_admin:
988+
raise Exception("Authorizing user does not have the correct role to perform this action",
989+
401)
990+
else:
991+
return f(*args, **kwargs)
992+
return decorated
993+
899994

900995
########################
901996
#
@@ -994,7 +1089,7 @@ def email_webhook():
9941089

9951090

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

12991394

13001395
@app.route("/edit/<id>")
1301-
@debug_only
1396+
@requires_admin
13021397
def edit(id):
13031398

13041399
if checkAccessPointExists(id):
@@ -1318,7 +1413,7 @@ def edit(id):
13181413

13191414

13201415
@app.route("/admin")
1321-
@debug_only
1416+
@requires_admin
13221417
def admin():
13231418
return render_template(
13241419
"admin.html",
@@ -1396,7 +1491,7 @@ def submit_suggestion():
13961491

13971492

13981493
@app.route("/deleteTag/<name>", methods=["POST"])
1399-
@debug_only
1494+
@requires_admin
14001495
def deleteTag(name):
14011496
deleteTagGivenName(name)
14021497
return redirect("/admin")
@@ -1408,7 +1503,7 @@ def deleteTag(name):
14081503

14091504

14101505
@app.route("/delete/<id>", methods=["POST"])
1411-
@debug_only
1506+
@requires_admin
14121507
def delete(id):
14131508
if checkAccessPointExists(id):
14141509
deleteAccessPointEntry(id)
@@ -1424,7 +1519,7 @@ def delete(id):
14241519

14251520

14261521
@app.route("/editaccesspoint/<id>", methods=["POST"])
1427-
@debug_only
1522+
@requires_admin
14281523
def editAccessPoint(id):
14291524
m = db.session.execute(
14301525
db.select(AccessPoint).where(AccessPoint.id == id)
@@ -1479,7 +1574,7 @@ def editAccessPoint(id):
14791574

14801575

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

14951590

14961591
@app.route("/edittitle/<id>", methods=["POST"])
1497-
@debug_only
1592+
@requires_admin
14981593
def editTitle(id):
14991594
m = db.session.execute(
15001595
db.select(AccessPoint).where(AccessPoint.id == id)
@@ -1511,7 +1606,7 @@ def editTitle(id):
15111606

15121607

15131608
@app.route("/editimage/<id>", methods=["POST"])
1514-
@debug_only
1609+
@requires_admin
15151610
def editImage(id):
15161611
image = db.session.execute(db.select(Image).where(Image.id == id)).scalar_one()
15171612

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

15341629

15351630
@app.route("/makethumbnail", methods=["POST"])
1536-
@debug_only
1631+
@requires_admin
15371632
def makeThumbnail():
15381633
access_point_id = request.args.get("accesspointid", None)
15391634
image_id = request.args.get("imageid", None)
@@ -1563,7 +1658,7 @@ def makeThumbnail():
15631658

15641659

15651660
@app.route("/detachimage/<image_id>/from/<item_id>", methods=["POST"])
1566-
@debug_only
1661+
@requires_admin
15671662
def detachImageEndpoint(image_id, item_id):
15681663

15691664
detachImageByID(image_id, item_id)
@@ -1579,7 +1674,7 @@ def detachImageEndpoint(image_id, item_id):
15791674

15801675

15811676
@app.route("/export", methods=["POST"])
1582-
@debug_only
1677+
@requires_admin
15831678
def export_data():
15841679
public = bool(int(request.args.get("p")))
15851680
now = datetime.now()
@@ -1600,7 +1695,7 @@ def export_data():
16001695

16011696

16021697
@app.route("/import", methods=["POST"])
1603-
@debug_only
1698+
@requires_admin
16041699
def import_data():
16051700
return ("", 501)
16061701

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

16121707

16131708
@app.route("/addTag", methods=["POST"])
1614-
@debug_only
1709+
@requires_admin
16151710
def add_tag():
16161711
tag = Tag(name=request.form["name"], description="")
16171712

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

16281723

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

16471742

16481743
@app.route("/upload/elevator", methods=["POST"])
1649-
@debug_only
1744+
@requires_admin
16501745
def upload():
16511746

16521747
# Step 1: Find the building by its number

0 commit comments

Comments
 (0)