Files
account-manager/scripts/service/account_add_commands.py
chendelian 71bef19514
All checks were successful
技能自动化发布 / release (push) Successful in 5s
chore: auto release commit (2026-06-19 11:19:10)
2026-06-19 11:19:10 +08:00

540 lines
18 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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.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()
log.info("account_add_web_success id=%s platform=%s profile_dir=%s", new_id, key, resolved_profile_dir)
_say(_ok_json({
"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,
}))
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": "",
}))