Skip to content

Commit 0c2d9cb

Browse files
Ability to generate public QR codes in the UI and admin panel to manage access tokens
1 parent b330808 commit 0c2d9cb

File tree

9 files changed

+688
-84
lines changed

9 files changed

+688
-84
lines changed

pydatalab/src/pydatalab/permissions.py

Lines changed: 27 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,32 @@
1010
from pydatalab.logger import LOGGER
1111
from pydatalab.login import UserRole
1212
from pydatalab.models.people import AccountStatus
13-
from pydatalab.mongo import get_database
13+
from pydatalab.mongo import flask_mongo, get_database
1414

1515
PUBLIC_USER_ID = ObjectId(24 * "0")
1616

1717

1818
def active_users_or_get_only(func):
1919
"""Decorator to ensure that only active user accounts can access the route,
2020
unless it is a GET-route, in which case deactivated accounts can also access it.
21-
21+
Now also allows access with valid access tokens.
2222
"""
2323

2424
@wraps(func)
2525
def wrapped_route(*args, **kwargs):
26+
access_token = request.args.get("at")
27+
refcode = kwargs.get("refcode")
28+
29+
if not refcode and access_token:
30+
path_parts = request.path.strip("/").split("/")
31+
if len(path_parts) >= 2 and path_parts[0] == "items":
32+
refcode = path_parts[1]
33+
34+
if access_token and refcode:
35+
token_valid = check_access_token(refcode, access_token)
36+
if token_valid:
37+
return func(*args, **kwargs)
38+
2639
if (
2740
(
2841
current_user.is_authenticated
@@ -70,21 +83,23 @@ def check_access_token(refcode: str, token: str | None = None) -> bool:
7083
7184
"""
7285

73-
if not token:
86+
if not token or not refcode:
7487
return False
7588

76-
db = get_database()
89+
try:
90+
if len(refcode.split(":")) != 2:
91+
refcode = f"{CONFIG.IDENTIFIER_PREFIX}:{refcode}"
7792

78-
hashed_token = sha512(token.encode("utf-8")).hexdigest()
93+
token_hash = sha512(token.encode("utf-8")).hexdigest()
7994

80-
access_document = db.api_keys.find_one(
81-
{"token": hashed_token}, projection={"refcode": 1, "valid": 1}
82-
)
83-
if refcode == access_document["refcode"] and access_document["active"]:
84-
LOGGER.info("Access to refcode %s granted with token", refcode, token)
85-
return True
95+
access_token_doc = flask_mongo.db.api_keys.find_one(
96+
{"token": token_hash, "refcode": refcode, "active": True, "type": "access_token"}
97+
)
98+
99+
return bool(access_token_doc)
86100

87-
return False
101+
except Exception:
102+
return False
88103

89104

90105
def get_default_permissions(

pydatalab/src/pydatalab/routes/v0_1/admin.py

Lines changed: 78 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from hashlib import sha512
1+
import datetime
22

33
from bson import ObjectId
44
from flask import Blueprint, jsonify, request
@@ -97,17 +97,86 @@ def save_role(user_id):
9797
return (jsonify({"status": "success"}), 200)
9898

9999

100-
@ADMIN.route("/items/<refcode>/invalidate-access-token")
101-
def invalidate_access_token(refcode: str, METHODS=["POST"]):
102-
request_json = request.get_json() # noqa: F821 pylint: disable=undefined-variable
103-
token = request_json.get("token")
104-
if not token:
105-
return jsonify({"status": "error", "detail": "No token provided."}), 400
100+
@ADMIN.route("/items/<refcode>/invalidate-access-token", methods=["POST"])
101+
def invalidate_access_token(refcode: str):
102+
request_json = request.get_json(silent=True) or {}
103+
104+
if len(refcode.split(":")) != 2:
105+
refcode = f"{CONFIG.IDENTIFIER_PREFIX}:{refcode}"
106+
107+
if request_json.get("token") == "admin-invalidation":
108+
query = {"refcode": refcode, "active": True, "type": "access_token"}
109+
else:
110+
query = {"refcode": refcode, "active": True, "type": "access_token"}
106111

107112
response = flask_mongo.db.api_keys.update_one(
108-
{"token": sha512(token.encode("utf-8")).hexdigest()}, {"$set": {"active": False}}
113+
query,
114+
{
115+
"$set": {
116+
"active": False,
117+
"invalidated_at": datetime.datetime.now(tz=datetime.timezone.utc),
118+
"invalidated_by": ObjectId(current_user.id),
119+
}
120+
},
109121
)
122+
110123
if response.modified_count == 1:
111124
return jsonify({"status": "success"}), 200
125+
else:
126+
return jsonify({"status": "error", "detail": "Token not found or already invalidated"}), 404
127+
128+
129+
@ADMIN.route("/access-tokens", methods=["GET"])
130+
def list_access_tokens():
131+
"""List all access tokens with their status and metadata."""
132+
133+
pipeline = [
134+
{"$match": {"type": "access_token"}},
135+
{
136+
"$lookup": {
137+
"from": "items",
138+
"localField": "refcode",
139+
"foreignField": "refcode",
140+
"as": "item_info",
141+
}
142+
},
143+
{
144+
"$lookup": {
145+
"from": "users",
146+
"localField": "user",
147+
"foreignField": "_id",
148+
"as": "user_info",
149+
}
150+
},
151+
{
152+
"$project": {
153+
"_id": 1,
154+
"refcode": 1,
155+
"active": 1,
156+
"created_at": 1,
157+
"invalidated_at": 1,
158+
"token": {"$substr": ["$token", 0, 16]},
159+
"item_name": {
160+
"$cond": {
161+
"if": {"$gt": [{"$size": "$item_info"}, 0]},
162+
"then": {"$arrayElemAt": ["$item_info.name", 0]},
163+
"else": None,
164+
}
165+
},
166+
"item_id": {"$arrayElemAt": ["$item_info.item_id", 0]},
167+
"item_type": {
168+
"$cond": {
169+
"if": {"$gt": [{"$size": "$item_info"}, 0]},
170+
"then": {"$arrayElemAt": ["$item_info.type", 0]},
171+
"else": "deleted",
172+
}
173+
},
174+
"created_by": {"$arrayElemAt": ["$user_info.display_name", 0]},
175+
}
176+
},
177+
{"$sort": {"created_at": -1}},
178+
]
179+
180+
tokens = list(flask_mongo.db.api_keys.aggregate(pipeline))
112181

113-
return jsonify({"status": "error", "detail": "Unable to invalidate token"}), 400
182+
return jsonify({"status": "success", "tokens": tokens}), 200

0 commit comments

Comments
 (0)