validation.sh 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. # Validation helpers for interactive setup.
  2. validate_uri() {
  3. local uri="$1"
  4. local db_type="$2"
  5. if [[ -z "$uri" ]]; then
  6. return 1
  7. fi
  8. case "$db_type" in
  9. postgresql)
  10. [[ "$uri" =~ ^postgres(ql)?://.+ ]]
  11. return $?; ;;
  12. neo4j)
  13. [[ "$uri" =~ ^(neo4j(\+s|\+ssc)?|bolt)://.+ ]]
  14. return $?; ;;
  15. mongodb)
  16. [[ "$uri" =~ ^mongodb(\+srv)?://.+ ]]
  17. return $?; ;;
  18. redis)
  19. [[ "$uri" =~ ^rediss?://.+ ]]
  20. return $?; ;;
  21. milvus|qdrant)
  22. [[ "$uri" =~ ^https?://.+ ]]
  23. return $?; ;;
  24. memgraph)
  25. [[ "$uri" =~ ^bolt://.+ ]]
  26. return $?; ;;
  27. *)
  28. return 1
  29. ;;
  30. esac
  31. }
  32. validate_api_key() {
  33. local key="$1"
  34. local provider="$2"
  35. if [[ -z "$key" ]]; then
  36. return 1
  37. fi
  38. case "$provider" in
  39. openai|openrouter)
  40. return 0; ;;
  41. *)
  42. [[ ${#key} -ge 8 ]]
  43. return $?; ;;
  44. esac
  45. }
  46. validate_port() {
  47. local port="$1"
  48. if [[ ! "$port" =~ ^[0-9]+$ ]]; then
  49. return 1
  50. fi
  51. if (( port < 1 || port > 65535 )); then
  52. return 1
  53. fi
  54. return 0
  55. }
  56. validate_positive_integer() {
  57. local value="$1"
  58. if [[ ! "$value" =~ ^[0-9]+$ ]]; then
  59. return 1
  60. fi
  61. (( 10#$value > 0 ))
  62. }
  63. validate_non_negative_integer() {
  64. local value="$1"
  65. if [[ ! "$value" =~ ^[0-9]+$ ]]; then
  66. return 1
  67. fi
  68. (( 10#$value >= 0 ))
  69. }
  70. validate_non_empty() {
  71. local value="$1"
  72. [[ -n "$value" ]]
  73. }
  74. validate_existing_file() {
  75. local path="$1"
  76. [[ -n "$path" && -f "$path" ]]
  77. }
  78. check_storage_compatibility() {
  79. local kv_storage="$1"
  80. local vector_storage="$2"
  81. local graph_storage="$3"
  82. local doc_status_storage="$4"
  83. local warnings=()
  84. if [[ "$vector_storage" == "MongoVectorDBStorage" ]]; then
  85. warnings+=("MongoDB vector storage requires Atlas Search / Vector Search support, such as an Atlas cluster or Atlas Local deployment.")
  86. fi
  87. if [[ "$graph_storage" == "Neo4JStorage" && "$kv_storage" == "JsonKVStorage" ]]; then
  88. warnings+=("Neo4j graph with JSON KV storage is fine for dev, but not ideal for production.")
  89. fi
  90. if [[ "$graph_storage" == "NetworkXStorage" ]]; then
  91. warnings+=("NetworkX graph storage is memory-bound and suited for small datasets only.")
  92. fi
  93. if [[ "$vector_storage" == "FaissVectorDBStorage" ]]; then
  94. warnings+=("Faiss vector storage is local-only and requires manual persistence management.")
  95. fi
  96. if [[ "$kv_storage" == "JsonKVStorage" || "$doc_status_storage" == "JsonDocStatusStorage" ]]; then
  97. warnings+=("JSON-based KV/doc status storage is recommended only for local development.")
  98. fi
  99. if ((${#warnings[@]} > 0)); then
  100. echo "${COLOR_YELLOW:-}Storage compatibility/performance warnings:${COLOR_RESET:-}" >&2
  101. for warning in "${warnings[@]}"; do
  102. echo " - $warning" >&2
  103. done
  104. fi
  105. return 0
  106. }
  107. format_error() {
  108. local message="$1"
  109. local suggestion="${2:-}"
  110. echo "${COLOR_RED:-}Error:${COLOR_RESET:-} $message" >&2
  111. if [[ -n "$suggestion" ]]; then
  112. echo "${COLOR_YELLOW:-}Hint:${COLOR_RESET:-} $suggestion" >&2
  113. fi
  114. }
  115. contains_env_interpolation_syntax() {
  116. local value="$1"
  117. [[ "$value" == *'${'* ]]
  118. }
  119. is_sensitive_env_key() {
  120. local key="$1"
  121. case "$key" in
  122. AUTH_ACCOUNTS|*API_KEY*|*ACCESS_KEY*|*PUBLIC_KEY*|*SECRET*|*PASSWORD*|*TOKEN*)
  123. return 0
  124. ;;
  125. *)
  126. return 1
  127. ;;
  128. esac
  129. }
  130. validate_sensitive_env_literals() {
  131. local key value
  132. local invalid_keys=()
  133. for key in "${!ENV_VALUES[@]}"; do
  134. if ! is_sensitive_env_key "$key"; then
  135. continue
  136. fi
  137. value="${ENV_VALUES[$key]:-}"
  138. if [[ -n "$value" ]] && contains_env_interpolation_syntax "$value"; then
  139. invalid_keys+=("$key")
  140. fi
  141. done
  142. if ((${#invalid_keys[@]} > 0)); then
  143. format_error \
  144. "Sensitive values must not contain \${...} interpolation syntax: ${invalid_keys[*]}" \
  145. "Use literal values, plain \$ characters, or inject those secrets via runtime environment variables instead of .env."
  146. return 1
  147. fi
  148. return 0
  149. }
  150. validate_required_variables() {
  151. local storages=("$@")
  152. local missing=()
  153. local unknown=()
  154. local storage required var
  155. for storage in "${storages[@]}"; do
  156. if [[ -z "$storage" ]]; then
  157. continue
  158. fi
  159. if [[ ! -v "STORAGE_ENV_REQUIREMENTS[$storage]" ]]; then
  160. unknown+=("$storage")
  161. continue
  162. fi
  163. required="${STORAGE_ENV_REQUIREMENTS[$storage]}"
  164. if [[ -z "$required" ]]; then
  165. continue
  166. fi
  167. for var in $required; do
  168. if [[ -z "${ENV_VALUES[$var]:-}" ]]; then
  169. missing+=("$var")
  170. fi
  171. done
  172. done
  173. if ((${#unknown[@]} > 0)); then
  174. format_error \
  175. "Unsupported storage selections: ${unknown[*]}" \
  176. "Use a supported LightRAG storage class name or rerun setup to pick a valid backend."
  177. return 1
  178. fi
  179. if ((${#missing[@]} > 0)); then
  180. format_error "Missing required variables: ${missing[*]}" "Fill them in .env or re-run setup."
  181. return 1
  182. fi
  183. return 0
  184. }
  185. validate_opensearch_hosts_format() {
  186. local hosts="${1:-${ENV_VALUES[OPENSEARCH_HOSTS]:-}}"
  187. local entry=""
  188. local trimmed=""
  189. local has_host="no"
  190. local -a entries=()
  191. if [[ "$hosts" == *"://"* ]]; then
  192. format_error \
  193. "OPENSEARCH_HOSTS must use bare host:port entries, not URLs." \
  194. "Set comma-separated host:port values such as localhost:9200; control TLS with OPENSEARCH_USE_SSL."
  195. return 1
  196. fi
  197. IFS=',' read -r -a entries <<< "$hosts"
  198. for entry in "${entries[@]}"; do
  199. trimmed="${entry#"${entry%%[![:space:]]*}"}"
  200. trimmed="${trimmed%"${trimmed##*[![:space:]]}"}"
  201. if [[ -z "$trimmed" ]]; then
  202. format_error \
  203. "OPENSEARCH_HOSTS must not contain empty host entries." \
  204. "Use comma-separated host:port values such as localhost:9200 or host1:9200,host2:9200."
  205. return 1
  206. fi
  207. has_host="yes"
  208. done
  209. if [[ "$has_host" != "yes" ]]; then
  210. format_error \
  211. "OPENSEARCH_HOSTS must include at least one host:port entry." \
  212. "Set it to a value such as localhost:9200."
  213. return 1
  214. fi
  215. return 0
  216. }
  217. validate_opensearch_password_strength() {
  218. local password="${1:-${ENV_VALUES[OPENSEARCH_PASSWORD]:-}}"
  219. if [[ ${#password} -lt 8 || ! "$password" =~ [A-Z] || ! "$password" =~ [a-z] || ! "$password" =~ [0-9] || ! "$password" =~ [^A-Za-z0-9] ]]; then
  220. format_error \
  221. "OpenSearch requires a strong OPENSEARCH_PASSWORD." \
  222. "Use at least 8 characters with uppercase, lowercase, number, and special character."
  223. return 1
  224. fi
  225. return 0
  226. }
  227. validate_opensearch_config() {
  228. local deployment_mode="${1:-${ENV_VALUES[LIGHTRAG_SETUP_OPENSEARCH_DEPLOYMENT]:-}}"
  229. local hosts="${2:-${ENV_VALUES[OPENSEARCH_HOSTS]:-}}"
  230. local user="${3:-${ENV_VALUES[OPENSEARCH_USER]:-}}"
  231. local password="${4:-${ENV_VALUES[OPENSEARCH_PASSWORD]:-}}"
  232. local num_shards="${5-${ENV_VALUES[OPENSEARCH_NUMBER_OF_SHARDS]-1}}"
  233. local num_replicas="${6-${ENV_VALUES[OPENSEARCH_NUMBER_OF_REPLICAS]-0}}"
  234. if ! validate_opensearch_hosts_format "$hosts"; then
  235. return 1
  236. fi
  237. if [[ -z "$user" || -z "$password" ]]; then
  238. if [[ "$deployment_mode" == "docker" ]]; then
  239. format_error \
  240. "Bundled OpenSearch requires OPENSEARCH_USER and OPENSEARCH_PASSWORD." \
  241. "Set both variables or rerun setup; the managed Docker service starts with security enabled."
  242. else
  243. format_error \
  244. "OpenSearch requires both OPENSEARCH_USER and OPENSEARCH_PASSWORD." \
  245. "This setup wizard only supports authenticated OpenSearch clusters. Set both values or rerun setup."
  246. fi
  247. return 1
  248. fi
  249. if ! validate_opensearch_password_strength "$password"; then
  250. if [[ "$deployment_mode" == "docker" ]]; then
  251. echo "${COLOR_YELLOW:-}Hint:${COLOR_RESET:-} The managed Docker image also enforces this password strength at startup." >&2
  252. fi
  253. return 1
  254. fi
  255. if ! validate_positive_integer "$num_shards"; then
  256. format_error \
  257. "OPENSEARCH_NUMBER_OF_SHARDS must be a positive integer." \
  258. "Set it to 1 or greater, or rerun setup to regenerate the OpenSearch index settings."
  259. return 1
  260. fi
  261. if ! validate_non_negative_integer "$num_replicas"; then
  262. format_error \
  263. "OPENSEARCH_NUMBER_OF_REPLICAS must be a non-negative integer." \
  264. "Set it to 0 or greater, or rerun setup to regenerate the OpenSearch index settings."
  265. return 1
  266. fi
  267. return 0
  268. }
  269. validate_mongo_vector_storage_config() {
  270. local vector_storage="$1"
  271. local mongo_uri="${2:-${ENV_VALUES[MONGO_URI]:-}}"
  272. local mongo_deployment="${3:-${ENV_VALUES[LIGHTRAG_SETUP_MONGODB_DEPLOYMENT]:-}}"
  273. if [[ "$vector_storage" != "MongoVectorDBStorage" ]]; then
  274. return 0
  275. fi
  276. if ! validate_uri "$mongo_uri" mongodb; then
  277. format_error \
  278. "MongoVectorDBStorage requires a valid MongoDB URI." \
  279. "Set MONGO_URI to a mongodb:// or mongodb+srv:// endpoint that supports Atlas Search / Vector Search."
  280. return 1
  281. fi
  282. if [[ "$mongo_deployment" == "docker" ]]; then
  283. if [[ ! "$mongo_uri" =~ ^mongodb://([^/?#]+@)?(mongodb|localhost|127\.0\.0\.1|0\.0\.0\.0):27017([/?#].*)?$ ]] || ! _mongo_uri_has_direct_connection_true "$mongo_uri"; then
  284. format_error \
  285. "MongoVectorDBStorage requires the bundled Atlas Local endpoint when LIGHTRAG_SETUP_MONGODB_DEPLOYMENT=docker." \
  286. "Set MONGO_URI to the wizard-managed local MongoDB URI, or remove the docker deployment marker and use a mongodb+srv:// Atlas cluster URI."
  287. return 1
  288. fi
  289. return 0
  290. fi
  291. if [[ "$mongo_uri" =~ ^mongodb\+srv:// ]]; then
  292. return 0
  293. fi
  294. if ! _mongo_uri_has_direct_connection_true "$mongo_uri"; then
  295. format_error \
  296. "MongoVectorDBStorage requires an Atlas-capable MongoDB URI." \
  297. "Use a mongodb+srv:// Atlas cluster URI, a mongodb:// Atlas Local URI with ?directConnection=true, or rerun the wizard with the bundled Atlas Local Docker MongoDB service."
  298. return 1
  299. fi
  300. return 0
  301. }
  302. _mongo_uri_has_direct_connection_true() {
  303. local uri="$1"
  304. local direct_connection_pattern='[?&]directConnection=true([&#]|$)'
  305. [[ "$uri" =~ ^mongodb:// ]] && [[ "$uri" =~ $direct_connection_pattern ]]
  306. }
  307. validate_auth_accounts_format() {
  308. local auth_accounts="$1"
  309. local entry username password
  310. if [[ -z "$auth_accounts" ]]; then
  311. return 0
  312. fi
  313. if [[ "$auth_accounts" == ,* || "$auth_accounts" == *, || "$auth_accounts" == *",,"* ]]; then
  314. return 1
  315. fi
  316. IFS=',' read -r -a entries <<< "$auth_accounts"
  317. for entry in "${entries[@]}"; do
  318. if [[ -z "$entry" || "$entry" != *:* ]]; then
  319. return 1
  320. fi
  321. username="${entry%%:*}"
  322. password="${entry#*:}"
  323. if [[ -z "$username" || -z "$password" ]]; then
  324. return 1
  325. fi
  326. done
  327. return 0
  328. }
  329. validate_auth_accounts_password_safety() {
  330. local auth_accounts="$1"
  331. local entry password normalized_password
  332. if [[ -z "$auth_accounts" ]]; then
  333. return 0
  334. fi
  335. IFS=',' read -r -a entries <<< "$auth_accounts"
  336. for entry in "${entries[@]}"; do
  337. password="${entry#*:}"
  338. normalized_password="${password,,}"
  339. if [[ "$normalized_password" == admin* || "$normalized_password" == pass* ]]; then
  340. return 1
  341. fi
  342. done
  343. return 0
  344. }
  345. validate_auth_accounts_runtime_config() {
  346. local auth_accounts="$1"
  347. if [[ -z "$auth_accounts" ]]; then
  348. return 0
  349. fi
  350. if ! validate_auth_accounts_format "$auth_accounts"; then
  351. format_error \
  352. "AUTH_ACCOUNTS must use comma-separated user:password pairs." \
  353. "Use entries like admin:{bcrypt}<hash> or admin:secret,reader:another-secret."
  354. return 1
  355. fi
  356. return 0
  357. }
  358. whitelist_exposes_api_routes() {
  359. local whitelist_paths="$1"
  360. local entry trimmed_entry normalized_entry
  361. IFS=',' read -r -a entries <<< "$whitelist_paths"
  362. for entry in "${entries[@]}"; do
  363. trimmed_entry="${entry#"${entry%%[![:space:]]*}"}"
  364. trimmed_entry="${trimmed_entry%"${trimmed_entry##*[![:space:]]}"}"
  365. normalized_entry="$trimmed_entry"
  366. if [[ "$normalized_entry" == *"/*" ]]; then
  367. normalized_entry="${normalized_entry%/*}"
  368. fi
  369. if [[ "$normalized_entry" != "/" ]]; then
  370. normalized_entry="${normalized_entry%/}"
  371. fi
  372. if [[ "$normalized_entry" == "/api" || "$normalized_entry" == /api/* ]]; then
  373. return 0
  374. fi
  375. done
  376. return 1
  377. }
  378. validate_security_config() {
  379. local auth_accounts="${1:-${ENV_VALUES[AUTH_ACCOUNTS]:-}}"
  380. local token_secret="${2:-${ENV_VALUES[TOKEN_SECRET]:-}}"
  381. local _api_key="${3:-${ENV_VALUES[LIGHTRAG_API_KEY]:-}}"
  382. local _unused_flag="${4:-no}"
  383. local _unused_whitelist="${5:-${ENV_VALUES[WHITELIST_PATHS]:-}}"
  384. local _unused_whitelist_is_set="${6:-}"
  385. if [[ -z "$auth_accounts" ]]; then
  386. return 0
  387. fi
  388. if ! validate_auth_accounts_format "$auth_accounts"; then
  389. format_error \
  390. "AUTH_ACCOUNTS must use comma-separated user:password pairs." \
  391. "Use entries like admin:{bcrypt}<hash> or admin:secret,reader:another-secret."
  392. return 1
  393. fi
  394. if ! validate_auth_accounts_password_safety "$auth_accounts"; then
  395. format_error \
  396. "AUTH_ACCOUNTS passwords must not start with 'admin' or 'pass'." \
  397. "Choose a less predictable password or use lightrag-hash-password to generate a {bcrypt} value."
  398. return 1
  399. fi
  400. if [[ -z "$token_secret" ]]; then
  401. format_error \
  402. "AUTH_ACCOUNTS is set but TOKEN_SECRET is missing." \
  403. "Set a non-empty JWT signing secret before enabling account-based authentication."
  404. return 1
  405. fi
  406. if [[ "$token_secret" == "lightrag-jwt-default-secret-key!" ]]; then
  407. format_error \
  408. "TOKEN_SECRET must not use the built-in default value when AUTH_ACCOUNTS is enabled." \
  409. "Generate a unique JWT signing secret and update TOKEN_SECRET."
  410. return 1
  411. fi
  412. return 0
  413. }
  414. check_docker_availability() {
  415. if ! command -v docker >/dev/null 2>&1; then
  416. format_error "Docker is not installed or not in PATH." "Install Docker or disable docker service generation."
  417. return 1
  418. fi
  419. if ! docker compose version >/dev/null 2>&1; then
  420. format_error "Docker Compose is not available." "Install the Docker Compose plugin or use docker-compose."
  421. return 1
  422. fi
  423. return 0
  424. }