# -*- coding: utf-8 -*- """Account creation CLI commands (add-web, add-secret).""" import json import os import sys import time from typing import Optional from db.accounts_repo import ( get_account_by_id, insert_account, insert_credential, resolve_auth_strategy_for_platform, ) from db.auth_strategy import ( ALL_AUTH_STRATEGIES, AUTH_STRATEGY_DEVICE_BOUND, ) from db.connection import get_conn, init_db from db.credentials_repo import insert_credential_encrypted from service._svc_common import ( PlatformCallerMeta, _derive_session_persistent, _err_json, _insert_secret_holder_account, _ok_json, _resolve_or_auto_register, _resolve_or_fail, _say, _say_err, ) from service.crypto import CredentialDecryptError from service.master_key import MasterKeyMissingError, is_master_key_available from service.secret_store import ( mask_secret, make_windows_credential_target, write_secret, ) from util.logging_config import get_skill_logger from util.platforms import get_platform_spec, seed_platforms from util.profile_shortcut import create_profile_browser_shortcut from util.runtime_paths import get_default_profile_dir_for_web def cmd_account_add_web( platform_input: str, login_id: Optional[str], login_id_type: str, label: Optional[str], tenant_id: Optional[str], environment: str, role: str, provider_code: Optional[str], url: Optional[str], extra_json_str: Optional[str], profile_dir_override: Optional[str], *, auth_strategy: Optional[str] = None, session_persistent: Optional[int] = None, device_fingerprint: Optional[str] = None, platform_meta: Optional[PlatformCallerMeta] = None, ) -> None: """account add-web — create a web/RPA account record.""" log = get_skill_logger() log.info("account_add_web_attempt platform=%s login_id_type=%s env=%s role=%s", platform_input, login_id_type, environment, role) key = _resolve_or_auto_register(platform_input, platform_meta) if not key: return init_db() now = int(time.time()) conn = get_conn() try: seed_platforms(conn) platform_spec = get_platform_spec(key, conn=conn) finally: conn.close() safe_label = (label or "").strip() or f"{platform_spec.get('display_name', key)} {environment} {role}" extra = {} if extra_json_str: try: extra = json.loads(extra_json_str) except json.JSONDecodeError: _say(_err_json("ERROR:CLI_BAD_ARGS", "extra_json 不是合法的 JSON 字符串。")) return if profile_dir_override: resolved_profile_dir = os.path.abspath(profile_dir_override) else: resolved_profile_dir = get_default_profile_dir_for_web( platform_key=key, label=safe_label, login_id=login_id, domain=platform_spec.get("domain", "generic"), ) os.makedirs(resolved_profile_dir, exist_ok=True) resolved_url = (url or "").strip() or platform_spec.get("default_url", "") resolved_provider = provider_code or platform_spec.get("provider_code") or key resolved_extra = json.dumps(extra, ensure_ascii=False) conn = get_conn() try: seed_platforms(conn) if auth_strategy is None: resolved_auth = resolve_auth_strategy_for_platform(conn, key) else: if auth_strategy not in ALL_AUTH_STRATEGIES: _say(_err_json( "ERR_AUTH_STRATEGY_NOT_SUPPORTED", f"不支持的 auth_strategy:{auth_strategy}", )) return resolved_auth = auth_strategy if session_persistent is None: sp = _derive_session_persistent(resolved_auth) else: if session_persistent not in (0, 1): _say(_err_json( "ERR_INVALID_SESSION_PERSISTENT", "session_persistent 必须是 0 或 1。", )) return sp = session_persistent if device_fingerprint is not None and resolved_auth != AUTH_STRATEGY_DEVICE_BOUND: _say(_err_json( "ERR_DEVICE_FINGERPRINT_NOT_APPLICABLE", "device_fingerprint 仅在 auth_strategy=device_bound 时可设置。", )) return new_id = insert_account( conn=conn, platform_key=key, account_label=safe_label, login_id=login_id or None, login_id_type=login_id_type, tenant_id=tenant_id or None, environment=environment, role=role, provider_code=resolved_provider, profile_dir=resolved_profile_dir, url=resolved_url, extra_json=resolved_extra, now=now, auth_strategy=resolved_auth, session_persistent=sp, device_fingerprint=device_fingerprint, ) conn.commit() except Exception: conn.rollback() log.exception("account_add_web_failure") _say(_err_json("ERROR:DB_ERROR", "Database insert failed.")) return finally: conn.close() shortcut_path = create_profile_browser_shortcut(resolved_profile_dir, safe_label, log) log.info("account_add_web_success id=%s platform=%s profile_dir=%s", new_id, key, resolved_profile_dir) ok_payload = { "id": new_id, "platform_key": key, "account_label": safe_label, "profile_dir": resolved_profile_dir, "url": resolved_url, "environment": environment, "role": role, "tenant_id": tenant_id or "", "provider_code": resolved_provider, "status": "active", "session_status": "unknown", "auth_strategy": resolved_auth, "session_persistent": sp, "device_fingerprint": device_fingerprint, } if shortcut_path: ok_payload["profile_shortcut"] = shortcut_path _say(_ok_json(ok_payload)) def cmd_account_add_secret( platform_input: str, credential_type: str, label: Optional[str], secret_storage: str, secret_stdin: bool, secret_ref: Optional[str], account_id: Optional[int], environment: Optional[str], role: Optional[str], extra_json_str: Optional[str], ) -> None: """account add-secret — create a credential record with masked secret.""" log = get_skill_logger() log.info("account_add_secret_attempt platform=%s type=%s storage=%s", platform_input, credential_type, secret_storage) key = _resolve_or_fail(platform_input) if not key: return storage_n = secret_storage.strip().lower() if storage_n not in ("none", "env", "windows_credential", "local_encrypted"): _say(_err_json("ERROR:SECRET_STORAGE_UNSUPPORTED", f"不支持的存储方式:{secret_storage}。支持:none / env / windows_credential / local_encrypted")) return if storage_n == "none" and credential_type not in ("browser_profile", "note"): _say(_err_json("ERROR:SECRET_STORAGE_UNSUPPORTED", "storage=none 仅允许 credential_type=browser_profile 或 note。")) return if storage_n == "env" and not secret_ref: _say(_err_json("ERROR:SECRET_REF_MISSING", "env 存储时必须提供 --secret-ref 环境变量名。")) return init_db() conn = get_conn() try: seed_platforms(conn) platform_spec = get_platform_spec(key, conn=conn) finally: conn.close() now = int(time.time()) safe_label = (label or "").strip() or f"{platform_spec.get('display_name', key)} {credential_type}" if storage_n == "local_encrypted": if not is_master_key_available(): _say(_err_json( "ERR_MASTER_KEY_MISSING", "master key 未初始化。先跑 `account init-master-key`。", )) return if not secret_stdin: _say(_err_json( "ERR_LOCAL_ENCRYPTED_REQUIRES_STDIN", "local_encrypted 后端必须 --secret-stdin 输入密文,避免命令历史泄漏。", )) return if credential_type not in ( "password", "api_key", "api_token", "bearer_token", "refresh_token", "oauth_client", ): _say(_err_json( "ERR_LOCAL_ENCRYPTED_TYPE_NOT_ALLOWED", f"local_encrypted 不接受 credential_type={credential_type}(仅 secret 类)。", )) return if secret_ref: _say_err("[add-secret] WARNING: secret_ref ignored for local_encrypted (secret comes from stdin).") extra: dict = {} if extra_json_str: try: extra = json.loads(extra_json_str) except json.JSONDecodeError: extra = {} resolved_env = environment or "production" resolved_role = role or "api" extra["environment"] = resolved_env extra["role"] = resolved_role resolved_extra = json.dumps(extra, ensure_ascii=False) resolved_url = (platform_spec.get("default_url") or "").strip() or None resolved_provider = platform_spec.get("provider_code") or key plaintext = sys.stdin.readline().rstrip("\r\n") if not plaintext: _say(_err_json("ERR_LOCAL_ENCRYPTED_EMPTY_STDIN", "stdin 没读到密码内容。")) return secret_mask_out = mask_secret(plaintext) conn = get_conn() cred_id: Optional[int] = None holder_id: Optional[int] = None try: seed_platforms(conn) holder_id = account_id if holder_id is not None: acc = get_account_by_id(holder_id) if not acc: _say(_err_json( "ERROR:ACCOUNT_NOT_FOUND", f"account_id={holder_id} 不存在。", )) return if holder_id is None: holder_id = _insert_secret_holder_account( conn, platform_key=key, account_label=safe_label, environment=resolved_env, role=resolved_role, provider_code=resolved_provider, url=resolved_url, now=now, ) cred_id = insert_credential_encrypted( conn, account_id=holder_id, platform_key=key, credential_label=safe_label, credential_type=credential_type, plaintext=plaintext, expires_at=None, extra_json=resolved_extra, now=now, ) conn.commit() except MasterKeyMissingError as e: conn.rollback() _say(_err_json("ERR_MASTER_KEY_MISSING", str(e))) return except CredentialDecryptError as e: conn.rollback() _say(_err_json("ERR_CREDENTIAL_DECRYPT_FAILED", str(e))) return except Exception: conn.rollback() log.exception("account_add_secret_failure") _say(_err_json("ERROR:DB_ERROR", "Database insert failed.")) return finally: conn.close() log.info( "account_add_secret_encrypted_success cred_id=%s account_id=%s", cred_id, holder_id, ) _say(_ok_json({ "credential_id": cred_id, "account_id": holder_id, "platform_key": key, "credential_type": credential_type, "secret_storage": "local_encrypted", "secret_mask": secret_mask_out, })) return if storage_n == "env": secret_ref_value = secret_ref.strip() secret_mask_value = mask_secret(f"env:{secret_ref_value}") extra = {} if extra_json_str: try: extra = json.loads(extra_json_str) except json.JSONDecodeError: extra = {} resolved_env = environment or "production" resolved_role = role or "api" extra["environment"] = resolved_env extra["role"] = resolved_role extra_json_final = json.dumps(extra, ensure_ascii=False) resolved_url = (platform_spec.get("default_url") or "").strip() or None resolved_provider = platform_spec.get("provider_code") or key conn = get_conn() try: seed_platforms(conn) holder_id = account_id if holder_id is None: holder_id = _insert_secret_holder_account( conn, platform_key=key, account_label=safe_label, environment=resolved_env, role=resolved_role, provider_code=resolved_provider, url=resolved_url, now=now, ) cred_id = insert_credential( conn=conn, account_id=holder_id, platform_key=key, credential_label=safe_label, credential_type=credential_type, secret_storage=storage_n, secret_ref=secret_ref_value, secret_mask=secret_mask_value, expires_at=None, extra_json=extra_json_final, now=now, ) conn.commit() except Exception: conn.rollback() log.exception("account_add_secret_failure") _say(_err_json("ERROR:DB_ERROR", "Database insert failed.")) return finally: conn.close() log.info("account_add_secret_success id=%s platform=%s storage=env ref=%s", cred_id, key, secret_ref_value) _say(_ok_json({ "account_id": holder_id, "credential_id": cred_id, "platform_key": key, "credential_label": safe_label, "credential_type": credential_type, "secret_storage": storage_n, "secret_ref": secret_ref_value, "secret_mask": secret_mask_value, "warning": "env 存储方式:请确保环境变量在运行时可用。", })) return if storage_n == "windows_credential": if not secret_stdin: _say(_err_json("ERROR:CLI_BAD_ARGS", "windows_credential 需要 --secret-stdin 从 stdin 读取 secret。")) return secret_value = sys.stdin.readline().strip() if not secret_value: _say(_err_json("ERROR:CLI_BAD_ARGS", "stdin 中未读到 secret 值。")) return secret_mask_value = mask_secret(secret_value) cred_label_for_target = safe_label target = make_windows_credential_target(key, credential_type, cred_label_for_target) try: write_secret("windows_credential", target, secret_value) except (OSError, ValueError) as e: log.error("secret_write_failed storage=windows_credential error=%s", str(e)) _say(_err_json("ERROR:SECRET_WRITE_FAILED", f"写 Windows Credential Manager 失败:{e}")) return extra = {} if extra_json_str: try: extra = json.loads(extra_json_str) except json.JSONDecodeError: extra = {} resolved_env = environment or "production" resolved_role = role or "api" extra["environment"] = resolved_env extra["role"] = resolved_role extra_json_final = json.dumps(extra, ensure_ascii=False) resolved_url = (platform_spec.get("default_url") or "").strip() or None resolved_provider = platform_spec.get("provider_code") or key conn = get_conn() try: seed_platforms(conn) holder_id = account_id if holder_id is None: holder_id = _insert_secret_holder_account( conn, platform_key=key, account_label=safe_label, environment=resolved_env, role=resolved_role, provider_code=resolved_provider, url=resolved_url, now=now, ) cred_id = insert_credential( conn=conn, account_id=holder_id, platform_key=key, credential_label=safe_label, credential_type=credential_type, secret_storage=storage_n, secret_ref=target, secret_mask=secret_mask_value, expires_at=None, extra_json=extra_json_final, now=now, ) conn.commit() except Exception: conn.rollback() log.exception("account_add_secret_failure") _say(_err_json("ERROR:DB_ERROR", "Database insert failed.")) return finally: conn.close() log.info("account_add_secret_success id=%s platform=%s storage=windows_credential", cred_id, key) secret_value = "" _say(_ok_json({ "account_id": holder_id, "credential_id": cred_id, "platform_key": key, "credential_label": safe_label, "credential_type": credential_type, "secret_storage": storage_n, "secret_ref": target, "secret_mask": secret_mask_value, })) return conn = get_conn() try: seed_platforms(conn) cred_id = insert_credential( conn=conn, account_id=account_id, platform_key=key, credential_label=safe_label, credential_type=credential_type, secret_storage=storage_n, secret_ref=None, secret_mask="", expires_at=None, extra_json=extra_json_str or "{}", now=now, ) conn.commit() except Exception: conn.rollback() log.exception("account_add_secret_failure") _say(_err_json("ERROR:DB_ERROR", "Database insert failed.")) return finally: conn.close() log.info("account_add_secret_success id=%s platform=%s storage=none", cred_id, key) _say(_ok_json({ "account_id": account_id, "credential_id": cred_id, "platform_key": key, "credential_label": safe_label, "credential_type": credential_type, "secret_storage": storage_n, "secret_ref": "", "secret_mask": "", }))