Compare commits
	
		
			12 Commits
		
	
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 4e493069ab | |||
| f5cfb3454a | |||
| eeeb5ecace | |||
| e52a8af0b4 | |||
| 5afee284b7 | |||
| abffe7c594 | |||
| d6807e3f01 | |||
| eebfc9efe6 | |||
| ed7c3f0607 | |||
| 988b7e964e | |||
| 55ca235946 | |||
| 829da1f55f | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,3 @@ | |||||||
| .env | .env | ||||||
| .overnodebundle | .overnodebundle | ||||||
|  | .venv | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| FROM harbor.k-space.ee/k-space/microservice-base | FROM harbor.k-space.ee/k-space/microservice-base:latest | ||||||
| RUN pip3 install httpx |  | ||||||
| WORKDIR /app | WORKDIR /app | ||||||
| COPY app /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 | On kdoorpi override `KDOORPI_API_ALLOWED`, `KDOORPI_API_LONGPOLL` environment variables | ||||||
| to redirect requests to your dev instance. | 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 | #!/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 | 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 = Sanic(__name__) | ||||||
|  | app.register_blueprint(slack_app) | ||||||
| monitor(app).expose_endpoint() | 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"] | 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_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"] | MONGO_URI = os.environ["MONGO_URI"] | ||||||
| SWIPE_URI = os.environ["SWIPE_URI"] |  | ||||||
|  |  | ||||||
| assert len(DOORBOY_SECRET_FLOOR) >= 10 | assert len(DOORBOY_SECRET_FLOOR) >= 10 | ||||||
| assert len(DOORBOY_SECRET_WORKSHOP) >= 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 |     # 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() | ||||||
|  |  | ||||||
| @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: |     if key == DOORBOY_SECRET_FLOOR: | ||||||
|         groups.append(FLOOR_ACCESS_GROUP) |         users = kube.users_with_group("k-space:floor") | ||||||
|     if key == DOORBOY_SECRET_WORKSHOP: |     elif key == DOORBOY_SECRET_WORKSHOP: | ||||||
|         groups.append(WORKSHOP_ACCESS_GROUP) |         users = kube.users_with_group("k-space:workshop") | ||||||
|     if not groups: |     else: | ||||||
|         return "fail", 500 |         print("WARN: unknown door token in /allowed") | ||||||
|     async with httpx.AsyncClient() as client: |         return "unknown doorboy secret token", 403 | ||||||
|         r = await client.post(CARD_URI, json={ |  | ||||||
|             "groups": groups |     flt = { | ||||||
|         }, headers={ |         "token.uid_hash": {"$exists": True}, | ||||||
|             "Content-Type": "application/json", |         "inventory.owner.username": {"$in": users}, | ||||||
|             "Authorization": f"Basic {INVENTORY_API_KEY}" |     } | ||||||
|         }) |     prj = {"token.uid_hash": True} | ||||||
|     j = r.json() |     tokens = await request.app.ctx.db.inventory.find(flt, prj) | ||||||
|     allowed_uids = [] |  | ||||||
|     for obj in j: |     print(f"delegating {len(tokens)} doorkey tokens") | ||||||
|         allowed_uids.append({ |     return json({"allowed_uids": tokens}) | ||||||
|             "token": obj["token"] |  | ||||||
|         }) |  | ||||||
|     return json({"allowed_uids": allowed_uids}) |  | ||||||
|  |  | ||||||
| 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() | ||||||
|  |  | ||||||
| @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({ | # Only identified and successful events. **Endpoint not in use.** | ||||||
|         "component": "doorboy", | @app.route("/logs") | ||||||
|         "type": "open-door", | async def view_open_door_events(request): | ||||||
|         "$or": [ |     return text("not an admin"), 403 | ||||||
|             { "approved": True }, |  | ||||||
|             { "success": True }, |     results = ( | ||||||
|         ], |         await app.ctx.db.eventlog.find( | ||||||
|         "$or": [ |             { | ||||||
|             { "type": "open-door" }, |                 "component": "doorboy", | ||||||
|             { "event": "card-swiped" }, |                 "type": "open-door", | ||||||
|         ], |                 "approved": True, | ||||||
|         "door": { "$exists": True }, |                 "$or": [ | ||||||
|         "timestamp": { "$exists": True } |                     {"type": "open-door"}, | ||||||
|     }).sort("timestamp", -1).to_list(length=None) |                     {"event": "card-swiped"}, | ||||||
|  |                 ], | ||||||
|  |                 "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) |  | ||||||
| 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") |     response = await request.respond(content_type="text/event-stream") | ||||||
|     await response.send("data: response-generator-started\n\n") |     await response.send("data: response-generator-started\n\n") | ||||||
|     pipeline = [ |     pipeline = [ | ||||||
|         { |         { | ||||||
|             "$match": { |             "$match": { | ||||||
|                  "operationType": "insert", |                 "operationType": "insert", | ||||||
|              } |             } | ||||||
|         } |         } | ||||||
|     ] |     ] | ||||||
|     try: |     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") |             await response.send("data: watch-stream-opened\n\n") | ||||||
|             async for event in stream: |             async for event in stream: | ||||||
|                 if event["fullDocument"].get("type") == "open-door" and event["fullDocument"].get("approved", False): |                 ev = event["fullDocument"] | ||||||
|                     await response.send("data: %s\n\n" % |                 if ev["approved"] != "true": | ||||||
|                                         event["fullDocument"]["door"]) |                     continue | ||||||
|     except pymongo.errors.PyMongoError as e: |                 if ev["type"] == "card": | ||||||
|  |                     continue | ||||||
|  |  | ||||||
|  |                 response.send("data: %s\n\n" % ev["door"]) | ||||||
|  |     except PyMongoError as e: | ||||||
|         print(e) |         print(e) | ||||||
|         await response.send("data: response-generator-ended\n\n") |         await response.send("data: response-generator-ended\n\n") | ||||||
|         return |         return | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Door controller reporting a card swipe. No authn/z about the event (done offline on door controller), only logging. | ||||||
| @app.post("/swipe") | @app.post("/swipe") | ||||||
| async def forward_swipe(request): | @authenticate_door | ||||||
|  | async def swipe(request): | ||||||
|  |     # authorize | ||||||
|     key = request.headers.get("KEY") |     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 |     data = request.json | ||||||
|     doors = set() |     doors = set() | ||||||
|     if key == DOORBOY_SECRET_FLOOR: |     if key == DOORBOY_SECRET_FLOOR: | ||||||
| @@ -142,19 +172,54 @@ async def forward_swipe(request): | |||||||
|     if key == DOORBOY_SECRET_WORKSHOP: |     if key == DOORBOY_SECRET_WORKSHOP: | ||||||
|         doors.add("workshopdoor") |         doors.add("workshopdoor") | ||||||
|     if data.get("door") not in doors: |     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) |         return text("Not allowed", 403) | ||||||
|  |  | ||||||
|     async with httpx.AsyncClient() as client: |     timestamp = ( | ||||||
|         r = await client.post(SWIPE_URI, json=data, headers={ |         parse(data["timestamp"]) | ||||||
|             "Content-Type": "application/json", |         if data.get("timestamp") | ||||||
|             "Authorization": f"Basic {INVENTORY_API_KEY}" |         else datetime.now(datetime.timezone.utc) | ||||||
|         }) |     ) | ||||||
|     if r.status_code == 200: |  | ||||||
|         return text("ok") |     # Update token, create if unknown | ||||||
|     else: |     await request.app.ctx.db.inventory.update_one( | ||||||
|         return text("Failed", 500) |         {"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__": | 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: | services: | ||||||
|   mongoexpress: |   mongoexpress: | ||||||
|     image: mongo-express |     image: mongo-express | ||||||
| @@ -22,15 +20,15 @@ services: | |||||||
|       driver: none |       driver: none | ||||||
|  |  | ||||||
|   doorboy_proxy: |   doorboy_proxy: | ||||||
|     network_mode: host |  | ||||||
|     environment: |     environment: | ||||||
|       INVENTORY_API_KEY: "sptWL6XFxl4b8" |       OIDC_USERS_NAMESPACE: passmower | ||||||
|       DOORBOY_SECRET_FLOOR: "0123456789" |       DOORBOY_SECRET_FLOOR: "0123456789" | ||||||
|       DOORBOY_SECRET_WORKSHOP: "9999999999" |       DOORBOY_SECRET_WORKSHOP: "9999999999" | ||||||
|       DOORBOY_SECRET_OPEN_EVENTS: "1111111111" |       SLACK_VERIFICATION_TOKEN: DEV | ||||||
|       FLOOR_ACCESS_GROUP: "k-space:floor" |       SLACK_DOORLOG_CALLBACK: DEV | ||||||
|       WORKSHOP_ACCESS_GROUP: "k-space:workshop" |       SLACK_CHANNEL_ID: CDL9H8Q9W | ||||||
|       CARD_URI: "https://inventory-app-72zn4.codemowers.ee/cards" |     env_file: .env | ||||||
|       SWIPE_URI: "https://inventory-app-72zn4.codemowers.ee/m/doorboy/swipe" |     ports: | ||||||
|  |      - "5000:5000" | ||||||
|     build: |     build: | ||||||
|       context: . |       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