Compare commits
	
		
			7 Commits
		
	
	
		
			treeview
			...
			doorboy-di
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| fb8c63e86f | |||
| 14a5e8e42d | |||
| f0109677b0 | |||
| ac14ca1adf | |||
| 76cc8e6883 | |||
| ee064bde2d | |||
| 6bba3f9831 | 
							
								
								
									
										13
									
								
								Dockerfile
									
									
									
									
									
								
							
							
						
						
									
										13
									
								
								Dockerfile
									
									
									
									
									
								
							@@ -4,12 +4,17 @@ RUN apt-get update \
 | 
				
			|||||||
    curl ca-certificates iputils-ping \
 | 
					    curl ca-certificates iputils-ping \
 | 
				
			||||||
    python3-ldap python3-pip python3-ldap \
 | 
					    python3-ldap python3-pip python3-ldap \
 | 
				
			||||||
    libjpeg-dev libturbojpeg0-dev \
 | 
					    libjpeg-dev libturbojpeg0-dev \
 | 
				
			||||||
 && rm -rf /var/lib/apt/lists/* \
 | 
					 && rm -rf /var/lib/apt/lists/*
 | 
				
			||||||
 && apt-get clean
 | 
					
 | 
				
			||||||
 COPY requirements.txt ./
 | 
					 COPY requirements.txt ./
 | 
				
			||||||
#necessary hack for cffi
 | 
					#necessary hack for cffi
 | 
				
			||||||
RUN pip3 install cffi
 | 
					RUN pip3 install cffi
 | 
				
			||||||
RUN pip3 install -r requirements.txt
 | 
					RUN pip3 install -r requirements.txt
 | 
				
			||||||
COPY inventory-app /app
 | 
					
 | 
				
			||||||
 | 
					ENV PYTHONUNBUFFERED=1
 | 
				
			||||||
 | 
					ENV ENVIRONMENT_TYPE=PROD
 | 
				
			||||||
 | 
					
 | 
				
			||||||
WORKDIR /app
 | 
					WORKDIR /app
 | 
				
			||||||
ENTRYPOINT /app/main.py
 | 
					COPY inventory-app .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					ENTRYPOINT ["python3", "/app/main.py"]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,3 +10,5 @@
 | 
				
			|||||||
|k-space:inventory:audit|Update last time item information confirmed to be accurate|
 | 
					|k-space:inventory:audit|Update last time item information confirmed to be accurate|
 | 
				
			||||||
|k-space:inventory:edit|Edit all non-key items. Browse items with Protected visibility.|
 | 
					|k-space:inventory:edit|Edit all non-key items. Browse items with Protected visibility.|
 | 
				
			||||||
|k-space:inventory:keys|Edit keys|
 | 
					|k-space:inventory:keys|Edit keys|
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For door access, assumes `k-space:floor` and `k-space:workshop`, same in doorboy-proxy.
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					# This is unmaintained. See git.k-space.ee/k-space/kube//hackerspace/inventory.yaml
 | 
				
			||||||
---
 | 
					---
 | 
				
			||||||
apiVersion: apps/v1
 | 
					apiVersion: apps/v1
 | 
				
			||||||
kind: Deployment
 | 
					kind: Deployment
 | 
				
			||||||
@@ -23,14 +24,6 @@ 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
 | 
					 | 
				
			||||||
              value: "changeme"
 | 
					 | 
				
			||||||
            - name: INVENTORY_API_KEY
 | 
					 | 
				
			||||||
              value: "sptWL6XFxl4b8"
 | 
					 | 
				
			||||||
            - name: PYTHONUNBUFFERED
 | 
					 | 
				
			||||||
              value: "1"
 | 
					 | 
				
			||||||
            # Google test key
 | 
					            # Google test key
 | 
				
			||||||
            - name: RECAPTCHA_PUBLIC_KEY
 | 
					            - name: RECAPTCHA_PUBLIC_KEY
 | 
				
			||||||
              value: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
 | 
					              value: 6LeIxAcTAAAAAJcZVRqyHh71UMIEGNQ_MXjiZKhI
 | 
				
			||||||
@@ -48,8 +41,6 @@ spec:
 | 
				
			|||||||
                secretKeyRef:
 | 
					                secretKeyRef:
 | 
				
			||||||
                  name: miniobucket-inventory-app-owner-secrets
 | 
					                  name: miniobucket-inventory-app-owner-secrets
 | 
				
			||||||
                  key: MINIO_URI
 | 
					                  key: MINIO_URI
 | 
				
			||||||
            - name: SECRET_KEY
 | 
					 | 
				
			||||||
              value: "bad_secret"
 | 
					 | 
				
			||||||
            - name: ENVIRONMENT_TYPE
 | 
					            - name: ENVIRONMENT_TYPE
 | 
				
			||||||
              value: "DEV"
 | 
					              value: "DEV"
 | 
				
			||||||
            - name: MY_POD_NAME
 | 
					            - name: MY_POD_NAME
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,117 +0,0 @@
 | 
				
			|||||||
import re
 | 
					 | 
				
			||||||
import const
 | 
					 | 
				
			||||||
import time
 | 
					 | 
				
			||||||
import threading
 | 
					 | 
				
			||||||
from datetime import datetime
 | 
					 | 
				
			||||||
from functools import wraps
 | 
					 | 
				
			||||||
from pymongo import MongoClient
 | 
					 | 
				
			||||||
from flask import Blueprint, g, request, jsonify
 | 
					 | 
				
			||||||
from common import slack_post
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
page_api = Blueprint("api", __name__)
 | 
					 | 
				
			||||||
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'])
 | 
					 | 
				
			||||||
def view_slack_doorboy():
 | 
					 | 
				
			||||||
    begin_time = time.perf_counter()
 | 
					 | 
				
			||||||
    if request.form.get("token") != const.SLACK_VERIFICATION_TOKEN:
 | 
					 | 
				
			||||||
        return "Invalid token was supplied"
 | 
					 | 
				
			||||||
    if request.form.get("channel_id") not in ("C01CWPF5H8W", "CDL9H8Q9W"):
 | 
					 | 
				
			||||||
        return "Invalid channel was supplied"
 | 
					 | 
				
			||||||
    command =  request.form.get("command")
 | 
					 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
        door = {
 | 
					 | 
				
			||||||
            "/open-all-doors": "outsidedoors",
 | 
					 | 
				
			||||||
            "/open-back-door": "backdoor",
 | 
					 | 
				
			||||||
            "/open-front-door": "frontdoor",
 | 
					 | 
				
			||||||
            "/open-ground-door": "grounddoor",
 | 
					 | 
				
			||||||
            "/open-workshop-door": "workshopdoor"
 | 
					 | 
				
			||||||
        }[command]
 | 
					 | 
				
			||||||
    except KeyError:
 | 
					 | 
				
			||||||
        return "Invalid command was supplied"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    member = None
 | 
					 | 
				
			||||||
    for user in g.users:
 | 
					 | 
				
			||||||
        if user.slack_id == request.form.get("user_id"):
 | 
					 | 
				
			||||||
            member = user
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if door == "workshopdoor":
 | 
					 | 
				
			||||||
        access_group = "k-space:workshop"
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        access_group = "k-space:floor"
 | 
					 | 
				
			||||||
    approved = access_group in member.groups
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    doors = [door]
 | 
					 | 
				
			||||||
    if door == "outsidedoors":
 | 
					 | 
				
			||||||
         doors = ["backdoor", "frontdoor", "grounddoor"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    status = "Permitted" if approved else "Denied"
 | 
					 | 
				
			||||||
    subject = member.display_name
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    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()
 | 
					 | 
				
			||||||
    print(f"view_slack_doorboy done in {end_time - begin_time:.4f} seconds")
 | 
					 | 
				
			||||||
    return return_message
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def handle_slack_door_event(doors, approved, member, door, status, subject):
 | 
					 | 
				
			||||||
    begin_time = time.perf_counter()
 | 
					 | 
				
			||||||
    for d in doors:
 | 
					 | 
				
			||||||
        db.eventlog.insert_one({
 | 
					 | 
				
			||||||
            "method": "slack",
 | 
					 | 
				
			||||||
            "approved": approved,
 | 
					 | 
				
			||||||
            "duration": 5,
 | 
					 | 
				
			||||||
            "component": "doorboy",
 | 
					 | 
				
			||||||
            "type": "open-door",
 | 
					 | 
				
			||||||
            "door": d,
 | 
					 | 
				
			||||||
            "member_id": member.username,
 | 
					 | 
				
			||||||
            "member": member.display_name,
 | 
					 | 
				
			||||||
            "timestamp": datetime.utcnow(),
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    msg = "%s %s door access for %s via Slack bot" % (status, door, subject)
 | 
					 | 
				
			||||||
    slack_post(msg, "doorboy")
 | 
					 | 
				
			||||||
    end_time = time.perf_counter()
 | 
					 | 
				
			||||||
    print(f"handle_slack_door_event done in {end_time - begin_time:.4f} seconds")
 | 
					 | 
				
			||||||
@@ -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 = []
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,12 +12,8 @@ def file_exists(path):
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
ENVIRONMENT_TYPE = getenv_in("ENVIRONMENT_TYPE", "DEV", "PROD")
 | 
					ENVIRONMENT_TYPE = getenv_in("ENVIRONMENT_TYPE", "DEV", "PROD")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
SECRET_KEY = os.environ["SECRET_KEY"]
 | 
					 | 
				
			||||||
AWS_S3_ENDPOINT_URL = os.environ["AWS_S3_ENDPOINT_URL"]
 | 
					AWS_S3_ENDPOINT_URL = os.environ["AWS_S3_ENDPOINT_URL"]
 | 
				
			||||||
BUCKET_NAME = os.environ["BUCKET_NAME"]
 | 
					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_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"]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +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 api import check_api_key
 | 
					import pytz
 | 
				
			||||||
from common import slack_post, User
 | 
					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()
 | 
				
			||||||
@@ -28,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 }
 | 
				
			||||||
@@ -128,23 +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/<door>/open")
 | 
					@page_doorboy.route("/m/doorboy/<door>/open")
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
def view_doorboy_open(door):
 | 
					def view_doorboy_open(door):
 | 
				
			||||||
@@ -158,42 +161,22 @@ 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:
 | 
				
			||||||
        return "", 401
 | 
					        return "", 401
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
@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")
 | 
					@page_doorboy.route("/m/doorboy")
 | 
				
			||||||
@login_required
 | 
					@login_required
 | 
				
			||||||
def view_doorboy():
 | 
					def view_doorboy():
 | 
				
			||||||
@@ -295,73 +278,3 @@ def view_doorboy_token_events(token_id):
 | 
				
			|||||||
    token = db.inventory.find_one({"_id": ObjectId(token_id)})
 | 
					    token = db.inventory.find_one({"_id": ObjectId(token_id)})
 | 
				
			||||||
    latest_events = db.eventlog.find({"component": "doorboy", "event":"card-swiped", "token.uid_hash": token.get("token").get("uid_hash")}).sort([("timestamp", -1)])
 | 
					    latest_events = db.eventlog.find({"component": "doorboy", "event":"card-swiped", "token.uid_hash": token.get("token").get("uid_hash")}).sort([("timestamp", -1)])
 | 
				
			||||||
    return render_template("doorboy.html", **locals())
 | 
					    return render_template("doorboy.html", **locals())
 | 
				
			||||||
 | 
					 | 
				
			||||||
class FormSwipe(FlaskForm):
 | 
					 | 
				
			||||||
    class Meta:
 | 
					 | 
				
			||||||
        csrf = False
 | 
					 | 
				
			||||||
    uid = StringField('uid', validators=[])
 | 
					 | 
				
			||||||
    uid_hash = StringField('uid', validators=[])
 | 
					 | 
				
			||||||
    door = StringField('door', validators=[DataRequired()])
 | 
					 | 
				
			||||||
    success = BooleanField('success', validators=[])
 | 
					 | 
				
			||||||
    timestamp = DateTimeField('timestamp')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@page_doorboy.route("/m/doorboy/swipe", methods=["POST"])
 | 
					 | 
				
			||||||
@check_api_key
 | 
					 | 
				
			||||||
def view_swipe():
 | 
					 | 
				
			||||||
    form = request.json
 | 
					 | 
				
			||||||
    print(form)
 | 
					 | 
				
			||||||
    timestamp = parse(form["timestamp"]) if form.get("timestamp") else None
 | 
					 | 
				
			||||||
    now = datetime.utcnow()
 | 
					 | 
				
			||||||
    # Make sure token exists
 | 
					 | 
				
			||||||
    db.inventory.update_one({
 | 
					 | 
				
			||||||
        "type": "token",
 | 
					 | 
				
			||||||
        "component": "doorboy",
 | 
					 | 
				
			||||||
        "token.uid_hash": form["uid_hash"]
 | 
					 | 
				
			||||||
    }, {
 | 
					 | 
				
			||||||
        "$set": {
 | 
					 | 
				
			||||||
            "last_seen": timestamp or now
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "$setOnInsert": {
 | 
					 | 
				
			||||||
            "component": "doorboy",
 | 
					 | 
				
			||||||
            "type": "token",
 | 
					 | 
				
			||||||
            "first_seen": now,
 | 
					 | 
				
			||||||
            "inventory": {
 | 
					 | 
				
			||||||
                "claimable": True,
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
    }, upsert=True)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    # Fetch token to read owner
 | 
					 | 
				
			||||||
    token = db.inventory.find_one({
 | 
					 | 
				
			||||||
        "type": "token",
 | 
					 | 
				
			||||||
        "component": "doorboy",
 | 
					 | 
				
			||||||
        "token.uid_hash": form["uid_hash"]
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    event_swipe = {
 | 
					 | 
				
			||||||
        "component": "doorboy",
 | 
					 | 
				
			||||||
        "timestamp": timestamp,
 | 
					 | 
				
			||||||
        "door": form["door"],
 | 
					 | 
				
			||||||
        "event": "card-swiped",
 | 
					 | 
				
			||||||
        "success": form["success"],
 | 
					 | 
				
			||||||
        "token": {
 | 
					 | 
				
			||||||
          "uid_hash": form["uid_hash"]
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        "inventory": {}
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if token.get("inventory", {}).get("owner", {}).get("username", None):
 | 
					 | 
				
			||||||
        event_swipe["inventory"]["owner_id"] = token["inventory"]["owner"]["username"]
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    db.eventlog.insert_one(event_swipe)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    status = "Permitted" if form["success"] else "Denied"
 | 
					 | 
				
			||||||
    username = token.get("inventory", {}).get("owner", {}).get("username", None)
 | 
					 | 
				
			||||||
    if username and username in g.users_lookup:
 | 
					 | 
				
			||||||
        subject = g.users_lookup[username].display_name or username
 | 
					 | 
				
			||||||
    else:
 | 
					 | 
				
			||||||
        subject = "Unknown"
 | 
					 | 
				
			||||||
    msg = "%s %s door access for %s identified by keycard/keyfob" % (status, form["door"], subject)
 | 
					 | 
				
			||||||
    slack_post(msg, "doorboy")
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return "ok"
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,9 +25,8 @@ from wtforms import (
 | 
				
			|||||||
import const
 | 
					import const
 | 
				
			||||||
from common import devenv, format_name, get_users, User
 | 
					from common import devenv, format_name, get_users, User
 | 
				
			||||||
from inventory import page_inventory
 | 
					from inventory import page_inventory
 | 
				
			||||||
from oidc import page_oidc, login_required, read_user
 | 
					from oidc import page_oidc, login_required
 | 
				
			||||||
from doorboy import page_doorboy
 | 
					from doorboy import page_doorboy
 | 
				
			||||||
from api import page_api
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
def check_foreign_key_format(item):
 | 
					def check_foreign_key_format(item):
 | 
				
			||||||
    owner = item.get("inventory", {}).get("owner", {})
 | 
					    owner = item.get("inventory", {}).get("owner", {})
 | 
				
			||||||
@@ -124,12 +123,9 @@ app = Flask(__name__)
 | 
				
			|||||||
app.wsgi_app = ReverseProxied(app.wsgi_app)
 | 
					app.wsgi_app = ReverseProxied(app.wsgi_app)
 | 
				
			||||||
app.register_blueprint(page_inventory)
 | 
					app.register_blueprint(page_inventory)
 | 
				
			||||||
app.register_blueprint(page_oidc)
 | 
					app.register_blueprint(page_oidc)
 | 
				
			||||||
app.register_blueprint(page_api)
 | 
					 | 
				
			||||||
app.register_blueprint(page_doorboy)
 | 
					app.register_blueprint(page_doorboy)
 | 
				
			||||||
metrics = PrometheusMetrics(app, group_by="path")
 | 
					metrics = PrometheusMetrics(app, group_by="path")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
app.config['SECRET_KEY'] = const.SECRET_KEY
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
mongoclient = MongoClient(const.MONGO_URI)
 | 
					mongoclient = MongoClient(const.MONGO_URI)
 | 
				
			||||||
mongodb = mongoclient.get_default_database()
 | 
					mongodb = mongoclient.get_default_database()
 | 
				
			||||||
mongodb.member.create_index("ad.username", sparse=True, unique=True)
 | 
					mongodb.member.create_index("ad.username", sparse=True, unique=True)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -84,7 +84,7 @@ Does not include door opens by webhook.
 | 
				
			|||||||
        <td>{{ o.door }}</td>
 | 
					        <td>{{ o.door }}</td>
 | 
				
			||||||
        <td>{% if o.inventory and o.inventory.owner %}<a href="/m/user/{{ o.inventory.owner.username }}">{{ o.inventory.owner.username | display_name }}</a>{% else %}Unknown{% endif %}</td>
 | 
					        <td>{% if o.inventory and o.inventory.owner %}<a href="/m/user/{{ o.inventory.owner.username }}">{{ o.inventory.owner.username | display_name }}</a>{% else %}Unknown{% endif %}</td>
 | 
				
			||||||
        <td><a href="/m/doorboy/{{ o._id }}/events">{{ o.token.uid_hash[-6:] }}</a></td>
 | 
					        <td><a href="/m/doorboy/{{ o._id }}/events">{{ o.token.uid_hash[-6:] }}</a></td>
 | 
				
			||||||
        <!-- <td>{% if o.success %}<i class="material-icons">check_circle</i>{% else %} {% endif %}</td> -->
 | 
					        <!-- <td>{% if o.approved %}<i class="material-icons">check_circle</i>{% else %} {% endif %}</td> -->
 | 
				
			||||||
        <td>{% if o.inventory and o.inventory.owner %}{{ o.token.comment }}{% else %}<a class="waves-effect waves-light btn" href="/m/doorboy/{{ o._id }}/claim">Claim keycard</a>{% endif %}</td>
 | 
					        <td>{% if o.inventory and o.inventory.owner %}{{ o.token.comment }}{% else %}<a class="waves-effect waves-light btn" href="/m/doorboy/{{ o._id }}/claim">Claim keycard</a>{% endif %}</td>
 | 
				
			||||||
      </tr>
 | 
					      </tr>
 | 
				
			||||||
    {% endfor %}
 | 
					    {% endfor %}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user