fix: validate Sub2API pool key through admin API
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
Pipelines as Code CI / hwlab-web-probe-sentinel-nc01- Success
This commit is contained in:
@@ -1070,15 +1070,77 @@ def generate_api_key():
|
||||
alphabet = string.ascii_letters + string.digits
|
||||
return "sk-unidesk-codex-" + "".join(secrets.choice(alphabet) for _ in range(48))
|
||||
|
||||
def sub2api_key_value(item):
|
||||
if not isinstance(item, dict):
|
||||
return None
|
||||
key = item.get("key")
|
||||
return key if isinstance(key, str) and key else None
|
||||
|
||||
def find_pool_key_by_name(keys):
|
||||
return next((item for item in keys if isinstance(item, dict) and item.get("name") == POOL_API_KEY_NAME), None)
|
||||
|
||||
def existing_sub2api_pool_api_key(token):
|
||||
try:
|
||||
existing = next((item for item in list_user_keys(token) if item.get("name") == POOL_API_KEY_NAME), None)
|
||||
existing = find_pool_key_by_name(list_user_keys(token))
|
||||
except Exception:
|
||||
return None
|
||||
if not isinstance(existing, dict):
|
||||
return None
|
||||
key = existing.get("key")
|
||||
return key if isinstance(key, str) and key else None
|
||||
return sub2api_key_value(existing)
|
||||
|
||||
def resolve_pool_api_key_for_validate(token):
|
||||
secret_value = None
|
||||
secret_error = None
|
||||
try:
|
||||
secret_value = decode_secret_value(POOL_API_KEY_SECRET_NAME, POOL_API_KEY_SECRET_KEY)
|
||||
except Exception as exc:
|
||||
if RUNTIME_MODE != "host-docker":
|
||||
raise
|
||||
secret_error = str(exc)
|
||||
keys = list_user_keys(token)
|
||||
matched_item = None
|
||||
if secret_value:
|
||||
matched_item = next((item for item in keys if isinstance(item, dict) and item.get("key") == secret_value), None)
|
||||
if isinstance(matched_item, dict) and matched_item.get("name") == POOL_API_KEY_NAME:
|
||||
return secret_value, matched_item, {
|
||||
"ok": True,
|
||||
"source": pool_api_key_secret_location(),
|
||||
"sourceAction": "kept-existing",
|
||||
"secretPresent": True,
|
||||
"lookup": "matched-by-value",
|
||||
"hostEnvAction": "none",
|
||||
"valuesPrinted": False,
|
||||
}
|
||||
if RUNTIME_MODE == "host-docker":
|
||||
named_item = find_pool_key_by_name(keys)
|
||||
named_key = sub2api_key_value(named_item)
|
||||
if named_key:
|
||||
host_env_action = write_host_env_value(POOL_API_KEY_SECRET_KEY, named_key)
|
||||
return named_key, named_item, {
|
||||
"ok": True,
|
||||
"source": "sub2api-admin-api",
|
||||
"sourceAction": "recovered-missing" if not secret_value else "repaired-mismatch",
|
||||
"secretPresent": bool(secret_value),
|
||||
"lookup": "matched-by-name",
|
||||
"hostEnvAction": host_env_action,
|
||||
"hostEnvPath": HOST_DOCKER_ENV_PATH,
|
||||
"hostEnvKey": POOL_API_KEY_SECRET_KEY,
|
||||
"valuesPrinted": False,
|
||||
}
|
||||
if not secret_value:
|
||||
detail = f"; envReadError={secret_error}" if secret_error else ""
|
||||
raise RuntimeError(f"{pool_api_key_secret_location()} missing and Sub2API admin API key named {POOL_API_KEY_NAME} did not return a recoverable key{detail}")
|
||||
matched_name = matched_item.get("name") if isinstance(matched_item, dict) else None
|
||||
raise RuntimeError(f"{pool_api_key_secret_location()} present but does not match pool key {POOL_API_KEY_NAME}; matchedName={matched_name or '-'}; admin API did not return a recoverable named key")
|
||||
if not secret_value:
|
||||
raise RuntimeError(f"{POOL_API_KEY_SECRET_NAME}.{POOL_API_KEY_SECRET_KEY} missing")
|
||||
return secret_value, matched_item, {
|
||||
"ok": isinstance(matched_item, dict) and matched_item.get("name") == POOL_API_KEY_NAME,
|
||||
"source": pool_api_key_secret_location(),
|
||||
"sourceAction": "kept-existing",
|
||||
"secretPresent": True,
|
||||
"lookup": "matched-by-value" if matched_item else "missing-in-admin-api",
|
||||
"hostEnvAction": "not-applicable",
|
||||
"valuesPrinted": False,
|
||||
}
|
||||
|
||||
def ensure_api_key_secret(group_id, token):
|
||||
existing = None
|
||||
@@ -2614,6 +2676,7 @@ def run_sync():
|
||||
"webSocketsV2": ws_v2_status,
|
||||
"tempUnschedulable": temp_unschedulable_status,
|
||||
"apiKey": {
|
||||
"ok": True,
|
||||
"name": POOL_API_KEY_NAME,
|
||||
"secret": pool_api_key_secret_location(),
|
||||
"secretAction": secret_action,
|
||||
@@ -2633,11 +2696,9 @@ def run_sync():
|
||||
}
|
||||
|
||||
def run_validate():
|
||||
api_key = decode_secret_value(POOL_API_KEY_SECRET_NAME, POOL_API_KEY_SECRET_KEY)
|
||||
if not api_key:
|
||||
raise RuntimeError(f"{POOL_API_KEY_SECRET_NAME}.{POOL_API_KEY_SECRET_KEY} missing")
|
||||
admin_email, token, admin_compliance = login()
|
||||
key_item = next((item for item in list_user_keys(token) if item.get("key") == api_key), None)
|
||||
api_key, key_item, api_key_source = resolve_pool_api_key_for_validate(token)
|
||||
api_key_ok = isinstance(key_item, dict) and key_item.get("name") == POOL_API_KEY_NAME
|
||||
owner_balance = None
|
||||
owner_concurrency = None
|
||||
if key_item is not None and key_item.get("user_id") is not None:
|
||||
@@ -2656,7 +2717,7 @@ def run_validate():
|
||||
runtime_capabilities = validate_runtime_capabilities(token)
|
||||
sentinel = sentinel_runtime_status()
|
||||
return {
|
||||
"ok": gateway["ok"] is True and responses_smoke["ok"] is True and (owner_concurrency is None or owner_concurrency["ok"] is True) and capacity_status["ok"] is True and load_factor_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True and manual_account_protections.get("ok") is True and sentinel.get("ok") is True and runtime_capabilities.get("ok") is True,
|
||||
"ok": api_key_ok is True and gateway["ok"] is True and responses_smoke["ok"] is True and (owner_concurrency is None or owner_concurrency["ok"] is True) and capacity_status["ok"] is True and load_factor_status["ok"] is True and ws_v2_status["ok"] is True and temp_unschedulable_status["ok"] is True and manual_account_protections.get("ok") is True and sentinel.get("ok") is True and runtime_capabilities.get("ok") is True,
|
||||
"degraded": bool(responses_smoke.get("degraded")) or bool(compact_evidence.get("degraded")) or bool(responses_evidence.get("degraded")) or runtime_capabilities.get("ok") is not True,
|
||||
"mode": "validate",
|
||||
"namespace": NAMESPACE,
|
||||
@@ -2664,7 +2725,10 @@ def run_validate():
|
||||
"appPod": APP_POD,
|
||||
"admin": {"email": admin_email, "tokenPrinted": False, "compliance": admin_compliance},
|
||||
"apiKey": {
|
||||
"ok": api_key_ok,
|
||||
"name": POOL_API_KEY_NAME,
|
||||
"secret": pool_api_key_secret_location(),
|
||||
"source": api_key_source,
|
||||
"sub2apiId": key_item.get("id") if isinstance(key_item, dict) else None,
|
||||
"userId": key_item.get("user_id") if isinstance(key_item, dict) else None,
|
||||
"groupId": key_item.get("group_id") if isinstance(key_item, dict) else None,
|
||||
|
||||
@@ -258,6 +258,7 @@ export function applyScript(
|
||||
appSourcePath: secretMaterial.appSourcePath,
|
||||
sourceAction: secretMaterial.action,
|
||||
fingerprint: secretMaterial.fingerprint,
|
||||
adminEmail: secretMaterial.adminEmail,
|
||||
valuesPrinted: false,
|
||||
}))})`;
|
||||
const exposureSecretFile = publicExposureSecretMaterial === null
|
||||
|
||||
@@ -287,6 +287,7 @@ export interface ExternalActiveSecretMaterial {
|
||||
appSourcePath: string;
|
||||
action: "create" | "update" | "none";
|
||||
fingerprint: string;
|
||||
adminEmail: string;
|
||||
values: Record<typeof requiredSecretKeys[number], string>;
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,7 @@ export function hostDockerEnvFile(sub2api: Sub2ApiConfig, target: Sub2ApiTargetC
|
||||
REDIS_POOL_SIZE: "32",
|
||||
REDIS_MIN_IDLE_CONNS: "2",
|
||||
REDIS_ENABLE_TLS: "false",
|
||||
ADMIN_EMAIL: "admin@sub2api.platform-infra.local",
|
||||
ADMIN_EMAIL: secretMaterial.adminEmail,
|
||||
ADMIN_PASSWORD: secretMaterial.values.ADMIN_PASSWORD,
|
||||
JWT_SECRET: secretMaterial.values.JWT_SECRET,
|
||||
TOTP_ENCRYPTION_KEY: secretMaterial.values.TOTP_ENCRYPTION_KEY,
|
||||
@@ -240,6 +240,7 @@ export function hostDockerApplyScript(sub2api: Sub2ApiConfig, target: Sub2ApiTar
|
||||
appSourcePath: secretMaterial.appSourcePath,
|
||||
sourceAction: secretMaterial.action,
|
||||
fingerprint: secretMaterial.fingerprint,
|
||||
adminEmail: secretMaterial.adminEmail,
|
||||
valuesPrinted: false,
|
||||
};
|
||||
return `
|
||||
|
||||
@@ -64,6 +64,7 @@ export function prepareExternalActiveSecret(sub2api: Sub2ApiConfig): ExternalAct
|
||||
const existing = existedBefore ? parseEnvFile(readFileSync(appSourcePath, "utf8")) : {};
|
||||
const next = { ...existing };
|
||||
next.POSTGRES_PASSWORD = dbPassword;
|
||||
if (next.ADMIN_EMAIL === undefined || next.ADMIN_EMAIL.length === 0) next.ADMIN_EMAIL = "admin@sub2api.platform-infra.local";
|
||||
if (next.ADMIN_PASSWORD === undefined || next.ADMIN_PASSWORD.length === 0) next.ADMIN_PASSWORD = randomBytes(16).toString("hex");
|
||||
if (next.JWT_SECRET === undefined || next.JWT_SECRET.length === 0) next.JWT_SECRET = randomBytes(32).toString("hex");
|
||||
if (next.TOTP_ENCRYPTION_KEY === undefined || next.TOTP_ENCRYPTION_KEY.length === 0) next.TOTP_ENCRYPTION_KEY = randomBytes(32).toString("hex");
|
||||
@@ -75,12 +76,13 @@ export function prepareExternalActiveSecret(sub2api: Sub2ApiConfig): ExternalAct
|
||||
};
|
||||
const missing = requiredSecretKeys.filter((key) => values[key].length === 0);
|
||||
if (missing.length > 0) throw new Error(`external-active app secret source is missing ${missing.join(", ")}`);
|
||||
const managedAppSourceKeys = [...requiredSecretKeys, "ADMIN_EMAIL"] as const;
|
||||
const action = !existedBefore
|
||||
? "create"
|
||||
: requiredSecretKeys.some((key) => existing[key] !== values[key])
|
||||
: managedAppSourceKeys.some((key) => existing[key] !== next[key])
|
||||
? "update"
|
||||
: "none";
|
||||
if (action !== "none") writeEnvFile(appSourcePath, { ...existing, ...values });
|
||||
if (action !== "none") writeEnvFile(appSourcePath, { ...existing, ...values, ADMIN_EMAIL: next.ADMIN_EMAIL });
|
||||
return {
|
||||
sourceRef: database.sourceRef,
|
||||
sourcePath: dbSource.sourcePathRedacted,
|
||||
@@ -88,6 +90,7 @@ export function prepareExternalActiveSecret(sub2api: Sub2ApiConfig): ExternalAct
|
||||
appSourcePath: redactRepoPath(appSourcePath),
|
||||
action,
|
||||
fingerprint: fingerprintSecretValues(values, [...requiredSecretKeys]),
|
||||
adminEmail: next.ADMIN_EMAIL,
|
||||
values,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user