doorboy-direct #5
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,3 @@ | ||||
| .env | ||||
| .overnodebundle | ||||
| .venv | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| FROM harbor.k-space.ee/k-space/microservice-base | ||||
| RUN pip3 install httpx | ||||
| FROM harbor.k-space.ee/k-space/microservice-base:latest | ||||
|  | ||||
| WORKDIR /app | ||||
| COPY app /app | ||||
| CMD /app/doorboy-proxy.py | ||||
| CMD ["python3", "/app/doorboy-proxy.py"] | ||||
|   | ||||
							
								
								
									
										17
									
								
								README.md
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								README.md
									
									
									
									
									
								
							| @@ -34,3 +34,20 @@ docker-compose -f docker-compose.yml up --build | ||||
|  | ||||
| On kdoorpi override `KDOORPI_API_ALLOWED`, `KDOORPI_API_LONGPOLL` environment variables | ||||
| to redirect requests to your dev instance. | ||||
|  | ||||
| # Deployment | ||||
| ## Slack credentials | ||||
| 1. https://api.slack.com/apps → Create new app → From scratch | ||||
| 1. Verification Token as `SLACK_VERIFICATION_TOKEN` | ||||
| 1. App home → Bot user | ||||
|   - `commands` | ||||
|   - `users:read` | ||||
|   <!-- `users:read_email` --> | ||||
|   <!-- `channel:read` --> | ||||
|   <!-- `groups:read` --> | ||||
|   <!-- `incoming-webhook` --> | ||||
| 1. Add commands. Request URL `https://doorboy-proxy.k-space.ee/slack-open` | ||||
| 1. Incoming Webhooks → assign to channel -> Webhook URL as `SLACK_DOORLOG_CALLBACK` | ||||
|  | ||||
| ## OIDC groups | ||||
| Assumes `k-space:floor` and `k-space:workshop`, same in inventory-app. | ||||
|   | ||||
| @@ -1,25 +1,28 @@ | ||||
| #!/usr/bin/env python3 | ||||
| from datetime import date, datetime | ||||
| from sanic import Sanic | ||||
| from sanic.response import text, json | ||||
| from sanic_prometheus import monitor | ||||
| from motor.motor_asyncio import AsyncIOMotorClient | ||||
| import httpx | ||||
| import pymongo | ||||
| import os | ||||
| from datetime import date, datetime | ||||
| from functools import wraps | ||||
| from typing import List | ||||
|  | ||||
| import kube | ||||
| from dateutil.parser import parse | ||||
| from motor.motor_asyncio import AsyncIOMotorClient | ||||
| from pymongo.errors import PyMongoError | ||||
| from sanic import Sanic | ||||
| from sanic.response import json, text | ||||
| from sanic_prometheus import monitor | ||||
| from slack import slack_app | ||||
|  | ||||
| app = Sanic(__name__) | ||||
| app.register_blueprint(slack_app) | ||||
| monitor(app).expose_endpoint() | ||||
|  | ||||
| INVENTORY_API_KEY = os.environ["INVENTORY_API_KEY"] | ||||
| # API key for godoor controllers authenticating to k-space:floor | ||||
| DOORBOY_SECRET_FLOOR = os.environ["DOORBOY_SECRET_FLOOR"] | ||||
| # API key for godoor controllers authenticating to k-space:workshop | ||||
| DOORBOY_SECRET_WORKSHOP = os.environ["DOORBOY_SECRET_WORKSHOP"] | ||||
| DOORBOY_SECRET_OPEN_EVENTS = os.environ["DOORBOY_SECRET_OPEN_EVENTS"] | ||||
| CARD_URI = os.environ["CARD_URI"] | ||||
| FLOOR_ACCESS_GROUP = os.environ["FLOOR_ACCESS_GROUP"] | ||||
| WORKSHOP_ACCESS_GROUP = os.environ["WORKSHOP_ACCESS_GROUP"] | ||||
|  | ||||
| MONGO_URI = os.environ["MONGO_URI"] | ||||
| SWIPE_URI = os.environ["SWIPE_URI"] | ||||
|  | ||||
| assert len(DOORBOY_SECRET_FLOOR) >= 10 | ||||
| assert len(DOORBOY_SECRET_WORKSHOP) >= 10 | ||||
| @@ -31,110 +34,137 @@ async def setup_db(app, loop): | ||||
|     # https://github.com/sanic-org/sanic/issues/919 | ||||
|     app.ctx.db = AsyncIOMotorClient(MONGO_URI).get_default_database() | ||||
|  | ||||
| @app.route("/allowed") | ||||
| async def view_doorboy_uids(request): | ||||
|     key = request.headers.get("KEY") | ||||
|     if not key or key not in [DOORBOY_SECRET_FLOOR, DOORBOY_SECRET_WORKSHOP]: | ||||
|         return text("how about no") | ||||
|  | ||||
|     groups = [] | ||||
| def authenticate_door(wrapped): | ||||
|     def decorator(f): | ||||
|         @wraps(f) | ||||
|         async def decorated_function(request, *args, **kwargs): | ||||
|             doorboy_secret = request.headers.get("KEY") | ||||
|             if doorboy_secret not in [DOORBOY_SECRET_FLOOR, DOORBOY_SECRET_WORKSHOP]: | ||||
|                 return text("Invalid doorboy secret token", status=401) | ||||
|  | ||||
|             return await f(request, *args, **kwargs) | ||||
|  | ||||
|         return decorated_function | ||||
|  | ||||
|     return decorator(wrapped) | ||||
|  | ||||
|  | ||||
| # Door controllers query uid_hashes for offline use. There is no online uid_hash authn/z, only logging. | ||||
| @app.route("/allowed") | ||||
| @authenticate_door | ||||
| async def view_doorboy_uids(request): | ||||
|     users = List[str] | ||||
|  | ||||
|     # authorize | ||||
|     key = request.headers.get("KEY") | ||||
|     if key == DOORBOY_SECRET_FLOOR: | ||||
|         groups.append(FLOOR_ACCESS_GROUP) | ||||
|     if key == DOORBOY_SECRET_WORKSHOP: | ||||
|         groups.append(WORKSHOP_ACCESS_GROUP) | ||||
|     if not groups: | ||||
|         return "fail", 500 | ||||
|     async with httpx.AsyncClient() as client: | ||||
|         r = await client.post(CARD_URI, json={ | ||||
|             "groups": groups | ||||
|         }, headers={ | ||||
|             "Content-Type": "application/json", | ||||
|             "Authorization": f"Basic {INVENTORY_API_KEY}" | ||||
|         }) | ||||
|     j = r.json() | ||||
|     allowed_uids = [] | ||||
|     for obj in j: | ||||
|         allowed_uids.append({ | ||||
|             "token": obj["token"] | ||||
|         }) | ||||
|     return json({"allowed_uids": allowed_uids}) | ||||
|         users = kube.users_with_group("k-space:floor") | ||||
|     elif key == DOORBOY_SECRET_WORKSHOP: | ||||
|         users = kube.users_with_group("k-space:workshop") | ||||
|     else: | ||||
|         print("WARN: unknown door token in /allowed") | ||||
|         return "unknown doorboy secret token", 403 | ||||
|  | ||||
|     flt = { | ||||
|         "token.uid_hash": {"$exists": True}, | ||||
|         "inventory.owner.username": {"$in": users}, | ||||
|     } | ||||
|     prj = {"token.uid_hash": True} | ||||
|     tokens = await request.app.ctx.db.inventory.find(flt, prj) | ||||
|  | ||||
|     print(f"delegating {len(tokens)} doorkey tokens") | ||||
|     return json({"allowed_uids": tokens}) | ||||
|  | ||||
|  | ||||
| def datetime_to_json_formatting(o): | ||||
|     if isinstance(o, (date, datetime)): | ||||
|         return o.isoformat() | ||||
|  | ||||
| @app.route("/open-door-events") | ||||
| async def view_open_door_events(request): | ||||
|     key = request.headers.get("KEY") | ||||
|     if not key or key != DOORBOY_SECRET_OPEN_EVENTS: | ||||
|         return text("Invalid token") | ||||
|  | ||||
|     results = await app.ctx.db.eventlog.find({ | ||||
|         "component": "doorboy", | ||||
|         "type": "open-door", | ||||
|         "$or": [ | ||||
|             { "approved": True }, | ||||
|             { "success": True }, | ||||
|         ], | ||||
|         "$or": [ | ||||
|             { "type": "open-door" }, | ||||
|             { "event": "card-swiped" }, | ||||
|         ], | ||||
|         "door": { "$exists": True }, | ||||
|         "timestamp": { "$exists": True } | ||||
|     }).sort("timestamp", -1).to_list(length=None) | ||||
| # Only identified and successful events. **Endpoint not in use.** | ||||
| @app.route("/logs") | ||||
| async def view_open_door_events(request): | ||||
|     return text("not an admin"), 403 | ||||
|  | ||||
|     results = ( | ||||
|         await app.ctx.db.eventlog.find( | ||||
|             { | ||||
|                 "component": "doorboy", | ||||
|                 "type": "open-door", | ||||
|                 "approved": True, | ||||
|                 "$or": [ | ||||
|                     {"type": "open-door"}, | ||||
|                     {"event": "card-swiped"}, | ||||
|                 ], | ||||
|                 "door": {"$exists": True}, | ||||
|                 "timestamp": {"$exists": True}, | ||||
|             } | ||||
|         ) | ||||
|         .sort("timestamp", -1) | ||||
|         .to_list(length=None) | ||||
|     ) | ||||
|  | ||||
|     transformed = [] | ||||
|     for r in results: | ||||
|         if r.get("type") == "open-door" and r.get("approved") and r.get("method"): | ||||
|             transformed.append({ | ||||
|                 "method": r.get("method"), | ||||
|                 "door": r["door"], | ||||
|                 "timestamp": r.get("timestamp"), | ||||
|                 "member": r.get("member"), | ||||
|             }) | ||||
|             transformed.append( | ||||
|                 { | ||||
|                     "method": r.get("method"), | ||||
|                     "door": r["door"], | ||||
|                     "timestamp": r.get("timestamp"), | ||||
|                     "member": r.get("member"), | ||||
|                 } | ||||
|             ) | ||||
|         if r.get("event") == "card-swiped" and r.get("success"): | ||||
|             transformed.append({ | ||||
|                 "method": "card-swiped", | ||||
|                 "door": r["door"], | ||||
|                 "timestamp": r.get("timestamp"), | ||||
|                 "member": r.get("inventory", {}).get("owner") | ||||
|             }) | ||||
|             transformed.append( | ||||
|                 { | ||||
|                     "method": "card-swiped", | ||||
|                     "door": r["door"], | ||||
|                     "timestamp": r.get("timestamp"), | ||||
|                     "member": r.get("inventory", {}).get("owner"), | ||||
|                 } | ||||
|             ) | ||||
|  | ||||
|     return json(transformed, default=datetime_to_json_formatting) | ||||
|  | ||||
| @app.route("/longpoll", stream=True) | ||||
| async def view_longpoll(request): | ||||
|     key = request.headers.get("KEY") | ||||
|     if not key or key not in [DOORBOY_SECRET_FLOOR, DOORBOY_SECRET_WORKSHOP]: | ||||
|         return text("Invalid token") | ||||
|  | ||||
| # Door controllers listen for open events (web, slack). | ||||
| @app.route("/longpoll", stream=True) | ||||
| @authenticate_door | ||||
| async def view_longpoll(request): | ||||
|     response = await request.respond(content_type="text/event-stream") | ||||
|     await response.send("data: response-generator-started\n\n") | ||||
|     pipeline = [ | ||||
|         { | ||||
|             "$match": { | ||||
|                  "operationType": "insert", | ||||
|              } | ||||
|                 "operationType": "insert", | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
|     try: | ||||
|         async with app.ctx.db.eventlog.watch(pipeline) as stream: | ||||
|         async with request.app.ctx.db.eventlog.watch(pipeline) as stream: | ||||
|             await response.send("data: watch-stream-opened\n\n") | ||||
|             async for event in stream: | ||||
|                 if event["fullDocument"].get("type") == "open-door" and event["fullDocument"].get("approved", False): | ||||
|                     await response.send("data: %s\n\n" % | ||||
|                                         event["fullDocument"]["door"]) | ||||
|     except pymongo.errors.PyMongoError as e: | ||||
|                 ev = event["fullDocument"] | ||||
|                 if ev["approved"] != "true": | ||||
|                     continue | ||||
|                 if ev["type"] == "card": | ||||
|                     continue | ||||
|  | ||||
|                 response.send("data: %s\n\n" % ev["door"]) | ||||
|     except PyMongoError as e: | ||||
|         print(e) | ||||
|         await response.send("data: response-generator-ended\n\n") | ||||
|         return | ||||
|  | ||||
|  | ||||
| # Door controller reporting a card swipe. No authn/z about the event (done offline on door controller), only logging. | ||||
| @app.post("/swipe") | ||||
| async def forward_swipe(request): | ||||
| @authenticate_door | ||||
| async def swipe(request): | ||||
|     # authorize | ||||
|     key = request.headers.get("KEY") | ||||
|     if not key or key not in [DOORBOY_SECRET_FLOOR, DOORBOY_SECRET_WORKSHOP]: | ||||
|         return text("Invalid token", status=401) | ||||
|     data = request.json | ||||
|     doors = set() | ||||
|     if key == DOORBOY_SECRET_FLOOR: | ||||
| @@ -142,19 +172,54 @@ async def forward_swipe(request): | ||||
|     if key == DOORBOY_SECRET_WORKSHOP: | ||||
|         doors.add("workshopdoor") | ||||
|     if data.get("door") not in doors: | ||||
|         print("Door", repr(data.get("door")), "not in", doors) | ||||
|         print("WARN: Door", repr(data.get("door")), "not in", doors) | ||||
|         return text("Not allowed", 403) | ||||
|  | ||||
|     async with httpx.AsyncClient() as client: | ||||
|         r = await client.post(SWIPE_URI, json=data, headers={ | ||||
|             "Content-Type": "application/json", | ||||
|             "Authorization": f"Basic {INVENTORY_API_KEY}" | ||||
|         }) | ||||
|     if r.status_code == 200: | ||||
|         return text("ok") | ||||
|     else: | ||||
|         return text("Failed", 500) | ||||
|     timestamp = ( | ||||
|         parse(data["timestamp"]) | ||||
|         if data.get("timestamp") | ||||
|         else datetime.now(datetime.timezone.utc) | ||||
|     ) | ||||
|  | ||||
|     # Update token, create if unknown | ||||
|     await request.app.ctx.db.inventory.update_one( | ||||
|         {"component": "doorboy", "type": "token", "token.uid_hash": data["uid_hash"]}, | ||||
|         { | ||||
|             "$set": {"last_seen": timestamp}, | ||||
|             "$setOnInsert": { | ||||
|                 "first_seen": timestamp, | ||||
|                 "inventory": { | ||||
|                     "claimable": True, | ||||
|                 }, | ||||
|             }, | ||||
|         }, | ||||
|         upsert=True, | ||||
|     ) | ||||
|  | ||||
|     token = await request.app.ctx.db.inventory.find_one( | ||||
|         {"component": "doorboy", "type": "token", "token.uid_hash": data["uid_hash"]} | ||||
|     ) | ||||
|  | ||||
|     event_swipe = { | ||||
|         "component": "doorboy", | ||||
|         "method": "card", | ||||
|         "timestamp": timestamp, | ||||
|         "door": data["door"], | ||||
|         "approved": data["approved"], | ||||
|         "user": { | ||||
|             "id": token.get("inventory", {}).get("owner", {}).get("username", ""), | ||||
|             "name": token.get("inventory", {}) | ||||
|             .get("owner", {}) | ||||
|             .get("display_name", "Unclaimed Token"), | ||||
|         }, | ||||
|         "uid_hash": data["uid_hash"], | ||||
|     } | ||||
|     await request.app.ctx.db.eventlog.insert_one(event_swipe) | ||||
|  | ||||
|     return text("ok") | ||||
|  | ||||
|  | ||||
| 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 | ||||
|     ) | ||||
|   | ||||
							
								
								
									
										42
									
								
								app/kube.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								app/kube.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,42 @@ | ||||
| import os | ||||
| from typing import List, Tuple | ||||
|  | ||||
| from kubernetes import client, config | ||||
|  | ||||
| OIDC_USERS_NAMESPACE = os.environ["OIDC_USERS_NAMESPACE"] | ||||
|  | ||||
|  | ||||
| def users_with_group(group: str) -> List[str]: | ||||
|     config.load_incluster_config() | ||||
|     api_instance = client.CustomObjectsApi() | ||||
|  | ||||
|     users = List[str] | ||||
|  | ||||
|     ret = api_instance.list_namespaced_custom_object( | ||||
|         "codemowers.cloud", "v1beta1", OIDC_USERS_NAMESPACE, "oidcusers" | ||||
|     ) | ||||
|     for item in ret["items"]: | ||||
|         if group not in item.get("status", {}).get("groups", []): | ||||
|             continue | ||||
|  | ||||
|         users.append(item["metadata"]["name"]) | ||||
|  | ||||
|     print(f"INFO: {len(users)} users in group {group}") | ||||
|     return users | ||||
|  | ||||
|  | ||||
| # -> (groups[], username) | ||||
| def by_slackid(slack_id: str) -> Tuple[List[str], str]: | ||||
|     config.load_incluster_config() | ||||
|     api_instance = client.CustomObjectsApi() | ||||
|  | ||||
|     ret = api_instance.list_namespaced_custom_object( | ||||
|         "codemowers.cloud", "v1beta1", OIDC_USERS_NAMESPACE, "oidcusers" | ||||
|     ) | ||||
|     for item in ret["items"]: | ||||
|         if slack_id == item.get("status", {}).get("slackId", None): | ||||
|             return item.get("status", {}).get("groups", []), item.get( | ||||
|                 "metadata", {} | ||||
|             ).get("name", "") | ||||
|  | ||||
|     return [], "" | ||||
							
								
								
									
										136
									
								
								app/slack.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								app/slack.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | ||||
| import os | ||||
| from datetime import datetime | ||||
| from typing import Tuple | ||||
|  | ||||
| import kube | ||||
| import requests | ||||
| from pymongo.errors import PyMongoError | ||||
| from requests.exceptions import RequestException | ||||
| from sanic import Blueprint | ||||
|  | ||||
| slack_app = Blueprint("slack", __name__) | ||||
|  | ||||
| # webhook logs to private channel or "DEV" to print to console. | ||||
| SLACK_DOORLOG_CALLBACK = os.environ["SLACK_DOORLOG_CALLBACK"] | ||||
| # used to verify (deprecated) incoming requests from slack | ||||
| SLACK_VERIFICATION_TOKEN = os.environ["SLACK_VERIFICATION_TOKEN"] | ||||
| SLACK_CHANNEL_ID = os.environ["SLACK_CHANNEL_ID"]  # TODO: | ||||
|  | ||||
|  | ||||
| def slack_post(msg): | ||||
|     if SLACK_DOORLOG_CALLBACK == "DEV": | ||||
|         print(f"[DEV SLACK]: {msg}") | ||||
|         return | ||||
|  | ||||
|     try: | ||||
|         requests.post(SLACK_DOORLOG_CALLBACK, json={"text": msg}).raise_for_status() | ||||
|     except RequestException as e: | ||||
|         print(f"[SLACK]: {e}") | ||||
|  | ||||
|  | ||||
| def approvedStr(approved: bool) -> str: | ||||
|     if approved: | ||||
|         return "Permitted" | ||||
|  | ||||
|     return "Denied" | ||||
|  | ||||
|  | ||||
| # consumes SLACK_DOORLOG_CALLBACK and app.ctx.db | ||||
| @slack_app.listener("after_server_start") | ||||
| async def slack_log_fwd(app, loop): | ||||
|     pipeline = [ | ||||
|         { | ||||
|             "$match": { | ||||
|                 "operationType": "insert", | ||||
|             } | ||||
|         } | ||||
|     ] | ||||
|     while True: | ||||
|         try: | ||||
|             async with app.ctx.db.eventlog.watch(pipeline) as stream: | ||||
|                 async for event in stream: | ||||
|                     ev = event["fullDocument"] | ||||
|  | ||||
|                     msg = "%s %s access for %s via %s" % ( | ||||
|                         approvedStr(ev["approved"]), | ||||
|                         ev["door"], | ||||
|                         ev["user"]["name"], | ||||
|                         ev("method"), | ||||
|                     ) | ||||
|                     slack_post(msg) | ||||
|  | ||||
|         except PyMongoError as e: | ||||
|             print(e) | ||||
|  | ||||
|  | ||||
| def authz_special(authzGroup, userGroups, user) -> Tuple[bool, str]: | ||||
|     if authzGroup not in userGroups: | ||||
|         return False, f"You are not in {authzGroup}. k-space.ee/membership" | ||||
|  | ||||
|     return True, user | ||||
|  | ||||
|  | ||||
| # -> approved, username | ||||
| # -> not approved, error message | ||||
| def slack_authz(user_id: str, channel_id: str, door: str) -> Tuple[bool, str]: | ||||
|     if door in ["alldoors", "backdoor", "frontdoor", "grounddoor"]: | ||||
|         if channel_id == SLACK_CHANNEL_ID: | ||||
|             return True | ||||
|  | ||||
|         groups, user = kube.by_slackid(user_id) | ||||
|         if "k-space:floor" not in groups: | ||||
|             return ( | ||||
|                 False, | ||||
|                 "No user with slack_id %s. Try in #members or doorboy.k-space.ee.", | ||||
|             ) | ||||
|  | ||||
|         return True, user | ||||
|  | ||||
|     groups, user = kube.by_slackid(user_id) | ||||
|     if user == "": | ||||
|         return False, "No user with slack_id %s. Try doorboy.k-space.ee." | ||||
|  | ||||
|     if door == "workshopdoor": | ||||
|         return authz_special("k-space:workshop", groups, user) | ||||
|  | ||||
|     return False, "Invalid door (git.k-space.ee/k-space/doorboy-proxy)" | ||||
|  | ||||
|  | ||||
| @slack_app.route("/slack-open", methods=["POST"]) | ||||
| async def slack_open(request): | ||||
|     if request.form.get("token") != SLACK_VERIFICATION_TOKEN: | ||||
|         return "Invalid token (are you Slack?)", 401 | ||||
|  | ||||
|     command = request.form.get("command") | ||||
|     door = command.removeprefix("/open-").replace("-", "") | ||||
|  | ||||
|     # user may be empty if authzed to SLACK_CHANNEL_ID | ||||
|     ok, userOrErrorMsg = slack_authz( | ||||
|         request.form.get("user_id"), | ||||
|         request.form.get("channel_id"), | ||||
|         door, | ||||
|     ) | ||||
|     if not ok: | ||||
|         return userOrErrorMsg, 403 | ||||
|  | ||||
|     doors = [door] | ||||
|     if door == "alldoors": | ||||
|         # outside non-special doors | ||||
|         doors = ["backdoor", "frontdoor", "grounddoor"] | ||||
|  | ||||
|     for d in doors: | ||||
|         await request.app.ctx.db.eventlog.insert_one( | ||||
|             { | ||||
|                 "component": "doorboy", | ||||
|                 "method": "slack", | ||||
|                 "timestamp": datetime.now(datetime.timezone.utc), | ||||
|                 "door": d, | ||||
|                 "approved": True, | ||||
|                 "user": { | ||||
|                     "id": userOrErrorMsg, | ||||
|                     "name": request.form.get("user_name"), | ||||
|                 }, | ||||
|             } | ||||
|         ) | ||||
|  | ||||
|     return f"Opening {door}…" | ||||
| @@ -1,5 +1,3 @@ | ||||
| version: '3.7' | ||||
|  | ||||
| services: | ||||
|   mongoexpress: | ||||
|     image: mongo-express | ||||
| @@ -22,15 +20,15 @@ services: | ||||
|       driver: none | ||||
|  | ||||
|   doorboy_proxy: | ||||
|     network_mode: host | ||||
|     environment: | ||||
|       INVENTORY_API_KEY: "sptWL6XFxl4b8" | ||||
|       OIDC_USERS_NAMESPACE: passmower | ||||
|       DOORBOY_SECRET_FLOOR: "0123456789" | ||||
|       DOORBOY_SECRET_WORKSHOP: "9999999999" | ||||
|       DOORBOY_SECRET_OPEN_EVENTS: "1111111111" | ||||
|       FLOOR_ACCESS_GROUP: "k-space:floor" | ||||
|       WORKSHOP_ACCESS_GROUP: "k-space:workshop" | ||||
|       CARD_URI: "https://inventory-app-72zn4.codemowers.ee/cards" | ||||
|       SWIPE_URI: "https://inventory-app-72zn4.codemowers.ee/m/doorboy/swipe" | ||||
|       SLACK_VERIFICATION_TOKEN: DEV | ||||
|       SLACK_DOORLOG_CALLBACK: DEV | ||||
|       SLACK_CHANNEL_ID: CDL9H8Q9W | ||||
|     env_file: .env | ||||
|     ports: | ||||
|      - "5000:5000" | ||||
|     build: | ||||
|       context: . | ||||
|   | ||||
							
								
								
									
										7
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								requirements.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,7 @@ | ||||
| motor==3.7.1 | ||||
| pymongo==4.14.0 | ||||
| python_dateutil==2.9.0 | ||||
| Requests==2.32.4 | ||||
| sanic==25.3.0 | ||||
| sanic_prometheus==0.2.1 | ||||
| kubernetes==33.1.0 | ||||
		Reference in New Issue
	
	Block a user