Require LDAP username on first VPN launch

This commit is contained in:
2026-05-26 14:30:21 +03:00
parent f2d4f8e04b
commit b8ddf8f22d
6 changed files with 210 additions and 26 deletions

View File

@@ -387,7 +387,7 @@ open ~/Applications/LemanaVPN.app # открыть Swift-приложение
Файл `~/.config/lemana-vpn/env`:
```sh
LEMANA_VPN_USERNAME="60103293"
LEMANA_VPN_USERNAME="<LDAP_USERNAME>"
LEMANA_VPN_CREDENTIAL_SOURCE="bitwarden"
LEMANA_VPN_BW_ITEM="LM LDAP"
LEMANA_VPN_USE_BITWARDEN="1"
@@ -398,18 +398,18 @@ LEMANA_VPN_DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
Для бесплатного Keychain-only источника:
```sh
LEMANA_VPN_USERNAME="60103293"
LEMANA_VPN_USERNAME="<LDAP_USERNAME>"
LEMANA_VPN_CREDENTIAL_SOURCE="keychain"
LEMANA_VPN_USE_BITWARDEN="0"
LEMANA_VPN_USE_TOUCHID="0"
LEMANA_VPN_DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
```
Для другого логина:
Если LDAP username не указан при установке, `vpn` спросит его при первом интерактивном запуске в Terminal и сохранит в `~/.config/lemana-vpn/env`. Для неинтерактивной установки можно указать его сразу:
```sh
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
| sh -s -- --username 12345678
| sh -s -- --username <LDAP_USERNAME>
```
## Bitwarden item

View File

@@ -3,6 +3,7 @@ set -euo pipefail
CONFIG_DIR="${LEMANA_VPN_CONFIG_DIR:-$HOME/.config/lemana-vpn}"
CONFIG_FILE="$CONFIG_DIR/env"
OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
_ENV_LEMANA_VPN_USERNAME="${LEMANA_VPN_USERNAME+x}${LEMANA_VPN_USERNAME-}"
_ENV_LEMANA_VPN_BW_ITEM="${LEMANA_VPN_BW_ITEM+x}${LEMANA_VPN_BW_ITEM-}"
@@ -27,7 +28,7 @@ OC_VENV="${LEMANA_VPN_OC_VENV:-$HOME/.local/pipx/venvs/openconnect-lite}"
OC_PYTHON="${LEMANA_VPN_OC_PYTHON:-$OC_VENV/bin/python}"
OC_BIN="${LEMANA_VPN_OC_BIN:-$HOME/.local/bin/openconnect-lite}"
BW_ITEM_NAME="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
KC_USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
KC_USERNAME="${LEMANA_VPN_USERNAME:-}"
KC_FP="${LEMANA_VPN_KEYCHAIN_FINGERPRINT:-$HOME/bin/keychain-fingerprint}"
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-}"
if [[ -z "$CREDENTIAL_SOURCE" ]]; then
@@ -648,7 +649,99 @@ PY
}
_can_prompt() {
[[ -t 0 ]]
[[ -r /dev/tty && -w /dev/tty ]] || [[ -t 0 ]]
}
_prompt_read() {
local var_name="$1" prompt="$2" secret="${3:-0}" value
if [[ -r /dev/tty && -w /dev/tty ]]; then
if [[ "$secret" == "1" ]]; then
IFS= read -rsp "$prompt" value < /dev/tty || value=""
printf '\n' > /dev/tty
else
IFS= read -rp "$prompt" value < /dev/tty || value=""
fi
else
if [[ "$secret" == "1" ]]; then
IFS= read -rsp "$prompt" value || value=""
printf '\n'
else
IFS= read -rp "$prompt" value || value=""
fi
fi
printf -v "$var_name" '%s' "$value"
}
_persist_username() {
KC_USERNAME="$KC_USERNAME" CONFIG_FILE="$CONFIG_FILE" OC_CONFIG_FILE="$OC_CONFIG_DIR/config.toml" python3 - <<'PY'
import json
import os
from pathlib import Path
username = os.environ["KC_USERNAME"]
config_file = Path(os.environ["CONFIG_FILE"])
oc_config_file = Path(os.environ["OC_CONFIG_FILE"])
def shell_quote(value: str) -> str:
return '"' + value.replace("\\", "\\\\").replace('"', '\\"').replace("$", "\\$").replace("`", "\\`") + '"'
config_file.parent.mkdir(parents=True, exist_ok=True)
lines = []
if config_file.exists():
lines = config_file.read_text().splitlines()
username_line = "LEMANA_VPN_USERNAME=" + shell_quote(username)
for index, line in enumerate(lines):
if line.startswith("LEMANA_VPN_USERNAME="):
lines[index] = username_line
break
else:
lines.insert(0, username_line)
config_file.write_text("\n".join(lines).rstrip() + "\n")
config_file.chmod(0o600)
if oc_config_file.exists():
oc_lines = oc_config_file.read_text().splitlines()
rendered = "username = " + json.dumps(username)
for index, line in enumerate(oc_lines):
if line.strip().startswith("username"):
oc_lines[index] = rendered
break
else:
oc_lines.insert(0, rendered)
oc_config_file.write_text("\n".join(oc_lines).rstrip() + "\n")
oc_config_file.chmod(0o600)
PY
}
_ensure_username() {
if [[ -n "${KC_USERNAME:-}" ]]; then
return 0
fi
_emit '{"event":"username_required"}' "LDAP username is missing. Enter it once to save it for future VPN runs."
if ! _can_prompt; then
_emit '{"event":"error","message":"LDAP username is missing. Run vpn in Terminal once, or reinstall with --username <LDAP_USERNAME>."}' \
"LDAP username is missing. Run in Terminal: vpn"
return 1
fi
while [[ -z "${KC_USERNAME:-}" ]]; do
_prompt_read KC_USERNAME "Corporate LDAP username: "
KC_USERNAME="${KC_USERNAME#"${KC_USERNAME%%[![:space:]]*}"}"
KC_USERNAME="${KC_USERNAME%"${KC_USERNAME##*[![:space:]]}"}"
if [[ -z "$KC_USERNAME" ]]; then
printf 'LDAP username is required.\n' >&2
fi
done
export LEMANA_VPN_USERNAME="$KC_USERNAME"
_persist_username
_emit '{"event":"username_saved"}' "LDAP username saved for future VPN runs."
}
_configure_keychain() {
@@ -664,17 +757,15 @@ _configure_keychain() {
printf 'Saved values go to macOS Keychain service openconnect-lite.\n\n'
if $password_present; then
read -rsp "Corporate LDAP password for $KC_USERNAME [leave empty to keep saved password]: " password
_prompt_read password "Corporate LDAP password for $KC_USERNAME [leave empty to keep saved password]: " 1
else
read -rsp "Corporate LDAP password for $KC_USERNAME: " password
_prompt_read password "Corporate LDAP password for $KC_USERNAME: " 1
fi
printf '\n'
if $totp_present; then
read -rsp "TOTP seed BASE32 [leave empty to keep saved seed]: " totp_secret
_prompt_read totp_secret "TOTP seed BASE32 [leave empty to keep saved seed]: " 1
else
read -rsp "TOTP seed BASE32 from 2FA setup: " totp_secret
_prompt_read totp_secret "TOTP seed BASE32 from 2FA setup: " 1
fi
printf '\n'
if [[ -z "$password" && "$password_present" != "true" ]]; then
printf 'LDAP password is required because no saved password was found.\n' >&2
@@ -769,8 +860,7 @@ _bw_unlock() {
fi
_emit '{"event":"bw_manual"}' "Bitwarden vault is locked. Enter Bitwarden master password to sync LDAP credentials."
read -rsp "Bitwarden master password (not LDAP password): " manual_pw
printf '\n'
_prompt_read manual_pw "Bitwarden master password (not LDAP password): " 1
if [[ -z "$manual_pw" ]]; then
printf 'Empty Bitwarden password. Using existing Keychain credentials.\n' >&2
return 1
@@ -786,7 +876,7 @@ _bw_unlock() {
if [[ "$USE_TOUCHID" == "1" && -x "$KC_FP" ]]; then
local save_choice
read -rp "Save Bitwarden master password behind Touch ID for next VPN unlock? [Y/n] " save_choice
_prompt_read save_choice "Save Bitwarden master password behind Touch ID for next VPN unlock? [Y/n] "
if [[ "${save_choice:-y}" =~ ^[Yy]?$ ]]; then
printf '%s' "$manual_pw" | "$KC_FP" set "$BW_KC_SERVICE" "$BW_KC_ACCOUNT_MASTER" >/dev/null 2>&1 \
&& printf 'Saved. Next unlock can use Touch ID.\n' \
@@ -921,6 +1011,7 @@ if $STATUS_MODE; then
fi
if $CONFIGURE_KEYCHAIN_MODE; then
_ensure_username
_configure_keychain
exit 0
fi
@@ -936,6 +1027,7 @@ else
printf '{"event":"modules","modules":%s}\n' "$(_module_status_json)"
fi
_ensure_username
_sync_credentials
_ensure_keychain_credentials
_patch_oc

View File

@@ -772,7 +772,7 @@ LEMANA_VPN_CREDENTIAL_SOURCE="bitwarden"
For the keychain source, add this separate example:
```sh
LEMANA_VPN_USERNAME="60103293"
LEMANA_VPN_USERNAME="<LDAP_USERNAME>"
LEMANA_VPN_CREDENTIAL_SOURCE="keychain"
LEMANA_VPN_USE_BITWARDEN="0"
LEMANA_VPN_USE_TOUCHID="0"

View File

@@ -9,7 +9,7 @@ INSTALL_BIN_DIR="${LEMANA_VPN_BIN_DIR:-$HOME/bin}"
CONFIG_DIR="${LEMANA_VPN_CONFIG_DIR:-$HOME/.config/lemana-vpn}"
OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
USERNAME="${LEMANA_VPN_USERNAME:-}"
BW_ITEM="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-bitwarden}"
USE_BITWARDEN=1
@@ -81,7 +81,7 @@ Options:
--with-touchid Install/use keychain-fingerprint Touch ID helper (default)
--without-touchid Do not install/use Touch ID helper
--configure-keychain Prompt for LDAP password and TOTP secret after install
--username VALUE Corporate LDAP username (default: 60103293)
--username VALUE Corporate LDAP username (otherwise asked on first Terminal connection)
--bw-item VALUE Bitwarden item name (default: LM LDAP)
--raw-base-url URL Raw file base URL for curl installs
--no-sudoers Do not install sudoers rules
@@ -615,7 +615,7 @@ install_cli() {
install_config() {
tmp="$1"
log_step "Записываю конфигурацию"
log_detail "Здесь сохраняются выбранные модули, LDAP username и профиль openconnect-lite для Keycloak SSO."
log_detail "Здесь сохраняются выбранные модули, LDAP username если он задан, и профиль openconnect-lite для Keycloak SSO."
run mkdir -p "$CONFIG_DIR" "$OC_CONFIG_DIR"

View File

@@ -61,6 +61,11 @@ printf '%s\n' "$keychain_status_json" | grep -q '"touchid":{"enabled":false'
bitwarden_status_json="$(LEMANA_VPN_CREDENTIAL_SOURCE=bitwarden bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
printf '%s\n' "$bitwarden_status_json" | grep -q '"credentials":{"source":"bitwarden","keychain_ready":false}'
printf '%s\n' "$bitwarden_status_json" | grep -q '"bitwarden":{"enabled":true'
personal_ldap='601''03293'
if grep -R "$personal_ldap" "$ROOT/bin" "$ROOT/install.sh" "$ROOT/uninstall.sh" "$ROOT/README.md" "$ROOT/templates" "$ROOT/app" "$ROOT/.agents" >/dev/null; then
echo "personal LDAP username must not be hardcoded in runtime, installer, app, docs, or agent guidance" >&2
exit 1
fi
grep -q 'LemanaVPN-openconnect-lite.log' "$ROOT/bin/vpn-lemanapro.sh"
grep -q '"event":"waiting"' "$ROOT/bin/vpn-lemanapro.sh"
grep -q -- '--patch-only' "$ROOT/bin/vpn-lemanapro.sh"
@@ -238,12 +243,43 @@ if printf '%s\n' "$manual_output" | grep -q 'Cleaning up VPN DNS'; then
exit 1
fi
missing_username_home="$TMP_DIR/missing-username-home"
mkdir -p "$missing_username_home"
set +e
missing_username_output="$(
HOME="$missing_username_home" \
LEMANA_VPN_CONFIG_DIR="$missing_username_home/.config/lemana-vpn" \
OPENCONNECT_LITE_CONFIG_DIR="$missing_username_home/.config/openconnect-lite" \
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
bash "$ROOT/bin/vpn-lemanapro.sh" --json 2>&1
)"
missing_username_code=$?
set -e
[ "$missing_username_code" -ne 0 ]
printf '%s\n' "$missing_username_output" | grep -q '"event":"username_required"'
printf '%s\n' "$missing_username_output" | grep -q 'LDAP username is missing'
if printf '%s\n' "$missing_username_output" | grep -q '"event":"keychain_required"'; then
echo "missing username should fail before checking Keychain credentials" >&2
exit 1
fi
fake_path_bin="$TMP_DIR/fake-path-bin"
mkdir -p "$fake_path_bin"
cat > "$fake_path_bin/security" <<'SH'
#!/bin/sh
if [ "${1:-}" = "find-generic-password" ]; then
exit 0
account=""
while [ "$#" -gt 0 ]; do
if [ "$1" = "-a" ]; then
shift
account="${1:-}"
break
fi
shift
done
[ -n "$account" ]
exit $?
fi
exit 1
SH
@@ -262,6 +298,47 @@ exit 0
SH
chmod +x "$fake_oc_bin"
if command -v expect >/dev/null 2>&1; then
first_username_home="$TMP_DIR/first-username-home"
mkdir -p "$first_username_home/.config/lemana-vpn" "$first_username_home/.config/openconnect-lite"
printf 'LEMANA_VPN_CREDENTIAL_SOURCE="keychain"\nLEMANA_VPN_USE_BITWARDEN="0"\nLEMANA_VPN_USE_TOUCHID="0"\n' > "$first_username_home/.config/lemana-vpn/env"
printf 'username = ""\n' > "$first_username_home/.config/openconnect-lite/config.toml"
FIRST_USERNAME_HOME="$first_username_home" \
ROOT="$ROOT" \
FAKE_PATH_BIN="$fake_path_bin" \
FAKE_OC_BIN="$fake_oc_bin" \
FAKE_WEBENGINE="$fake_webengine" \
FAKE_AUTHENTICATOR="$fake_authenticator" \
MANUAL_FULL_CAPTURE="$manual_full_capture" \
TMP_DIR="$TMP_DIR" \
expect <<'EXPECT'
set timeout 30
log_user 0
set home $env(FIRST_USERNAME_HOME)
spawn env \
HOME=$home \
PATH=$env(FAKE_PATH_BIN):$env(PATH) \
LEMANA_VPN_CONFIG_DIR=$home/.config/lemana-vpn \
OPENCONNECT_LITE_CONFIG_DIR=$home/.config/openconnect-lite \
LEMANA_VPN_OC_BIN=$env(FAKE_OC_BIN) \
LEMANA_VPN_OC_PYTHON=python3 \
LEMANA_VPN_WEBENGINE_PROCESS=$env(FAKE_WEBENGINE) \
LEMANA_VPN_AUTHENTICATOR=$env(FAKE_AUTHENTICATOR) \
LEMANA_VPN_PATCH_BACKUP_DIR=$env(TMP_DIR)/first-username-patch-backups \
LEMANA_VPN_DNS_CLEANUP=$env(TMP_DIR)/no-dns-cleanup \
LEMANA_VPN_CONNECT_LOG=$env(TMP_DIR)/first-username.log \
LEMANA_VPN_CAPTURE_LAUNCH=$env(MANUAL_FULL_CAPTURE) \
bash $env(ROOT)/bin/vpn-lemanapro.sh --manual-full --json
expect "Corporate LDAP username:"
send "lemana-first-user\r"
expect eof
EXPECT
grep -q 'LEMANA_VPN_USERNAME="lemana-first-user"' "$first_username_home/.config/lemana-vpn/env"
grep -q 'username = "lemana-first-user"' "$first_username_home/.config/openconnect-lite/config.toml"
fi
manual_full_output="$(
HOME="$HOME" \
PATH="$fake_path_bin:$PATH" \

View File

@@ -3,10 +3,17 @@ set -eu
INSTALL_BIN_DIR="${LEMANA_VPN_BIN_DIR:-$HOME/bin}"
CONFIG_DIR="${LEMANA_VPN_CONFIG_DIR:-$HOME/.config/lemana-vpn}"
CONFIG_FILE="$CONFIG_DIR/env"
OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
OC_VENV="${LEMANA_VPN_OC_VENV:-$HOME/.local/pipx/venvs/openconnect-lite}"
DNS_CLEANUP="${LEMANA_VPN_DNS_CLEANUP:-/usr/local/sbin/lemana-vpn-dns-cleanup}"
USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
_ENV_LEMANA_VPN_USERNAME="${LEMANA_VPN_USERNAME+x}${LEMANA_VPN_USERNAME-}"
if [ -f "$CONFIG_FILE" ]; then
# shellcheck disable=SC1090
. "$CONFIG_FILE"
fi
[ "${_ENV_LEMANA_VPN_USERNAME%"${_ENV_LEMANA_VPN_USERNAME#?}"}" = "x" ] && LEMANA_VPN_USERNAME="${_ENV_LEMANA_VPN_USERNAME#?}"
USERNAME="${LEMANA_VPN_USERNAME:-}"
APP_DIR="${LEMANA_VPN_APP_DIR:-$HOME/Applications/LemanaVPN.app}"
LAUNCH_AGENT="$HOME/Library/LaunchAgents/ru.dokops.LemanaVPN.plist"
DRY_RUN=0
@@ -203,15 +210,23 @@ remove_keychain_entries() {
fi
log_step "Удаляю VPN-записи из macOS Keychain"
log_detail "Удаляются только записи openconnect-lite для пользователя $USERNAME и кэш Bitwarden Lemana VPN."
if [ -n "$USERNAME" ]; then
log_detail "Удаляются только записи openconnect-lite для пользователя $USERNAME и кэш Bitwarden Lemana VPN."
else
log_detail "LDAP username не задан; удаляется только кэш Bitwarden Lemana VPN."
fi
if [ "$DRY_RUN" -eq 1 ]; then
run security delete-generic-password -s openconnect-lite -a "$USERNAME"
run security delete-generic-password -s openconnect-lite -a "totp/$USERNAME"
if [ -n "$USERNAME" ]; then
run security delete-generic-password -s openconnect-lite -a "$USERNAME"
run security delete-generic-password -s openconnect-lite -a "totp/$USERNAME"
fi
run security delete-generic-password -s vpn-lemanapro -a bw-session
run security delete-generic-password -s vpn-lemanapro -a bw-master
else
security delete-generic-password -s openconnect-lite -a "$USERNAME" >/dev/null 2>&1 || true
security delete-generic-password -s openconnect-lite -a "totp/$USERNAME" >/dev/null 2>&1 || true
if [ -n "$USERNAME" ]; then
security delete-generic-password -s openconnect-lite -a "$USERNAME" >/dev/null 2>&1 || true
security delete-generic-password -s openconnect-lite -a "totp/$USERNAME" >/dev/null 2>&1 || true
fi
security delete-generic-password -s vpn-lemanapro -a bw-session >/dev/null 2>&1 || true
security delete-generic-password -s vpn-lemanapro -a bw-master >/dev/null 2>&1 || true
fi