#!/usr/bin/env python3 """Plane workspace webhooks, as the workspace admin kappiee (session login; the internal /api/workspaces//webhooks/ endpoint needs admin). Never prints secrets: a new or regenerated secret goes straight into the Vault as PLANE_WEBHOOK_SECRET (then restart plane-agentd-webhook). Run under agent-access.py --config exec --secret-env PLANE_KAPPIEE_PASSWORD=PLANE_KAPPIEE_PASSWORD -- python3 -I tools/plane-webhook.py ... usage: list | create URL (inactive; work item + comment events) | patch ID key=value... (e.g. is_active=true) | regenerate ID | delete ID | logs ID [n]""" import http.cookiejar, json, os, ssl, sys, urllib.parse, urllib.request BASE = "https://plane.kappiee.com" WS = "kappiee" jar = http.cookiejar.CookieJar() op = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(jar)) HDR = {"Referer": BASE + "/", "Origin": BASE, "User-Agent": "plane-agentd-admin"} def csrf(): return next((c.value for c in jar if c.name == "csrftoken"), "") def call(method, path, body=None, form=None): h = dict(HDR) data = None if form is not None: data = urllib.parse.urlencode(form).encode() h["Content-Type"] = "application/x-www-form-urlencoded" elif body is not None: data = json.dumps(body).encode() h["Content-Type"] = "application/json" if method != "GET": h["X-CSRFToken"] = csrf() req = urllib.request.Request(BASE + path, data=data, headers=h, method=method) try: with op.open(req, timeout=30) as r: raw = r.read() return r.status, raw, r.geturl() except urllib.error.HTTPError as e: return e.code, e.read(), path def login(): st, raw, _ = call("GET", "/auth/get-csrf-token/") tok = json.loads(raw).get("csrf_token") or csrf() st, raw, url = call("POST", "/auth/sign-in/", form={"csrfmiddlewaretoken": tok, "email": "kappiee@kappiee.com", "password": os.environ["PLANE_KAPPIEE_PASSWORD"]}) ok = any(c.name.startswith("session") for c in jar) print("login", st, "session cookie" if ok else "NO SESSION", urllib.parse.urlparse(url).path) if not ok: sys.exit(1) def redact(d): return {k: ("<%d chars>" % len(v) if k == "secret_key" and v else v) for k, v in d.items()} def vault_store(name, value): ident = json.load(urllib.request.urlopen("https://nas.tail9531b.ts.net:8443/identity/agent.json", timeout=30)) v = ident["vault"] ctx = ssl.create_default_context() ctx.check_hostname = False ctx.verify_mode = ssl.CERT_NONE req = urllib.request.Request(v["url"].rstrip("/") + "/v1/credentials", method="POST", data=json.dumps({"vault": "default", "credentials": {name: value}}).encode(), headers={"Authorization": "Bearer " + v["token"], "X-Vault": "default", "Content-Type": "application/json"}) with urllib.request.urlopen(req, context=ctx, timeout=30) as r: print("vault store", name, r.status) def store_secret(w): if w.get("secret_key"): vault_store("PLANE_WEBHOOK_SECRET", w["secret_key"]) login() P = "/api/workspaces/%s/webhooks/" % WS cmd = sys.argv[1] if cmd == "list": st, raw, _ = call("GET", P) print("list", st) if st == 200: for w in json.loads(raw): print(json.dumps(redact(w))) else: print(raw[:300]) elif cmd == "create": body = {"url": sys.argv[2], "is_active": False, "project": False, "issue": True, "module": False, "cycle": False, "issue_comment": True} st, raw, _ = call("POST", P, body) print("create", st) if st in (200, 201): w = json.loads(raw) store_secret(w) print(json.dumps(redact(w))) else: print(raw[:500]) elif cmd == "patch": body = {} for kv in sys.argv[3:]: k, v = kv.split("=", 1) body[k] = {"true": True, "false": False}.get(v, v) st, raw, _ = call("PATCH", P + sys.argv[2] + "/", body) print("patch", st, json.dumps(redact(json.loads(raw))) if st == 200 else raw[:500]) elif cmd == "regenerate": st, raw, _ = call("POST", P + sys.argv[2] + "/regenerate/", {}) print("regenerate", st) if st in (200, 201): w = json.loads(raw) store_secret(w) print(json.dumps(redact(w))) else: print(raw[:500]) elif cmd == "delete": st, raw, _ = call("DELETE", P + sys.argv[2] + "/") print("delete", st, raw[:200]) elif cmd == "logs": st, raw, _ = call("GET", "/api/workspaces/%s/webhook-logs/%s/" % (WS, sys.argv[2])) print("logs", st) if st == 200: for l in json.loads(raw)[: int(sys.argv[3]) if len(sys.argv) > 3 else 10]: print(l.get("created_at"), l.get("event_type"), l.get("request_method"), l.get("response_status"), (l.get("response_body") or "")[:120], l.get("retry_count"))