#!/usr/bin/env python3 """세션 시계 네 개를 한 화면에 모읍니다 (운영 매뉴얼 M1). 🟢 읽기만 합니다. 아무것도 바꾸지 않습니다. 표준 라이브러리만 씁니다 — 설치할 것이 없습니다. export CF_API_TOKEN=<읽기 전용 토큰> # 권한: Access Read export CF_ACCOUNT_ID=<계정 ID> python sessions.py 왜 모아 보나 — 값이 **네 곳에 흩어져** 있어서 화면으로는 한눈에 못 봅니다. 그래서 「전역을 15분으로 줄였는데 왜 하루 종일 안 묻지?」가 생깁니다. ① 단말(Cloudflare One Client) 세션 조직 설정 · 앱에서 켜 두면 나머지 셋을 이깁니다 ② 정책 세션 정책마다 · 비워 두면 앱 값을 그대로 씁니다 ③ 앱(애플리케이션) 세션 앱마다 ④ 전역 세션 조직 하나 🔴 **앱에서 「Authenticate with Cloudflare One Client」 를 켜 두면 ①이 이깁니다.** 이 표에서 그 앱은 「단말 세션 우선」으로 찍힙니다 — 그 앱의 ②③④ 는 그날 안 쓰일 수 있습니다. 🔴 **정책 세션이 비어 있으면 앱 값을 따릅니다.** 「②를 안 건드렸으니 ④가 듣겠지」가 아닙니다. 공식 문서 세션 관리 https://developers.cloudflare.com/cloudflare-one/access-controls/access-settings/session-management/ 조직 조회 https://developers.cloudflare.com/api/resources/zero_trust/subresources/organizations/methods/list/ 앱 목록 https://developers.cloudflare.com/api/resources/zero_trust/subresources/access/subresources/applications/methods/list/ 정책 목록 https://developers.cloudflare.com/api/resources/zero_trust/subresources/access/subresources/policies/methods/list/ """ import json import os import sys import urllib.error import urllib.parse import 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 를 환경변수로 넣어 주세요.") def call(path, params=None): url = API + "/accounts/" + ACCOUNT + path if params: url += "?" + urllib.parse.urlencode(params) req = urllib.request.Request(url) req.add_header("Authorization", "Bearer " + TOKEN) try: with urllib.request.urlopen(req, timeout=30) as r: return json.loads(r.read().decode("utf-8")) except urllib.error.HTTPError as e: raw = e.read().decode("utf-8", "replace") try: msg = json.dumps(json.loads(raw).get("errors"), ensure_ascii=False) except ValueError: msg = raw[:200] sys.exit("GET %s → HTTP %d %s" % (path, e.code, msg)) def get_all(path, params=None): out, cursor = [], "" for _ in range(50): q = dict(params or {}) q["per_page"] = "100" if cursor: q["cursor"] = cursor j = call(path, q) batch = j.get("result") or [] out.extend(batch) nxt = (j.get("result_info") or {}).get("cursor") or "" if not nxt or nxt == cursor or not batch: break cursor = nxt return out org = (call("/access/organizations") or {}).get("result") or {} print("④ 전역 세션 %s" % (org.get("session_duration") or "(응답에 없음 — 기본 24h)")) print("① 단말(WARP) 세션 %s" % (org.get("warp_auth_session_duration") or "(응답에 없음 — 화면에서 확인하십시오)")) print(" 자동 리다이렉트 %s" % ("예" if org.get("auto_redirect_to_identity") else "아니오")) print(" 조직 MFA 강제 %s" % ("예" if (org.get("mfa_config") or {}).get("mfa_required_for_all_apps") else "아니오")) apps = get_all("/access/apps") print("\n앱 %d개 — ③앱 세션 · ②정책 세션 · 단말 세션 우선 여부" % len(apps)) print("%-34s %-12s %-14s %s" % ("앱", "앱 세션", "단말세션 우선", "정책 세션")) print("-" * 86) for a in apps: warp_first = bool(a.get("allow_authenticate_via_warp")) pol_txt = [] for p in (a.get("policies") or []): d = p.get("session_duration") pol_txt.append("%s=%s" % (str(p.get("name") or p.get("id"))[:14], d or "앱 값 따름")) if not pol_txt: # 앱 응답에 정책이 안 실리는 경우가 있어 따로 물어봅니다. try: for p in get_all("/access/apps/%s/policies" % a.get("id")): d = p.get("session_duration") pol_txt.append("%s=%s" % (str(p.get("name") or p.get("id"))[:14], d or "앱 값 따름")) except SystemExit: pol_txt.append("(정책을 못 읽었습니다 — 권한)") print("%-34s %-12s %-14s %s" % ( str(a.get("name") or a.get("id"))[:34], a.get("session_duration") or "기본 24h", "예 (①이 이김)" if warp_first else "아니오", " · ".join(pol_txt) or "-")) print("\n읽는 법") print(" · 🔴 비어 있는 값은 「여기서 설정하지 않았다」는 뜻이고, 0(즉시 만료)이 아닙니다.") print(" 공식 문서 기준 — ④전역 24h(15분~1개월) · ③앱 24h(지연 없이~1개월) · ②정책 = 앱 값.") print(" ①단말 세션은 문서에 기본값이 없어, 화면에서 확인하셔야 합니다.") print(" · 「단말세션 우선 = 예」인 앱은 ①이 나머지를 이깁니다. 규정 시간을 맞추려면 ①을 줄입니다.") print(" · 「정책 세션 = 앱 값 따름」이면 ②를 비워 둔 것입니다 — ③이 그대로 ②가 됩니다.") print(" · 「앱 세션 = 기본 24h」는 그 앱에 값을 안 넣었다는 뜻입니다. ④전역을 따르는 것이 아니라") print(" 앱 자신의 기본값 24h 가 적용됩니다 — 전역을 15분으로 줄여도 이 앱은 24시간입니다.") print(" · 2단계(MFA) 지속시간은 이 넷과 **별개의 시계**입니다(앱·정책의 MFA 설정에서 봅니다).")