#!/usr/bin/env python3 """「한 대가 두 줄이 되는」 순간을 직접 재현합니다 (운영 매뉴얼 M4). 🔴 **이것만 실제로 지웁니다.** 시험용 단말로만 하십시오 — 사무실에서 쓰는 PC 로 하면 그 사람이 다시 로그인해야 합니다. 기본은 미리보기이고, `--apply` 를 붙여야 지웁니다. export CF_API_TOKEN=<토큰> # 권한: Zero Trust Read + Write export CF_ACCOUNT_ID=<계정 ID> python demo.py you@company.com # ① 미리보기 — 무엇을 지울지만 python demo.py you@company.com --apply # ② 실제로 지우고 재인증을 기다립니다 무엇을 보나 — 등록을 지우고 그 단말이 다시 인증하면, Cloudflare 는 기존 기기 줄에 등록을 다시 붙이지 않고 **기기 줄을 새로 만듭니다.** 물리적으로 같은 장비인데 목록에 두 줄이 되고, 옛 줄이 「등록 0개」로 남아 화면에서 숨겨집니다. 그래서 「지웠는데 왜 안 줄지?」로 보입니다. 공식 문서 등록 목록 https://developers.cloudflare.com/api/resources/zero_trust/subresources/devices/subresources/registrations/methods/list/ 등록 삭제 https://developers.cloudflare.com/api/resources/zero_trust/subresources/devices/subresources/registrations/methods/delete/ 개념 https://developers.cloudflare.com/cloudflare-one/team-and-resources/devices/device-registration/ """ import json, os, sys, time, urllib.parse, urllib.request API = "https://api.cloudflare.com/client/v4" TOKEN = os.environ.get("CF_API_TOKEN", "") ACCOUNT = os.environ.get("CF_ACCOUNT_ID", "") if not TOKEN or not ACCOUNT: sys.exit("CF_API_TOKEN 과 CF_ACCOUNT_ID 를 환경변수로 넣어 주세요.") args = [a for a in sys.argv[1:] if not a.startswith("--")] APPLY = "--apply" in sys.argv if not args: sys.exit("누구의 단말인지 지정해야 합니다: python demo.py you@company.com\n" "아무 등록이나 지우게 두지 않습니다 — 그것이 이 코드의 유일한 안전장치입니다.") WANT = args[0].strip().lower() WAIT = 180 def call(method, path, params=None): url = API + "/accounts/" + ACCOUNT + path + ("?" + urllib.parse.urlencode(params) if params else "") req = urllib.request.Request(url, method=method) req.add_header("Authorization", "Bearer " + TOKEN) with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read().decode("utf-8")) def get_all(path, params=None): out, cursor = [], "" while True: q = dict(params or {}) q["per_page"] = "100" if cursor: q["cursor"] = cursor j = call("GET", path, q) out.extend(j.get("result") or []) nxt = (j.get("result_info") or {}).get("cursor") or "" if not nxt or nxt == cursor or not j.get("result"): return out cursor = nxt def email_of(reg): for k in ("user", "last_seen_user"): v = (reg.get(k) or {}).get("email") if v: return v.lower() return "" # ── ① 지금 줄 수 ──────────────────────────────────────────────── before_devices = get_all("/devices/physical-devices", {"active_registrations": "include"}) # status=all 로 받되 **취소(revoke)된 줄은 표적에서 뺍니다.** 이미 죽은 줄이라 # 지워도 재인증이 일어나지 않아 M4 가 재현되지 않고, 「왜 안 늘지?」로 헛다리를 짚게 됩니다. before_regs = [r for r in get_all("/devices/registrations", {"status": "all"}) if email_of(r) == WANT and not r.get("revoked_at")] print("① 지금 기기 줄: %d개 (active_registrations=include 로 세었습니다)" % len(before_devices)) print(" 그중 %s 의 등록: %d건" % (WANT, len(before_regs))) if len(before_regs) != 1: print("\n② 표적을 하나로 좁혀야 합니다 — 지금 %d건입니다:" % len(before_regs)) for r in before_regs: print(" %s %s" % (r.get("id"), r.get("name") or "-")) sys.exit(0 if before_regs else 1) target = before_regs[0] print("\n② 표적: %s %s" % (target.get("id"), target.get("name") or "-")) print("\n③ 할 일 — 이 등록을 지우고, 단말이 스스로 재인증하기를 기다립니다.") print(" DELETE /devices/registrations/%s" % target.get("id")) print(" 그다음 ①을 다시 세면 **줄 수가 1 늘어 있습니다.** 같은 장비인데 목록에 두 줄입니다.") if not APPLY: print("\n미리보기였습니다. 실제로 해 보려면 --apply 를 붙이십시오 — 시험용 단말로만.") sys.exit(0) # ── ④ 지웁니다 ────────────────────────────────────────────────── call("DELETE", "/devices/registrations/" + str(target.get("id"))) print("\n④ 지웠습니다: %s" % target.get("id")) # ── ⑤ 기다립니다 ──────────────────────────────────────────────── print("⑤ 단말이 알아서 다시 인증하기를 기다립니다 (우리 랩에서 약 1분 30초, 최대 %d초)" % WAIT) print(" 그동안 그 단말에 아무것도 하지 마십시오 — 손대면 무엇 때문에 늘었는지 알 수 없습니다.") deadline = time.time() + WAIT old_ids = set(str(r.get("id")) for r in before_regs) fresh = None while time.time() < deadline: time.sleep(10) now = [r for r in get_all("/devices/registrations", {"status": "all"}) if email_of(r) == WANT] new = [r for r in now if str(r.get("id")) not in old_ids] if new: fresh = new[0] break print(" … 아직입니다 (남은 %d초)" % max(0, int(deadline - time.time()))) # ── ⑥ 다시 셉니다 ─────────────────────────────────────────────── after = get_all("/devices/physical-devices", {"active_registrations": "include"}) delta = len(after) - len(before_devices) print("\n⑥ 다시 세었습니다: %d → %d (%+d)" % (len(before_devices), len(after), delta)) if fresh: print(" 새 등록: %s %s" % (fresh.get("id"), fresh.get("name") or "-")) if delta > 0: print("\n🔴 **줄이 늘었습니다.** 같은 장비인데 목록에 두 줄입니다 — 이것이 M4 입니다.") print(" 옛 줄은 이제 「등록 0개」로 남아 있고, 그 줄은 아무 일도 하지 않습니다.") elif fresh: print("\n재인증은 됐는데 줄 수가 그대로입니다 — 옛 줄이 이미 정리됐거나 같은 줄에 붙은 것입니다.") else: print("\n아직 재인증이 안 됐습니다. 조금 뒤 다시 세어 보십시오.") print("\n⑦ 되돌리기 — 새로 생긴 줄은 그대로 두고 **옛 「등록 0개」 줄만** 지우면 원래 개수로 돌아옵니다.") print(" ⚠️ 새로 생긴 줄까지 지우면 그 단말이 또 재인증해서 또 늘어납니다. 그것이 이 실험의 교훈입니다.")