From c31dd5875c305203176dab1decdd35e9dabf2d5e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 9 Sep 2026 12:47:50 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20git=20identity=20=E3=82=92=20Web=20UI?= =?UTF-8?q?=20=E3=81=8B=E3=82=89=E7=99=BB=E9=8C=B2=E3=83=BB=E5=89=8A?= =?UTF-8?q?=E9=99=A4=E3=81=A7=E3=81=8D=E3=82=8B=E3=82=88=E3=81=86=E3=81=AB?= =?UTF-8?q?=E3=81=99=E3=82=8B=20(v2.1.263-f)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit helm --set-file ではなく UI から SSH 秘密鍵 + known_hosts / HTTPS トークンを登録可能に。 - git.guiIdentities.enabled(既定 true)。UI に「認証情報」カード(一覧/追加/削除) - GET/POST/DELETE /api/identities。登録分はセッション NS の Secret (ラベル identity-source=gui / identity-type)として保存。Helm 管理分と同一覧に統合 - manager.py: resolve_identity / list_identities / create_gui_identity / delete_gui_identity。 build_pod と create_session は Helm 管理 + GUI 登録の両方を解決。 /api/config の identities を {name,type,source} 配列に変更 - rbac.yaml: guiIdentities.enabled 時のみ manager Role に同 NS secrets の CRUD を付与 - 秘密鍵はリクエストボディで manager へ届くため README に経路 TLS 必須を明記 - helm lint / template 検証済み --- CHANGELOG-2.1.263-f.md | 36 ++++++ CLAUDE.md | 20 +++ Chart.yaml | 2 +- README.md | 18 ++- files/manager.py | 250 ++++++++++++++++++++++++++++++++++++-- templates/deployment.yaml | 4 + templates/rbac.yaml | 8 ++ templates/secret-git.yaml | 2 + values.schema.json | 5 + values.yaml | 15 ++- 10 files changed, 345 insertions(+), 15 deletions(-) create mode 100644 CHANGELOG-2.1.263-f.md diff --git a/CHANGELOG-2.1.263-f.md b/CHANGELOG-2.1.263-f.md new file mode 100644 index 0000000..dfdfa78 --- /dev/null +++ b/CHANGELOG-2.1.263-f.md @@ -0,0 +1,36 @@ +# CHANGELOG 2.1.263-f — 手動リリース + +## 追加: GUI からの git identity 登録 + +Web UI の **「認証情報(identity)」カード** から、`helm` を触らずに SSH 秘密鍵 + +`known_hosts` / HTTPS トークンを登録・削除できるようにした。 + +- `git.guiIdentities.enabled`(既定 `true`) +- API: + - `GET /api/identities` … Helm 管理 + GUI 登録の identity 一覧(`{name, type, source}`) + - `POST /api/identities` … 登録(`{name, type, privateKey, knownHosts}` または + `{name, type, username, token}`)。`ssh` は `known_hosts` 必須、秘密鍵の簡易検証あり。 + Helm 管理と同名は拒否。既存 GUI 分は置き換え。 + - `DELETE /api/identities/` … 削除(`identity-source=gui` のみ。Helm 管理は不可) +- 保存先: セッション NS の Secret。ラベル + `claude-code.cafepieters.com/identity-source: gui|helm` / + `claude-code.cafepieters.com/identity-type: ssh|https-token` で区別。 +- `build_pod` / セッション作成のバリデーションは Helm 管理と GUI 登録の両方を解決する + (`resolve_identity` / `list_identities`)。 +- `/api/config` の `identities` は文字列配列 → `{name,type,source}` 配列に変更。UI も追従。 + +## RBAC + +`git.guiIdentities.enabled: true` のとき、manager Role にセッション NS の +`secrets` `["get","list","create","update","patch","delete"]` を追加(`false` なら付かない)。 +同 NS には Claude 認証情報 Secret も存在するため、不要なら無効化を推奨。 + +## セキュリティ注意 + +秘密鍵はブラウザ → gateway → manager をリクエストボディで通る。経路の TLS +(Ingress の `tls` / LB の TLS 終端)を有効にすること。README「git — 接続先と認証 +アイデンティティ」に明記。 + +## バージョン + +`version` = `2.1.263-f` / `appVersion` = `"2.1.263"`。 diff --git a/CLAUDE.md b/CLAUDE.md index f4bc610..096319d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,6 +57,26 @@ Git リポジトリ(GitHub / Gitea など互換 Git 全般)をチェック ## 改修履歴 +### GUI からの git identity 登録(2026-09-09, v2.1.263-f) + +**要望**: SSH 鍵を `helm --set-file` ではなく Web UI から登録したい。 + +**修正内容**: +- `git.guiIdentities.enabled`(既定 `true`)。UI に「認証情報」カード(一覧 + 追加フォーム + + 削除)。`GET/POST/DELETE /api/identities`。 +- 保存先はセッション NS の Secret(ラベル `identity-source: gui|helm` / + `identity-type: ssh|https-token`)。`secret-git.yaml` の Helm 管理分にも同ラベルを付与。 +- `manager.py`: `resolve_identity` / `list_identities` / `create_gui_identity` / + `delete_gui_identity` を追加。`build_pod` と `create_session` は Helm 管理 + GUI 登録の + 両方を解決。`/api/config` の `identities` を `{name,type,source}` 配列に変更(UI も追従)。 +- `rbac.yaml`: `guiIdentities.enabled` 時のみ manager Role に同 NS `secrets` の + get/list/create/update/patch/delete を追加。 +- 秘密鍵はリクエストボディで manager へ届く → README に「経路の TLS 必須」を明記。 + +**対象ファイル**: `files/manager.py`, `values.yaml`, `values.schema.json`, +`templates/rbac.yaml`, `templates/secret-git.yaml`, `templates/deployment.yaml`, +`README.md`, `Chart.yaml`, `CHANGELOG-2.1.263-f.md` + ### リポジトリ URL バリデーション緩和 + エラーメッセージ具体化(2026-09-09, v2.1.263-e) **症状**: `claude@192.168.9.65:...`(SSH ユーザーをアカウント名にした URL)が diff --git a/Chart.yaml b/Chart.yaml index 17ad3e4..077d4c4 100644 --- a/Chart.yaml +++ b/Chart.yaml @@ -6,7 +6,7 @@ description: >- 使い捨てコンテナで Git リポジトリ(GitHub / Gitea など)をチェックアウト・編集・ 検証・プッシュする。一定時間アイドルなセッションは自動破棄される。 type: application -version: 2.1.263-e +version: 2.1.263-f appVersion: "2.1.263" keywords: - claude-code diff --git a/README.md b/README.md index 41b49b9..8f7ae97 100644 --- a/README.md +++ b/README.md @@ -401,7 +401,8 @@ Secret は **セッション NS** に作成されます。`credentials` モー | `git.allowedHosts` | GitHub/GitLab/Gitea | clone を許可するホスト allow-list | | `git.allowArbitraryRepos` | `false` | allow-list を無効化し任意 URL を許可(非推奨) | | `git.defaultAuthorName` / `Email` | `Claude` / `claude@cafepieters.com` | コミット author 既定 | -| `git.identities..type` | — | `ssh` / `https-token` | +| `git.guiIdentities.enabled` | `true` | **Web UI から identity を登録・削除できるようにする**(下記) | +| `git.identities..type` | — | `ssh` / `https-token`(Helm 管理・UI では読み取り専用) | | `…ssh.sshPrivateKey` / `knownHosts` | — | PEM 秘密鍵 / `ssh-keyscan` 出力(**`knownHosts` 必須**) | | `…https-token.username` / `token` | `x-access-token` / — | HTTPS ベーシック(GitHub PAT は username 任意) | | `…existingSecret` ほか | — | 既存 Secret 参照(キー名は `existingSecretKey*` で調整) | @@ -409,6 +410,20 @@ Secret は **セッション NS** に作成されます。`credentials` モー GitHub でも Gitea でも GitLab でも、SSH か HTTPS トークンで認証できる Git ホストなら同じ仕組みで扱えます。 セッション作成時に identity 名を選ぶと、その Secret だけがそのセッション Pod にマウントされます。 +#### GUI からの identity 登録(`git.guiIdentities.enabled: true`) + +Web UI の **「認証情報(identity)」カード** から、SSH 秘密鍵 + `known_hosts`、または +HTTPS トークンを登録・削除できます。`helm` を触らずにセッションで使う鍵を追加できます。 + +- 登録内容はセッション NS の Secret(ラベル `claude-code.cafepieters.com/identity-source: gui`) + として保存され、`git.identities`(Helm 管理・`identity-source: helm`)と同じ一覧に並びます。 + Helm 管理のものは UI から削除できません。 +- **セキュリティ**: 秘密鍵はブラウザ → gateway → manager をリクエストボディで通ります。 + **経路の TLS(Ingress の `tls` / LB の TLS 終端)を必ず有効に**してください。 + 有効化すると manager にセッション NS の `secrets` への `get`/`list`/`create`/`update`/`delete` + 権限が付きます(同 NS には Claude 認証情報 Secret も存在)。不要なら `enabled: false`。 +- `known_hosts` は登録時必須です(`ssh-keyscan ` の出力を貼り付け。ホスト鍵 TOFU 無効化)。 + **URL の形式**(`git.identities` で選んだ鍵で認証されます): | 形式 | 例 | identity | @@ -540,6 +555,7 @@ docker buildx build --platform linux/amd64,linux/arm64 \ | バージョン | Claude Code | 変更内容 | |---|---|---| +| 2.1.263-f | 2.1.263 | **Web UI から git identity(SSH 秘密鍵 + known_hosts / HTTPS トークン)を登録・削除**できる「認証情報」カードを追加(`git.guiIdentities.enabled`、既定 `true`)。`GET/POST/DELETE /api/identities`。GUI 登録分はセッション NS の Secret(`identity-source: gui`)として保存し、Helm 管理分と同じ一覧に統合。manager RBAC に同 NS の `secrets` CRUD を条件付き付与 | | 2.1.263-e | 2.1.263 | リポジトリ URL バリデーションを緩和(`ssh://user@…` / `user@host:path` / `host:path` も受理。ホスト allowlist は従来どおり適用)。エラーメッセージに受理形式と「SSH ユーザーは常に `git`」の注意を明記。UI の入力欄にヒントを追加 | | 2.1.263-d | 2.1.263 | 失敗したセッションの調査性を改善。`session.reaper.terminatedRetentionSeconds`(既定 900)で終了 Pod を即削除せず保持。UI に **「ログ」ボタン**(`GET /api/sessions//log`)を追加し、`Failed` の理由(exitCode / reason)を一覧に表示。セッションブートスクリプトが URL スキームと identity 型の不一致を警告し、`git clone` の出力をそのままログに出すよう変更 | | 2.1.263-c | 2.1.263 | 修正: gateway の nginx が起動時 `pcre2_compile() failed: missing closing parenthesis` でクラッシュループ。`location ~` の正規表現に `{10}` が含まれ、nginx がブロック区切りの `{}` と解釈していた。正規表現全体をダブルクォートで囲んで解消 | diff --git a/files/manager.py b/files/manager.py index 0285fbd..6f6082d 100644 --- a/files/manager.py +++ b/files/manager.py @@ -56,7 +56,9 @@ BOOTSTRAP_CM = env("SESSION_BOOTSTRAP_CONFIGMAP", FULLNAME + "-session") ANTHROPIC_SECRET = env("ANTHROPIC_SECRET", FULLNAME + "-anthropic") ANTHROPIC_AUTH_MODE = env("ANTHROPIC_AUTH_MODE", "apiKey") ANTHROPIC_CREDENTIALS_KEY = env("ANTHROPIC_CREDENTIALS_KEY", ".credentials.json") -GIT_IDENTITIES = env_json("GIT_IDENTITIES_JSON", {}) +GIT_IDENTITIES = env_json("GIT_IDENTITIES_JSON", {}) # Helm 管理(読み取り専用) +GUI_IDENTITIES_ENABLED = env("GUI_IDENTITIES_ENABLED", "false").lower() == "true" +GIT_FULLNAME_PREFIX = env("GIT_FULLNAME_PREFIX", FULLNAME) GIT_ALLOWED_HOSTS = [h.strip().lower() for h in env("GIT_ALLOWED_HOSTS", "").split(",") if h.strip()] GIT_ALLOW_ARBITRARY = env("GIT_ALLOW_ARBITRARY", "false").lower() == "true" GIT_AUTHOR_NAME = env("GIT_DEFAULT_AUTHOR_NAME", "Claude") @@ -103,8 +105,13 @@ LABEL_COMPONENT = "app.kubernetes.io/component" LABEL_SID = "claude-code.cafepieters.com/session-id" ANN_PREFIX = "claude-code.cafepieters.com/" SESSION_SELECTOR = LABEL_COMPONENT + "=session," + LABEL_APP + "=claude-code" +LABEL_GIT_IDENT = "claude-code.cafepieters.com/git-identity" +LABEL_IDENT_SOURCE = "claude-code.cafepieters.com/identity-source" +LABEL_IDENT_TYPE = "claude-code.cafepieters.com/identity-type" SID_RE = re.compile(r"^[a-f0-9]{10}$") +# identity 名は Secret 名の一部になるため DNS ラベル相当に制限 +IDENT_NAME_RE = re.compile(r"^[a-z0-9]([a-z0-9-]{0,38}[a-z0-9])?$") # 受け入れる repo URL: # https://host[:port]/path # ssh://[user@]host[:port]/path @@ -173,6 +180,137 @@ def session_log(sid, tail=800): return 400, "bad session id" return k8s_text(core("/pods/claude-sess-" + sid + "/log?container=claude&tailLines=" + str(int(tail)))) +# -------------------------------------------------------------------------- +# git identity(Helm 管理 + GUI 登録) +# -------------------------------------------------------------------------- +def gui_identity_secret(name): + return GIT_FULLNAME_PREFIX + "-git-" + name + + +def list_gui_identities(): + """GUI から登録された identity(source=gui ラベルの Secret)の一覧。""" + if not GUI_IDENTITIES_ENABLED: + return [] + status, payload = k8s("GET", core("/secrets?labelSelector=" + + urllib.parse.quote(LABEL_IDENT_SOURCE + "=gui"))) + if status != 200: + log.error("list identity secrets failed: %s", status) + return [] + out = [] + for sec in payload.get("items", []): + lbl = sec.get("metadata", {}).get("labels", {}) + nm = lbl.get(LABEL_GIT_IDENT) + if nm: + out.append({"name": nm, "type": lbl.get(LABEL_IDENT_TYPE, "ssh"), "source": "gui"}) + return out + + +def list_identities(): + """UI 用: Helm 管理 + GUI 登録の identity 一覧。""" + out = [{"name": n, "type": (v or {}).get("type", "ssh"), "source": "helm"} + for n, v in GIT_IDENTITIES.items()] + names = {i["name"] for i in out} + for i in list_gui_identities(): + if i["name"] not in names: + out.append(i) + return sorted(out, key=lambda x: x["name"]) + + +def resolve_identity(name): + """build_pod 用にマウント情報を返す。無ければ None。""" + if not name: + return None + if name in GIT_IDENTITIES: + return GIT_IDENTITIES[name] + if not GUI_IDENTITIES_ENABLED: + return None + status, sec = k8s("GET", core("/secrets/" + gui_identity_secret(name))) + if status != 200: + return None + lbl = sec.get("metadata", {}).get("labels", {}) + if lbl.get(LABEL_IDENT_SOURCE) != "gui": + return None + typ = lbl.get(LABEL_IDENT_TYPE, "ssh") + data = sec.get("data", {}) or {} + if typ == "https-token": + user = "x-access-token" + if data.get("username"): + try: + user = base64.b64decode(data["username"]).decode("utf-8").strip() or user + except Exception: # noqa: BLE001 + pass + return {"type": "https-token", "secret": gui_identity_secret(name), + "keyToken": "token", "username": user} + return {"type": "ssh", "secret": gui_identity_secret(name), + "keyPrivateKey": "id", "keyKnownHosts": "known_hosts"} + + +def create_gui_identity(body): + if not GUI_IDENTITIES_ENABLED: + return 403, {"error": "GUI からの identity 登録は無効です(git.guiIdentities.enabled)"} + name = str(body.get("name", "")).strip() + typ = str(body.get("type", "")).strip() + if not IDENT_NAME_RE.match(name): + return 400, {"error": "identity 名は英小文字・数字・ハイフン(先頭末尾は英数字、40 文字以内)"} + if name in GIT_IDENTITIES: + return 409, {"error": "Helm 管理の identity と同名です: " + name} + if typ == "ssh": + key = str(body.get("privateKey", "")).strip() + kh = str(body.get("knownHosts", "")).strip() + if "PRIVATE KEY" not in key: + return 400, {"error": "privateKey が OpenSSH/PEM 秘密鍵ではありません"} + if not kh: + return 400, {"error": "knownHosts は必須です(`ssh-keyscan ` の出力)"} + string_data = {"id": key + ("\n" if not key.endswith("\n") else ""), "known_hosts": kh + "\n"} + elif typ == "https-token": + token = str(body.get("token", "")).strip() + if not token: + return 400, {"error": "token は必須です"} + string_data = {"username": str(body.get("username", "") or "x-access-token").strip(), + "token": token} + else: + return 400, {"error": "type は ssh か https-token"} + + manifest = { + "apiVersion": "v1", "kind": "Secret", "type": "Opaque", + "metadata": { + "name": gui_identity_secret(name), + "namespace": SESSIONS_NS, + "labels": { + LABEL_APP: "claude-code", + LABEL_GIT_IDENT: name, + LABEL_IDENT_SOURCE: "gui", + LABEL_IDENT_TYPE: typ, + }, + }, + "stringData": string_data, + } + # 既存なら置き換え(PUT)、無ければ作成(POST) + status, _ = k8s("GET", core("/secrets/" + gui_identity_secret(name))) + if status == 200: + st, payload = k8s("PUT", core("/secrets/" + gui_identity_secret(name)), manifest) + else: + st, payload = k8s("POST", core("/secrets"), manifest) + if st not in (200, 201): + return st, {"error": "Secret 保存に失敗: %s" % payload.get("message", st)} + log.info("gui identity %s (%s) saved", name, typ) + return 200, {"ok": True, "name": name, "type": typ} + + +def delete_gui_identity(name): + if not GUI_IDENTITIES_ENABLED: + return 403, {"error": "無効です"} + if not IDENT_NAME_RE.match(name): + return 400, {"error": "bad name"} + status, sec = k8s("GET", core("/secrets/" + gui_identity_secret(name))) + if status != 200: + return 404, {"error": "見つかりません"} + if sec.get("metadata", {}).get("labels", {}).get(LABEL_IDENT_SOURCE) != "gui": + return 403, {"error": "Helm 管理の identity は GUI から削除できません"} + k8s("DELETE", core("/secrets/" + gui_identity_secret(name))) + log.info("gui identity %s deleted", name) + return 200, {"ok": True} + # -------------------------------------------------------------------------- # セッション操作 # -------------------------------------------------------------------------- @@ -273,7 +411,7 @@ def build_pod(sid, repo, ref, identity_name, owner): {"name": "bootstrap", "mountPath": "/bootstrap", "readOnly": True}, ] - ident = GIT_IDENTITIES.get(identity_name) + ident = resolve_identity(identity_name) if ident: env_list.append({"name": "GIT_IDENTITY_TYPE", "value": ident.get("type", "")}) if ident.get("type") == "ssh": @@ -425,7 +563,7 @@ def create_session(repo, ref, identity_name, owner): if not GIT_ALLOW_ARBITRARY and host not in GIT_ALLOWED_HOSTS: return None, ("許可されていない Git ホストです: %s(許可済み: %s)" % (host, ", ".join(GIT_ALLOWED_HOSTS) or "なし")) - if identity_name and identity_name not in GIT_IDENTITIES: + if identity_name and resolve_identity(identity_name) is None: return None, "未知の git identity: " + identity_name if active_session_count() >= MAX_SESSIONS: return None, "同時セッション数の上限(%d)に達しています" % MAX_SESSIONS @@ -586,6 +724,43 @@ pre#logbox{white-space:pre-wrap;background:#111;color:#eee;padding:.8rem;border- +
セッション一覧 @@ -607,17 +782,55 @@ pre#logbox{white-space:pre-wrap;background:#111;color:#eee;padding:.8rem;border- const $=s=>document.querySelector(s); let identities=[]; function api(path,opts){opts=opts||{};opts.credentials='same-origin';return fetch(path,opts);} +let guiIdents=false; async function boot(){ const r=await api('/api/config'); if(r.status===401){$('#login').hidden=false;return;} const c=await r.json(); - identities=c.identities||[]; + guiIdents=!!c.guiIdentities; $('#idle').textContent=c.idleTimeoutSeconds; - const sel=$('#ident'); - sel.innerHTML=''+identities.map(i=>``).join(''); $('#app').hidden=false; + $('#identcard').hidden=!guiIdents; + applyIdents(c.identities||[]); + if(guiIdents) loadIdents(); refresh(); } +function applyIdents(list){ + identities=list; + $('#ident').innerHTML=''+ + list.map(i=>``).join(''); +} +function identFields(){ + const ssh=$('#idt').value==='ssh'; + $('#f-ssh').hidden=!ssh;$('#f-tok').hidden=ssh; +} +async function loadIdents(){ + const r=await api('/api/identities');if(!r.ok)return; + const list=(await r.json()).identities||[]; + applyIdents(list); + $('#identrows').innerHTML=list.map(i=>` + ${esc(i.name)}${i.type} + ${i.source} + ${i.source==='gui' + ?`` + :'Helm 管理'}`).join('') + ||'未登録'; +} +async function addIdent(e){e.preventDefault(); + const t=$('#idt').value; + const body={name:$('#idn').value.trim(),type:t}; + if(t==='ssh'){body.privateKey=$('#idkey').value;body.knownHosts=$('#idkh').value;} + else{body.username=$('#idu').value.trim();body.token=$('#idtk').value;} + const r=await api('/api/identities',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(body)}); + const j=await r.json(); + if(!r.ok){alert(j.error||'保存に失敗しました');return false;} + $('#idn').value='';$('#idkey').value='';$('#idkh').value='';$('#idtk').value=''; + loadIdents();return false; +} +async function delIdent(name){if(!confirm('identity '+name+' を削除しますか?'))return; + const r=await api('/api/identities/'+encodeURIComponent(name),{method:'DELETE'}); + if(!r.ok){const j=await r.json();alert(j.error||'削除に失敗しました');} + loadIdents();} async function doLogin(e){e.preventDefault(); const r=await api('/login',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:$('#tk').value})}); if(r.ok){location.reload();}else{alert('トークンが違います');} @@ -722,12 +935,17 @@ class Handler(BaseHTTPRequestHandler): if not token_ok(self): return self._json(401, {"error": "unauthorized"}) return self._json(200, { - "identities": sorted(GIT_IDENTITIES.keys()), + "identities": list_identities(), + "guiIdentities": GUI_IDENTITIES_ENABLED, "idleTimeoutSeconds": IDLE_TIMEOUT, "maxLifetimeSeconds": MAX_LIFETIME, "maxSessions": MAX_SESSIONS, "authMode": AUTH_MODE, }) + if path == "/api/identities": + if not token_ok(self): + return self._json(401, {"error": "unauthorized"}) + return self._json(200, {"identities": list_identities()}) if path == "/api/sessions": if not token_ok(self): return self._json(401, {"error": "unauthorized"}) @@ -761,6 +979,9 @@ class Handler(BaseHTTPRequestHandler): if err: return self._json(400, {"error": err}) return self._json(201, {"id": sid, "url": "/s/" + sid + "/"}) + if path == "/api/identities": + code, payload = create_gui_identity(self._body()) + return self._json(code, payload) m = re.match(r"^/api/sessions/([a-f0-9]{10})/touch$", path) if m: touch_session(m.group(1), min_interval=0) @@ -770,11 +991,16 @@ class Handler(BaseHTTPRequestHandler): def do_DELETE(self): if not token_ok(self): return self._json(401, {"error": "unauthorized"}) - m = re.match(r"^/api/sessions/([a-f0-9]{10})$", urllib.parse.urlparse(self.path).path) - if not m: - return self._send(404, b'{"error":"not found"}') - delete_session(m.group(1)) - return self._json(200, {"ok": True}) + path = urllib.parse.urlparse(self.path).path + m = re.match(r"^/api/sessions/([a-f0-9]{10})$", path) + if m: + delete_session(m.group(1)) + return self._json(200, {"ok": True}) + m = re.match(r"^/api/identities/([a-z0-9][a-z0-9-]{0,39})$", path) + if m: + code, payload = delete_gui_identity(m.group(1)) + return self._json(code, payload) + return self._send(404, b'{"error":"not found"}') def main(): diff --git a/templates/deployment.yaml b/templates/deployment.yaml index 1eb93c5..4ff8175 100644 --- a/templates/deployment.yaml +++ b/templates/deployment.yaml @@ -137,6 +137,10 @@ spec: value: {{ .Values.anthropic.existingSecretKeyCredentials | default ".credentials.json" | quote }} - name: GIT_IDENTITIES_JSON value: {{ $idents | toJson | quote }} + - name: GUI_IDENTITIES_ENABLED + value: {{ .Values.git.guiIdentities.enabled | quote }} + - name: GIT_FULLNAME_PREFIX + value: {{ include "claude-code.fullname" . | quote }} - name: GIT_ALLOWED_HOSTS value: {{ join "," .Values.git.allowedHosts | quote }} - name: GIT_ALLOW_ARBITRARY diff --git a/templates/rbac.yaml b/templates/rbac.yaml index 17fa0a6..6e5f3d0 100644 --- a/templates/rbac.yaml +++ b/templates/rbac.yaml @@ -22,6 +22,14 @@ rules: - apiGroups: [""] resources: ["events"] verbs: ["get", "list", "watch"] + {{- if .Values.git.guiIdentities.enabled }} + # GUI からの git identity 登録(Secret の作成/更新/削除)。 + # list は同 NS の他 Secret も対象になりうるが、manager コードは + # ラベル claude-code.cafepieters.com/identity-source=gui のものだけを操作する。 + - apiGroups: [""] + resources: ["secrets"] + verbs: ["get", "list", "create", "update", "patch", "delete"] + {{- end }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding diff --git a/templates/secret-git.yaml b/templates/secret-git.yaml index b13e4a9..d913b81 100644 --- a/templates/secret-git.yaml +++ b/templates/secret-git.yaml @@ -12,6 +12,8 @@ metadata: labels: {{- include "claude-code.labels" $ctx | nindent 4 }} claude-code.cafepieters.com/git-identity: {{ $name | quote }} + claude-code.cafepieters.com/identity-source: helm + claude-code.cafepieters.com/identity-type: {{ $id.type }} type: Opaque stringData: {{- if eq $id.type "ssh" }} diff --git a/values.schema.json b/values.schema.json index 118a84e..517cc94 100644 --- a/values.schema.json +++ b/values.schema.json @@ -141,6 +141,11 @@ "allowArbitraryRepos": { "type": "boolean" }, "defaultAuthorName": { "type": "string" }, "defaultAuthorEmail": { "type": "string" }, + "guiIdentities": { + "type": "object", + "additionalProperties": false, + "properties": { "enabled": { "type": "boolean" } } + }, "identities": { "type": "object", "additionalProperties": { diff --git a/values.yaml b/values.yaml index 0cfdb91..55e9f4c 100644 --- a/values.yaml +++ b/values.yaml @@ -216,8 +216,21 @@ git: defaultAuthorName: "Claude" defaultAuthorEmail: "claude@cafepieters.com" - # 認証アイデンティティ。セッション作成時に名前で選択する。 + # GUI(Web UI)からの認証情報 identity 登録を許可する。 + # 有効にすると、UI の「認証情報」カードから SSH 秘密鍵 + known_hosts / HTTPS トークンを + # 登録・削除できる。登録内容はセッション NS の Secret として保存される。 + # トレードオフ: + # - manager にセッション NS の secrets への get/list/create/update/delete 権限が付く + # (同 NS には Claude 認証情報 Secret も存在する点に留意)。 + # - 秘密鍵はブラウザ→gateway→manager をリクエストボディで通る。経路の TLS + # (Ingress の tls / LB の TLS 終端)を必ず有効にすること。 + # 下の git.identities(Helm 管理)は常に読み取り専用で UI に表示される。 + guiIdentities: + enabled: true + + # 認証アイデンティティ(Helm 管理)。セッション作成時に名前で選択する。 # GitHub / Gitea / GitLab など SSH または HTTPS トークンで認証する任意のホストに対応。 + # GUI から登録したものと合わせて使える(同名は不可)。 identities: {} # gitea-ssh: # type: ssh # ssh | https-token