import os from datetime import datetime, timezone from typing import Tuple import kube import requests from pymongo.errors import PyMongoError from requests.exceptions import RequestException from sanic import Blueprint from sanic.response import text 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, "Anonymous #members user 🖕" 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(timezone.utc), "door": d, "approved": True, "user": { "id": userOrErrorMsg, "name": request.form.get("user_name"), }, } ) return text(f"Opening {door}…")