move /cards to doorboy-proxy + refactor mongo format

This commit is contained in:
2025-08-08 03:34:06 +03:00
parent ee064bde2d
commit 76cc8e6883
5 changed files with 48 additions and 124 deletions

View File

@@ -23,12 +23,8 @@ spec:
env: env:
- name: OIDC_USERS_NAMESPACE - name: OIDC_USERS_NAMESPACE
value: "default" value: "default"
- name: SLACK_DOORLOG_CALLBACK
value: "changeme"
- name: SLACK_VERIFICATION_TOKEN - name: SLACK_VERIFICATION_TOKEN
value: "changeme" value: "changeme"
- name: INVENTORY_API_KEY
value: "sptWL6XFxl4b8"
- name: PYTHONUNBUFFERED - name: PYTHONUNBUFFERED
value: "1" value: "1"
# Google test key # Google test key

View File

@@ -1,57 +1,14 @@
import re
import const
import time
import threading import threading
import time
from datetime import datetime from datetime import datetime
from functools import wraps
import const
from flask import Blueprint, g, request
from pymongo import MongoClient from pymongo import MongoClient
from flask import Blueprint, g, request, jsonify
from common import slack_post
page_api = Blueprint("api", __name__) page_api = Blueprint("api", __name__)
db = MongoClient(const.MONGO_URI).get_default_database() db = MongoClient(const.MONGO_URI).get_default_database()
def check_api_key(f):
@wraps(f)
def decorated_function(*args, **kwargs):
request_key = request.headers.get('Authorization', False)
if not request_key:
return "nope", 403
found_key = re.search(r"Basic (.*)", request_key).group(1)
if not found_key or found_key != const.INVENTORY_API_KEY:
return "nope", 403
return f(*args, **kwargs)
return decorated_function
@page_api.route("/cards", methods=["POST"])
@check_api_key
def get_group_cards():
request_groups = request.json.get("groups", False)
if not request_groups:
return "must specify groups in parameter", 400
print(f"found {len(g.users)} users for groups: {request_groups}")
keys = []
for u in g.users:
for group in u.groups:
if group in request_groups:
keys.append(u.username)
break
print(f"{len(keys)} doorkeys")
flt = {
"token.uid_hash": {"$exists": True},
"inventory.owner.username": {"$in": keys}
}
prj = {
"inventory.owner": True,
"token.uid_hash": True
}
found = []
for obj in db.inventory.find(flt, prj):
found.append({"token": obj["token"]})
fl = list(found)
print(f"{len(fl)} doorkey tokens")
return jsonify(fl)
@page_api.route("/api/slack/doorboy", methods=['POST']) @page_api.route("/api/slack/doorboy", methods=['POST'])
def view_slack_doorboy(): def view_slack_doorboy():
begin_time = time.perf_counter() begin_time = time.perf_counter()
@@ -91,27 +48,26 @@ def view_slack_doorboy():
threading.Thread(target=handle_slack_door_event, args=(doors, approved, member, door, status, subject)).start() threading.Thread(target=handle_slack_door_event, args=(doors, approved, member, door, status, subject)).start()
return_message = "Opening %s for %s" % (door, subject) if approved else "Permission denied"
end_time = time.perf_counter() end_time = time.perf_counter()
print(f"view_slack_doorboy done in {end_time - begin_time:.4f} seconds") print(f"view_slack_doorboy done in {end_time - begin_time:.4f} seconds")
return_message = "Opening %s for %s" % (door, subject) if approved else "Permission denied"
return return_message return return_message
def handle_slack_door_event(doors, approved, member, door, status, subject): def handle_slack_door_event(doors, approved, member, door, status, subject):
begin_time = time.perf_counter() begin_time = time.perf_counter()
for d in doors: for d in doors:
db.eventlog.insert_one({ db.eventlog.insert_one({
"method": "slack",
"approved": approved,
"duration": 5,
"component": "doorboy", "component": "doorboy",
"type": "open-door", "method": "slack",
"door": d,
"member_id": member.username,
"member": member.display_name,
"timestamp": datetime.utcnow(), "timestamp": datetime.utcnow(),
"door": d,
"approved": approved,
"user": {
"id": member.username,
"name": member.display_name,
}
}) })
msg = "%s %s door access for %s via Slack bot" % (status, door, subject)
slack_post(msg, "doorboy")
end_time = time.perf_counter() end_time = time.perf_counter()
print(f"handle_slack_door_event done in {end_time - begin_time:.4f} seconds") print(f"handle_slack_door_event done in {end_time - begin_time:.4f} seconds")

View File

@@ -1,17 +1,16 @@
import os
import collections.abc import collections.abc
from functools import wraps import os
import requests
from bson.objectid import ObjectId
from flask import g, request
from flask_wtf import FlaskForm
from pymongo import MongoClient
from kubernetes import client, config
from typing import List
from dataclasses import dataclass, field from dataclasses import dataclass, field
from functools import wraps
from typing import List
import const import const
from bson.objectid import ObjectId
from common import read_user
from flask import g, request
from flask_wtf import FlaskForm
from kubernetes import client, config
from pymongo import MongoClient
devenv = const.ENVIRONMENT_TYPE == "DEV" devenv = const.ENVIRONMENT_TYPE == "DEV"
OIDC_USERS_NAMESPACE = os.getenv("OIDC_USERS_NAMESPACE") OIDC_USERS_NAMESPACE = os.getenv("OIDC_USERS_NAMESPACE")
@@ -93,18 +92,6 @@ def flatten(d, parent_key='', sep='.'):
items.append((new_key, v)) items.append((new_key, v))
return dict(items) return dict(items)
def slack_post(msg, channel):
if devenv:
print(f"{channel}: {msg}")
return
channels = {
"doorboy": const.SLACK_DOORLOG_CALLBACK,
}
url = channels.get(channel, const.SLACK_DOORLOG_CALLBACK)
requests.post(url, json={"text": msg })
def build_query(base_query, fields=[], sort_fields={}): def build_query(base_query, fields=[], sort_fields={}):
top_usernames= ['k-space'] top_usernames= ['k-space']
selectors = [] selectors = []

View File

@@ -18,6 +18,4 @@ BUCKET_NAME = os.environ["BUCKET_NAME"]
INVENTORY_ASSETS_BASE_URL = os.environ["INVENTORY_ASSETS_BASE_URL"] INVENTORY_ASSETS_BASE_URL = os.environ["INVENTORY_ASSETS_BASE_URL"]
MONGO_URI = os.environ["MONGO_URI"] MONGO_URI = os.environ["MONGO_URI"]
SLACK_VERIFICATION_TOKEN = os.environ["SLACK_VERIFICATION_TOKEN"] # used to verify (deprecated) incoming requests from slack SLACK_VERIFICATION_TOKEN = os.environ["SLACK_VERIFICATION_TOKEN"] # used to verify (deprecated) incoming requests from slack
SLACK_DOORLOG_CALLBACK = os.environ["SLACK_DOORLOG_CALLBACK"] # used for sending logs to private channel
INVENTORY_API_KEY = os.environ["INVENTORY_API_KEY"] # used by doorboy-proxy (@check_api_key)
MACADDRESS_OUTLINK_BASEURL = os.environ["MACADDRESS_OUTLINK_BASEURL"] MACADDRESS_OUTLINK_BASEURL = os.environ["MACADDRESS_OUTLINK_BASEURL"]

View File

@@ -1,17 +1,17 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta
from dateutil.parser import parse, ParserError
from bson.objectid import ObjectId
from flask import Blueprint, g, redirect, render_template, request, abort
from flask_wtf import FlaskForm
from pymongo import MongoClient
from wtforms import StringField, IntegerField, SelectField, BooleanField, DateTimeField, validators
from wtforms.validators import DataRequired
import pytz
import const import const
from common import slack_post, User import pytz
from bson.objectid import ObjectId
from common import User
from dateutil.parser import ParserError, parse
from flask import Blueprint, abort, g, redirect, render_template, request
from flask_wtf import FlaskForm
from oidc import login_required, read_user from oidc import login_required, read_user
from pymongo import MongoClient
from wtforms import (BooleanField, IntegerField, SelectField, StringField,
validators)
from wtforms.validators import DataRequired
page_doorboy = Blueprint("doorboy", __name__) page_doorboy = Blueprint("doorboy", __name__)
db = MongoClient(const.MONGO_URI).get_default_database() db = MongoClient(const.MONGO_URI).get_default_database()
@@ -27,7 +27,7 @@ def view_doorboy_claim(event_id):
}) })
# Find token object to associate with user # Find token object to associate with user
token = db.inventory.update_one({ db.inventory.update_one({
"type": "token", "type": "token",
"token.uid_hash": event["token"]["uid_hash"], "token.uid_hash": event["token"]["uid_hash"],
"inventory.owner.username": { "$exists": False } "inventory.owner.username": { "$exists": False }
@@ -127,35 +127,27 @@ class HoldDoorForm(FlaskForm):
door_name = SelectField("Door name", choices=[(j,j) for j in ["grounddoor", "frontdoor", "backdoor"]], validators=[DataRequired()]) door_name = SelectField("Door name", choices=[(j,j) for j in ["grounddoor", "frontdoor", "backdoor"]], validators=[DataRequired()])
duration = IntegerField('Duration in seconds', validators=[DataRequired(), validators.NumberRange(min=5, max=21600)]) duration = IntegerField('Duration in seconds', validators=[DataRequired(), validators.NumberRange(min=5, max=21600)])
# duration=0 to override and close right away
@page_doorboy.route("/m/doorboy/hold", methods=["POST"]) @page_doorboy.route("/m/doorboy/hold", methods=["POST"])
@login_required @login_required
def view_doorboy_hold(): def view_doorboy_hold():
user = read_user() user = read_user()
form = HoldDoorForm(request.form) form = HoldDoorForm(request.form)
now = datetime.utcnow()
if form.validate_on_submit(): if form.validate_on_submit():
db.eventlog.insert_one({ db.eventlog.insert_one({
"component": "doorboy", "component": "doorboy",
"type": "hold", "method": "hold",
"requester": user["name"], "timestamp": datetime.utcnow(),
"door": form.door_name.data, "door": form.door_name.data,
"approved": True,
"user": {
"id": user["username"],
"name": user["name"]
},
"expires": datetime.utcnow() + timedelta(seconds=form.duration.data) "expires": datetime.utcnow() + timedelta(seconds=form.duration.data)
}) })
return redirect("/m/doorboy") return redirect("/m/doorboy")
@page_doorboy.route("/m/doorboy/slam", methods=["POST"])
@login_required
def view_doorboy_slam():
user = read_user()
db.eventlog.insert_one({
"component": "doorboy",
"type": "hold",
"requester": user["name"],
"door": form.door_name.data,
"expires": datetime.utcnow() + timedelta(minutes=form.duration_min.data)
})
return redirect("/m/doorboy")
@page_doorboy.route("/m/doorboy/<door>/open") @page_doorboy.route("/m/doorboy/<door>/open")
@login_required @login_required
def view_doorboy_open(door): def view_doorboy_open(door):
@@ -169,22 +161,17 @@ def view_doorboy_open(door):
access_group = "k-space:floor" access_group = "k-space:floor"
approved = access_group in g.users_lookup.get(user["username"], User()).groups approved = access_group in g.users_lookup.get(user["username"], User()).groups
db.eventlog.insert_one({ db.eventlog.insert_one({
"method": "web",
"approved": approved,
"duration": 5,
"component": "doorboy", "component": "doorboy",
"type": "open-door", "method": "web",
"door": door,
"member_id": user["username"],
"member": user["name"],
"timestamp": datetime.utcnow(), "timestamp": datetime.utcnow(),
"door": door,
"approved": approved,
"user": {
"id": user["username"],
"name": user["name"]
}
}) })
status = "Permitted" if approved else "Denied"
subject = user["name"]
msg = "%s %s door access for %s via https://inventory.k-space.ee/m/doorboy" % (status, door, subject)
slack_post(msg, "doorboy")
if approved: if approved:
return redirect("/m/doorboy") return redirect("/m/doorboy")
else: else: