Files
doorboy-proxy/app/slack.py
rasmus c5d4f603e2 fix slack-kube auth
1. reorder slack auth methods
2. refactor + fix kube slack lookup
2026-06-11 23:03:35 +03:00

136 lines
4.0 KiB
Python

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.doorlog.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_withgroup(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]:
# this mapping also duplicated to doorboy-proxy.py
authGroup = ""
match door:
case "alldoors" | "backdoor" | "frontdoor" | "grounddoor":
authGroup = "k-space:floor"
case "workshopdoor":
authGroup = "k-space:workshop"
case _:
return False, "Invalid door (git.k-space.ee/k-space/doorboy-proxy)"
groups, user = kube.by_slackid(user_id)
if user is None:
if authGroup == "k-space:floor":
if channel_id == SLACK_CHANNEL_ID:
return True, "🖕 #members user {user_id}"
return False, f"No user with slack_id {user_id}. Try in #members or doorboy.k-space.ee.",
else:
return False, f"No user with slack_id {user_id}. Try doorboy.k-space.ee."
return authz_withgroup(authGroup, groups, user)
@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 text(userOrErrorMsg)
doors = [door]
if door == "alldoors":
# outside non-special doors
doors = ["backdoor", "frontdoor", "grounddoor"]
for d in doors:
await request.app.ctx.db.doorlog.insert_one(
{
"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}")