fmt with ruff

This commit is contained in:
2025-08-08 01:14:46 +03:00
parent d6807e3f01
commit abffe7c594
5 changed files with 130 additions and 92 deletions

View File

@@ -1,13 +1,16 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
import os
from datetime import date, datetime from datetime import date, datetime
from functools import wraps
from typing import List from typing import List
from dateutil.parser import parse from dateutil.parser import parse
import httpx
from functools import wraps
from motor.motor_asyncio import AsyncIOMotorClient from motor.motor_asyncio import AsyncIOMotorClient
from pymongo.errors import PyMongoError from pymongo.errors import PyMongoError
import os from sanic import Sanic
from sanic.response import json, text
from sanic_prometheus import monitor
from .slack import add_slack_routes from .slack import add_slack_routes
from .users import users_with_group from .users import users_with_group
@@ -15,12 +18,14 @@ app = Sanic(__name__)
add_slack_routes(app) add_slack_routes(app)
monitor(app).expose_endpoint() monitor(app).expose_endpoint()
DOORBOY_SECRET_FLOOR = os.environ["DOORBOY_SECRET_FLOOR"] # API key for godoor controllers authenticating for k-space:floor # API key for godoor controllers authenticating to k-space:floor
DOORBOY_SECRET_WORKSHOP = os.environ["DOORBOY_SECRET_WORKSHOP"] # API key for godoor controllers authenticating for k-space:workshop DOORBOY_SECRET_FLOOR = os.environ["DOORBOY_SECRET_FLOOR"]
FLOOR_ACCESS_GROUP = os.environ["FLOOR_ACCESS_GROUP"] # API key for godoor controllers authenticating to k-space:workshop
WORKSHOP_ACCESS_GROUP = os.environ["WORKSHOP_ACCESS_GROUP"] DOORBOY_SECRET_WORKSHOP = os.environ["DOORBOY_SECRET_WORKSHOP"]
FLOOR_ACCESS_GROUP = os.getenv("FLOOR_ACCESS_GROUP", "k-space:floor")
WORKSHOP_ACCESS_GROUP = os.getenv("WORKSHOP_ACCESS_GROUP", "k-space:workshop")
MONGO_URI = os.environ["MONGO_URI"] MONGO_URI = os.environ["MONGO_URI"]
INVENTORY_API_KEY = os.environ["INVENTORY_API_KEY"] # asshole forwards to inventory-app, instead of using mongo directly
assert len(DOORBOY_SECRET_FLOOR) >= 10 assert len(DOORBOY_SECRET_FLOOR) >= 10
assert len(DOORBOY_SECRET_WORKSHOP) >= 10 assert len(DOORBOY_SECRET_WORKSHOP) >= 10
@@ -32,6 +37,7 @@ async def setup_db(app, loop):
# https://github.com/sanic-org/sanic/issues/919 # https://github.com/sanic-org/sanic/issues/919
app.ctx.db = AsyncIOMotorClient(MONGO_URI).get_default_database() app.ctx.db = AsyncIOMotorClient(MONGO_URI).get_default_database()
def authenticate_door(wrapped): def authenticate_door(wrapped):
def decorator(f): def decorator(f):
@wraps(f) @wraps(f)
@@ -41,9 +47,12 @@ def authenticate_door(wrapped):
return text("Invalid doorboy secret token", status=401) return text("Invalid doorboy secret token", status=401)
return await f(request, *args, **kwargs) return await f(request, *args, **kwargs)
return decorated_function return decorated_function
return decorator(wrapped) return decorator(wrapped)
@app.route("/allowed") @app.route("/allowed")
@authenticate_door @authenticate_door
async def view_doorboy_uids(request): async def view_doorboy_uids(request):
@@ -61,56 +70,67 @@ async def view_doorboy_uids(request):
flt = { flt = {
"token.uid_hash": {"$exists": True}, "token.uid_hash": {"$exists": True},
"inventory.owner.username": {"$in": users} "inventory.owner.username": {"$in": users},
}
prj = {
"token.uid_hash": True
} }
prj = {"token.uid_hash": True}
tokens = await app.ctx.db.inventory.find(flt, prj) tokens = await app.ctx.db.inventory.find(flt, prj)
print(f"delegating {len(tokens)} doorkey tokens") print(f"delegating {len(tokens)} doorkey tokens")
return json({"allowed_uids": tokens}) return json({"allowed_uids": tokens})
def datetime_to_json_formatting(o): def datetime_to_json_formatting(o):
if isinstance(o, (date, datetime)): if isinstance(o, (date, datetime)):
return o.isoformat() return o.isoformat()
# Only identified and successful events. **Endpoint not in use.** # Only identified and successful events. **Endpoint not in use.**
@app.route("/logs") @app.route("/logs")
async def view_open_door_events(request): async def view_open_door_events(request):
return text("not an admin"), 403 return text("not an admin"), 403
results = await app.ctx.db.eventlog.find({ results = (
"component": "doorboy", await app.ctx.db.eventlog.find(
"type": "open-door", {
"approved": True, "component": "doorboy",
"$or": [ "type": "open-door",
{ "type": "open-door" }, "approved": True,
{ "event": "card-swiped" }, "$or": [
], {"type": "open-door"},
"door": { "$exists": True }, {"event": "card-swiped"},
"timestamp": { "$exists": True } ],
}).sort("timestamp", -1).to_list(length=None) "door": {"$exists": True},
"timestamp": {"$exists": True},
}
)
.sort("timestamp", -1)
.to_list(length=None)
)
transformed = [] transformed = []
for r in results: for r in results:
if r.get("type") == "open-door" and r.get("approved") and r.get("method"): if r.get("type") == "open-door" and r.get("approved") and r.get("method"):
transformed.append({ transformed.append(
"method": r.get("method"), {
"door": r["door"], "method": r.get("method"),
"timestamp": r.get("timestamp"), "door": r["door"],
"member": r.get("member"), "timestamp": r.get("timestamp"),
}) "member": r.get("member"),
}
)
if r.get("event") == "card-swiped" and r.get("success"): if r.get("event") == "card-swiped" and r.get("success"):
transformed.append({ transformed.append(
"method": "card-swiped", {
"door": r["door"], "method": "card-swiped",
"timestamp": r.get("timestamp"), "door": r["door"],
"member": r.get("inventory", {}).get("owner") "timestamp": r.get("timestamp"),
}) "member": r.get("inventory", {}).get("owner"),
}
)
return json(transformed, default=datetime_to_json_formatting) return json(transformed, default=datetime_to_json_formatting)
@app.route("/longpoll", stream=True) @app.route("/longpoll", stream=True)
@authenticate_door @authenticate_door
async def view_longpoll(request): async def view_longpoll(request):
@@ -119,8 +139,8 @@ async def view_longpoll(request):
pipeline = [ pipeline = [
{ {
"$match": { "$match": {
"operationType": "insert", "operationType": "insert",
} }
} }
] ]
try: try:
@@ -139,6 +159,7 @@ async def view_longpoll(request):
await response.send("data: response-generator-ended\n\n") await response.send("data: response-generator-ended\n\n")
return return
# Called by the door to log a card swipe. Does not decide whether the door should be opened. # Called by the door to log a card swipe. Does not decide whether the door should be opened.
@app.post("/swipe") @app.post("/swipe")
@authenticate_door @authenticate_door
@@ -155,30 +176,30 @@ async def swipe(request):
print("WARN: Door", repr(data.get("door")), "not in", doors) print("WARN: Door", repr(data.get("door")), "not in", doors)
return text("Not allowed", 403) return text("Not allowed", 403)
timestamp = parse(data["timestamp"]) if data.get("timestamp") else datetime.now(datetime.timezone.utc) timestamp = (
parse(data["timestamp"])
if data.get("timestamp")
else datetime.now(datetime.timezone.utc)
)
# Update token, create if unknown # Update token, create if unknown
app.ctx.db.inventory.update_one({ await app.ctx.db.inventory.update_one(
"component": "doorboy", {"component": "doorboy", "type": "token", "token.uid_hash": data["uid_hash"]},
"type": "token", {
"token.uid_hash": data["uid_hash"] "$set": {"last_seen": timestamp},
}, { "$setOnInsert": {
"$set": { "first_seen": timestamp,
"last_seen": timestamp "inventory": {
"claimable": True,
},
},
}, },
"$setOnInsert": { upsert=True,
"first_seen": timestamp, )
"inventory": {
"claimable": True,
}
}
}, upsert=True)
token = app.ctx.db.inventory.find_one({ token = await app.ctx.db.inventory.find_one(
"component": "doorboy", {"component": "doorboy", "type": "token", "token.uid_hash": data["uid_hash"]}
"type": "token", )
"token.uid_hash": data["uid_hash"]
})
event_swipe = { event_swipe = {
"component": "doorboy", "component": "doorboy",
@@ -187,15 +208,20 @@ async def swipe(request):
"door": data["door"], "door": data["door"],
"event": "card-swiped", "event": "card-swiped",
"approved": data["approved"], "approved": data["approved"],
"uid_hash": data["uid_hash"], "uid_hash": data["uid_hash"],
"user": { "user": {
"id": token.get("inventory", {}).get("owner", {}).get("username", ""), "id": token.get("inventory", {}).get("owner", {}).get("username", ""),
"name": token.get("inventory", {}).get("owner", {}).get("display_name", "Unclaimed Token") "name": token.get("inventory", {})
} .get("owner", {})
.get("display_name", "Unclaimed Token"),
},
} }
app.ctx.db.eventlog.insert_one(event_swipe) await app.ctx.db.eventlog.insert_one(event_swipe)
return text("ok") return text("ok")
if __name__ == "__main__": if __name__ == "__main__":
app.run(debug=False, host="0.0.0.0", port=5000, single_process=True, access_log=True) app.run(
debug=False, host="0.0.0.0", port=5000, single_process=True, access_log=True
)

View File

@@ -3,10 +3,13 @@ from requests.exceptions import RequestException
import os import os
import requests import requests
SLACK_DOORLOG_CALLBACK = os.environ["SLACK_DOORLOG_CALLBACK"] # webhook logs to private channel or "DEV" to print to console. # webhook logs to private channel or "DEV" to print to console.
SLACK_DOORLOG_CALLBACK = os.environ["SLACK_DOORLOG_CALLBACK"]
def add_slack_routes(app): def add_slack_routes(app):
app.app.register_listener(slack_log, "after_server_start") # consumes SLACK_DOORLOG_CALLBACK and app.ctx.db app.app.register_listener(slack_log, "after_server_start")
def slack_post(msg): def slack_post(msg):
if SLACK_DOORLOG_CALLBACK == "DEV": if SLACK_DOORLOG_CALLBACK == "DEV":
@@ -14,29 +17,39 @@ def slack_post(msg):
return return
try: try:
requests.post(SLACK_DOORLOG_CALLBACK, json={"text": msg }).raise_for_status() requests.post(SLACK_DOORLOG_CALLBACK, json={"text": msg}).raise_for_status()
except RequestException as e: except RequestException as e:
print(f"[SLACK]: {e}") print(f"[SLACK]: {e}")
def approvedStr(approved: bool) -> str: def approvedStr(approved: bool) -> str:
if approved: if approved:
return "Permitted" return "Permitted"
return "Denied" return "Denied"
# consumes SLACK_DOORLOG_CALLBACK and app.ctx.db
async def slack_log(app, loop): async def slack_log(app, loop):
pipeline = [{ pipeline = [
"$match": { {
"operationType": "insert", "$match": {
"operationType": "insert",
}
} }
}] ]
while True: while True:
try: try:
async with app.ctx.db.eventlog.watch(pipeline) as stream: async with app.ctx.db.eventlog.watch(pipeline) as stream:
async for event in stream: async for event in stream:
ev = event["fullDocument"] ev = event["fullDocument"]
msg = "%s %s access for %s via %s" % (approvedStr(ev["approved"]), ev["door"], ev["user"]["name"], ev("method")) msg = "%s %s access for %s via %s" % (
approvedStr(ev["approved"]),
ev["door"],
ev["user"]["name"],
ev("method"),
)
slack_post(msg) slack_post(msg)
except PyMongoError as e: except PyMongoError as e:

View File

@@ -1,22 +1,24 @@
from typing import List from typing import List
from kubernetes import client, config from kubernetes import client, config
import os import os
OIDC_USERS_NAMESPACE = os.getenv("OIDC_USERS_NAMESPACE") OIDC_USERS_NAMESPACE = os.getenv("OIDC_USERS_NAMESPACE")
def users_with_group(group: str) -> List[str]: def users_with_group(group: str) -> List[str]:
config.load_incluster_config() config.load_incluster_config()
api_instance = client.CustomObjectsApi() api_instance = client.CustomObjectsApi()
users = List[str] users = List[str]
ret = api_instance.list_namespaced_custom_object("codemowers.cloud", "v1beta1", OIDC_USERS_NAMESPACE, "oidcusers") ret = api_instance.list_namespaced_custom_object(
"codemowers.cloud", "v1beta1", OIDC_USERS_NAMESPACE, "oidcusers"
)
for item in ret["items"]: for item in ret["items"]:
if group not in item.get("status", {}).get("groups", []): if group not in item.get("status", {}).get("groups", []):
continue continue
users.append(item['metadata']['name']) users.append(item["metadata"]["name"])
print(f"INFO: {len(users)} users in group {group}") print(f"INFO: {len(users)} users in group {group}")
return users return users

View File

@@ -1,5 +1,3 @@
version: '3.7'
services: services:
mongoexpress: mongoexpress:
image: mongo-express image: mongo-express

View File

@@ -1,8 +1,7 @@
httpx==0.28.1
motor==3.7.1 motor==3.7.1
pymongo==4.14.0 pymongo==4.14.0
python_dateutil==2.9.0 python_dateutil==2.9.0
Requests==2.32.4 Requests==2.32.4
sanic==25.3.0 sanic==25.3.0
sanic_prometheus==0.2.1 sanic_prometheus==0.2.1
kubernetes kubernetes==33.1.0