Make installer interactive for credential source selection
This commit is contained in:
70
.agents/skills/lemana-vpn-operations/SKILL.md
Normal file
70
.agents/skills/lemana-vpn-operations/SKILL.md
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
---
|
||||||
|
name: lemana-vpn-operations
|
||||||
|
description: Use when working in the lemana-vpn repo on install, uninstall, status, CLI aliases, menu-bar app behavior, live VPN connection handling, update instructions, or user-facing answers.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Lemana VPN Operations
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
This repo is a macOS VPN packaging layer around `openconnect`, `openconnect-lite`, Keychain/Bitwarden credential sync, runtime SSO patches, DNS cleanup, and a Swift menu-bar app. Treat live VPN state as real user state, not as a disposable test fixture.
|
||||||
|
|
||||||
|
## System Map
|
||||||
|
|
||||||
|
- `install.sh` installs/updates the whole package and restarts `LemanaVPN.app` only if it is already running.
|
||||||
|
- `bin/vpn-lemanapro.sh` is the runtime source for `vpn`, `vpn-auto`, `vpn-manual`, `vpn-debug`, `vpn-fix-dns`.
|
||||||
|
- `app/Sources/LemanaVPN/VPNManager.swift` shells out to `~/bin/vpn-lemanapro.sh --json`; app state must stay compatible with CLI JSON events.
|
||||||
|
- `templates/openconnect-lite-config.toml` holds Keycloak selectors and the VPN profile.
|
||||||
|
- `uninstall.sh` must stop the running menu-bar app when removing the app, not only delete the bundle.
|
||||||
|
|
||||||
|
## Live Connection Rules
|
||||||
|
|
||||||
|
Before any live connect, run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn --status
|
||||||
|
vpn --status --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not start another `vpn`, `vpn-auto`, `vpn-debug`, `vpn --manual`, or app connect if status says connected/connecting/reconnecting or if the previous connect attempt is still active. Inspect logs instead:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tail -f ~/Library/Logs/LemanaVPN-openconnect-lite.log
|
||||||
|
tail -f ~/Library/Logs/LemanaVPN.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `vpn --manual` for SSO diagnosis before automatic mode. Use `vpn-lemanapro.sh --patch-only` when only runtime patches need to be applied.
|
||||||
|
|
||||||
|
## Update Commands
|
||||||
|
|
||||||
|
From a checkout:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh install.sh
|
||||||
|
vpn --status
|
||||||
|
open ~/Applications/LemanaVPN.app
|
||||||
|
```
|
||||||
|
|
||||||
|
From the remote installer:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
If Bitwarden is disabled or unavailable and Keychain credentials are missing:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn --configure-keychain
|
||||||
|
```
|
||||||
|
|
||||||
|
## Answer Format
|
||||||
|
|
||||||
|
Final answers must always include how to run, how to check, and how to update. Also state whether live VPN was tested or intentionally skipped because an existing connection should not be disturbed.
|
||||||
|
|
||||||
|
## Common Mistakes
|
||||||
|
|
||||||
|
- Starting a second VPN session instead of reading `vpn --status` and logs.
|
||||||
|
- Testing only the Swift app while breaking CLI JSON events.
|
||||||
|
- Calling a Bitwarden master password an LDAP password.
|
||||||
|
- Treating the current 6-digit TOTP code as the TOTP seed.
|
||||||
|
- Forgetting that the menu-bar app may still be running after app files are changed or removed.
|
||||||
78
.agents/skills/lemana-vpn-sso-autofill/SKILL.md
Normal file
78
.agents/skills/lemana-vpn-sso-autofill/SKILL.md
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
---
|
||||||
|
name: lemana-vpn-sso-autofill
|
||||||
|
description: Use when touching Lemana VPN Bitwarden, Keychain, TOTP, openconnect-lite runtime patches, Keycloak SSO, auto-fill, manual mode, hidden browser mode, or authentication troubleshooting.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Lemana VPN SSO And Autofill
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
The connection path is: Bitwarden or existing Keychain -> `openconnect-lite` credentials -> patched Keycloak browser flow -> Cisco/openconnect tunnel -> status JSON/logs/DNS cleanup. Be precise about which credential source is active.
|
||||||
|
|
||||||
|
## Credential Flow
|
||||||
|
|
||||||
|
With Bitwarden enabled, `bin/vpn-lemanapro.sh` unlocks `bw`, reads item `LM LDAP`, extracts LDAP password and permanent TOTP seed, then writes both to macOS Keychain for `openconnect-lite`.
|
||||||
|
|
||||||
|
Without Bitwarden, connection can still work if Keychain already has:
|
||||||
|
|
||||||
|
- service `openconnect-lite`, account `<LDAP username>` for the LDAP password;
|
||||||
|
- service `openconnect-lite`, account `totp/<LDAP username>` for the permanent TOTP seed.
|
||||||
|
|
||||||
|
The Bitwarden master password unlocks the vault. It is not the corporate LDAP password. The TOTP seed is not the current 6-digit authenticator code.
|
||||||
|
|
||||||
|
If a GUI/no-tty path has incomplete Keychain credentials, do not invent an invisible prompt. Tell the user to run:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn --configure-keychain
|
||||||
|
```
|
||||||
|
|
||||||
|
## Runtime Patches
|
||||||
|
|
||||||
|
`vpn-lemanapro.sh` patches `openconnect-lite` before connection. The expected behavior includes:
|
||||||
|
|
||||||
|
- Qt hidden browser mode uses `offscreen`, not `minimal`.
|
||||||
|
- Keycloak fields keep direct `elem.value = ...`, plus `input` and `change` DOM events.
|
||||||
|
- Autofill stays in `ApplicationWorld`; do not reintroduce `MainWorld`, native setters, or stateful click guards unless there is fresh evidence.
|
||||||
|
- URL guard prevents Keycloak selectors from clicking Cisco ACS pages.
|
||||||
|
- Auth redirect reads 302 `Location` from `vpn.lemanapro.ru` without following a TLS-reset-prone final headend GET.
|
||||||
|
- `LEMANA_VPN_AUTOFILL_CLICK=0` disables submit for manual mode.
|
||||||
|
- `LEMANA_VPN_AUTOFILL_DISABLE=1` disables autofill for low-level diagnosis.
|
||||||
|
|
||||||
|
Apply patches without connecting:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn-lemanapro.sh --patch-only
|
||||||
|
```
|
||||||
|
|
||||||
|
## Modes
|
||||||
|
|
||||||
|
- `vpn` / `vpn-auto`: hidden browser, autofill, auto-submit.
|
||||||
|
- `vpn --manual` / `vpn-manual`: visible browser, autofill, no submit.
|
||||||
|
- `vpn-debug`: visible browser and raw logs.
|
||||||
|
|
||||||
|
When diagnosing SSO, use manual mode first. Do not repeatedly start automatic mode if a connection attempt is already in progress.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
For any SSO/autofill patch:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bash -n bin/vpn-lemanapro.sh
|
||||||
|
tests/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
If live behavior must be checked:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn --status
|
||||||
|
vpn --manual
|
||||||
|
tail -f ~/Library/Logs/LemanaVPN-openconnect-lite.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Skip live reconnect when VPN is already connected unless the user explicitly approves disrupting the current session.
|
||||||
|
|
||||||
|
## Troubleshooting Signals
|
||||||
|
|
||||||
|
- Looping on `employee.auth.lemanapro.ru/.../login-actions/authenticate`: check autofill events and Keycloak selectors.
|
||||||
|
- `SSLEOFError` / `UNEXPECTED_EOF_WHILE_READING` on `sslvpna` or `sslvpnb`: check the auth redirect patch.
|
||||||
|
- Menu-bar app shows credential failure without a prompt: use terminal `vpn --configure-keychain`; GUI should not wait for stdin.
|
||||||
95
.agents/skills/lemana-vpn-testing/SKILL.md
Normal file
95
.agents/skills/lemana-vpn-testing/SKILL.md
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
---
|
||||||
|
name: lemana-vpn-testing
|
||||||
|
description: Use when testing, validating, reviewing, or preparing release/update notes for lemana-vpn shell scripts, Swift app code, installer behavior, SSO patches, or live VPN changes.
|
||||||
|
---
|
||||||
|
|
||||||
|
# Lemana VPN Testing
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
Use a ladder: static checks first, isolated smoke tests second, Swift build third, live VPN only when the changed behavior requires it. Never use live reconnect as a default smoke test.
|
||||||
|
|
||||||
|
## Non-Live Verification
|
||||||
|
|
||||||
|
Run the smallest relevant set, expanding with risk:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh -n install.sh
|
||||||
|
sh -n uninstall.sh
|
||||||
|
bash -n bin/vpn-lemanapro.sh
|
||||||
|
sh -n tests/smoke.sh
|
||||||
|
tests/smoke.sh
|
||||||
|
swift build -c release --package-path app
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
`tests/smoke.sh` is the main repo smoke test. It uses temporary homes and fake `openconnect-lite` sources for patch checks, and it asserts status JSON, install dry-run output, uninstall dry-run output, app wiring, manual/auto flags, no-tty credential behavior, and autofill patch invariants.
|
||||||
|
|
||||||
|
## Status Checks
|
||||||
|
|
||||||
|
For source-script behavior:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bin/vpn-lemanapro.sh --status
|
||||||
|
bin/vpn-lemanapro.sh --status --json
|
||||||
|
```
|
||||||
|
|
||||||
|
For the installed user setup:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn --status
|
||||||
|
vpn --status --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Prefer installed status when verifying what the menu-bar app will see, because `VPNManager.swift` calls `~/bin/vpn-lemanapro.sh`.
|
||||||
|
|
||||||
|
## Live VPN Checks
|
||||||
|
|
||||||
|
Live checks are appropriate only for changes to connection start/stop, reconnect, SSO/autofill, Bitwarden/Keychain, `openconnect-lite` patching, status JSON consumed by the app, or DNS cleanup.
|
||||||
|
|
||||||
|
Before a live check:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn --status
|
||||||
|
```
|
||||||
|
|
||||||
|
If already connected, connecting, or reconnecting, do not start another session. Use logs/status and report that live reconnect was skipped to avoid disturbing the current tunnel.
|
||||||
|
|
||||||
|
When SSO/autofill changed, prefer:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn --manual
|
||||||
|
```
|
||||||
|
|
||||||
|
Only use automatic mode after manual mode proves the form is filled correctly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn
|
||||||
|
```
|
||||||
|
|
||||||
|
Use debug only for diagnosis:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn-debug
|
||||||
|
tail -f ~/Library/Logs/LemanaVPN-openconnect-lite.log
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installer/Uninstaller Checks
|
||||||
|
|
||||||
|
Use dry-runs before real install/uninstall:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh install.sh --dry-run --non-interactive --minimal
|
||||||
|
sh uninstall.sh --dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
Real install can modify Homebrew packages, pipx packages, sudoers, `/usr/local/sbin`, `~/.zshrc`, LaunchAgents, Keychain prompts, and the menu-bar app. Real uninstall can stop `LemanaVPN.app`. Be explicit in the answer when those were not run.
|
||||||
|
|
||||||
|
## Final Report
|
||||||
|
|
||||||
|
Report exact commands and results. Always include:
|
||||||
|
|
||||||
|
- Run: command to use the changed repo/app.
|
||||||
|
- Check: command to verify state or behavior.
|
||||||
|
- Update: `sh install.sh` from checkout or the remote `curl ... | sh`.
|
||||||
|
- Not run: especially any skipped live VPN reconnect, real install, or real uninstall.
|
||||||
88
AGENTS.md
Normal file
88
AGENTS.md
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
# Lemana VPN Agent Instructions
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This repository builds a macOS Lemana VPN installer, CLI, DNS cleanup wrapper, and Swift menu-bar app. Work in Russian by default, unless the user asks otherwise.
|
||||||
|
|
||||||
|
Before changing behavior, read the relevant repo-local skills:
|
||||||
|
|
||||||
|
- `.agents/skills/lemana-vpn-operations/SKILL.md` for install/update/connect/status/user-answer rules.
|
||||||
|
- `.agents/skills/lemana-vpn-testing/SKILL.md` for verification strategy and live VPN safety.
|
||||||
|
- `.agents/skills/lemana-vpn-sso-autofill/SKILL.md` for Bitwarden, Keychain, TOTP, SSO, and runtime autofill patches.
|
||||||
|
|
||||||
|
## System Model
|
||||||
|
|
||||||
|
- `install.sh` installs Homebrew/pipx dependencies, `openconnect-lite`, CLI scripts, config, DNS wrapper, sudoers rules, optional Touch ID helper, and optional `~/Applications/LemanaVPN.app`.
|
||||||
|
- `bin/vpn-lemanapro.sh` is the main runtime path. It reports module status, syncs Bitwarden credentials into macOS Keychain, verifies Keychain credentials, applies `openconnect-lite` runtime patches, starts SSO/openconnect, writes status JSON, and cleans VPN DNS on exit.
|
||||||
|
- `templates/openconnect-lite-config.toml` defines the SSO profile and Keycloak auto-fill selectors.
|
||||||
|
- `libexec/lemana-vpn-dns-cleanup` is the narrow root-owned DNS cleanup wrapper.
|
||||||
|
- `app/Sources/LemanaVPN/*` is a menu-bar wrapper over `~/bin/vpn-lemanapro.sh --json`; it does not implement a separate VPN client.
|
||||||
|
- `uninstall.sh` restores patch backups when possible, removes installed files, stops a running `LemanaVPN` process when the app is removed, and optionally removes Keychain/Touch ID/openconnect-lite state.
|
||||||
|
|
||||||
|
## Live VPN Safety
|
||||||
|
|
||||||
|
Always check current state before starting a live connection:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn --status
|
||||||
|
vpn --status --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `~/bin/vpn-lemanapro.sh` if aliases are not loaded. For repo-local code checks, `bin/vpn-lemanapro.sh --status` validates the source script, but it may not be the installed version used by the menu-bar app.
|
||||||
|
|
||||||
|
Do not run `vpn`, `vpn-auto`, `vpn-debug`, `vpn --manual`, or the Swift app connect action repeatedly when a connection is already connected, connecting, reconnecting, or when a live connect attempt is still running. Inspect status and logs instead:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
tail -f ~/Library/Logs/LemanaVPN-openconnect-lite.log
|
||||||
|
tail -f ~/Library/Logs/LemanaVPN.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `vpn --manual` before `vpn`/`vpn-auto` when debugging SSO/autofill, because manual mode shows the browser, fills fields, and does not press submit. Use `vpn-debug` only when raw logs and a visible browser are needed.
|
||||||
|
|
||||||
|
`vpn-lemanapro.sh --patch-only` is safe for applying runtime patches without starting a VPN session.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Prefer the non-live ladder first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh -n install.sh
|
||||||
|
sh -n uninstall.sh
|
||||||
|
bash -n bin/vpn-lemanapro.sh
|
||||||
|
sh -n tests/smoke.sh
|
||||||
|
tests/smoke.sh
|
||||||
|
swift build -c release --package-path app
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Run a live VPN check only when the change affects real connection behavior, credential flow, SSO/autofill, status reporting, reconnect, or DNS cleanup, and only after checking current status. If VPN is already connected, do not start a second session; report that live reconnect was intentionally skipped unless the user explicitly asks for it.
|
||||||
|
|
||||||
|
## Answer Contract
|
||||||
|
|
||||||
|
Every final answer for this repository must include:
|
||||||
|
|
||||||
|
- What changed, with file references when useful.
|
||||||
|
- How to run it.
|
||||||
|
- How to check it.
|
||||||
|
- How to update the installed local setup.
|
||||||
|
- What verification was run, and what was intentionally not run.
|
||||||
|
|
||||||
|
For update instructions, prefer the exact current path:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sh install.sh
|
||||||
|
vpn --status
|
||||||
|
open ~/Applications/LemanaVPN.app
|
||||||
|
```
|
||||||
|
|
||||||
|
For remote user-facing update instructions, use:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
If credentials are missing and Bitwarden is disabled or unavailable from the menu-bar app, point the user to:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn --configure-keychain
|
||||||
|
```
|
||||||
102
README.md
102
README.md
@@ -4,6 +4,8 @@ CLI-установка корпоративного VPN `vpn.lemanapro.ru` дл
|
|||||||
|
|
||||||
**Модули по умолчанию:** Core: включён; Bitwarden: включён; Touch ID: включён; DNS cleanup: включён; Swift Menu Bar app: включён; автозапуск приложения: включён; runtime-патчи: применяются автоматически перед подключением.
|
**Модули по умолчанию:** Core: включён; Bitwarden: включён; Touch ID: включён; DNS cleanup: включён; Swift Menu Bar app: включён; автозапуск приложения: включён; runtime-патчи: применяются автоматически перед подключением.
|
||||||
|
|
||||||
|
**Credential sources:** `bitwarden` синхронизирует LDAP-пароль и TOTP seed из Bitwarden в macOS Keychain; `keychain` хранит LDAP-пароль и постоянный TOTP seed напрямую в macOS Keychain. Оба источника используют один и тот же runtime `openconnect-lite` для SSO/autofill.
|
||||||
|
|
||||||
Репозиторий собирает в один воспроизводимый пакет то, что раньше было ручной локальной настройкой:
|
Репозиторий собирает в один воспроизводимый пакет то, что раньше было ручной локальной настройкой:
|
||||||
|
|
||||||
- `openconnect` как VPN-клиент;
|
- `openconnect` как VPN-клиент;
|
||||||
@@ -20,7 +22,7 @@ CLI-установка корпоративного VPN `vpn.lemanapro.ru` дл
|
|||||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Если установка запущена из терминала, скрипт сначала проверит, что уже стоит, и спросит по отсутствующим опциональным модулям.
|
Если установка запущена из терминала, скрипт работает как интерактивный wizard: проверит, что уже стоит, спросит как хранить credentials, предложит нужные модули и проведёт через настройку.
|
||||||
|
|
||||||
После установки открой новый shell или выполни:
|
После установки открой новый shell или выполни:
|
||||||
|
|
||||||
@@ -29,25 +31,33 @@ exec zsh
|
|||||||
vpn
|
vpn
|
||||||
```
|
```
|
||||||
|
|
||||||
## Варианты установки
|
## Интерактивная установка
|
||||||
|
|
||||||
Полная установка, режим по умолчанию:
|
Обычный путь — запустить installer без флагов и ответить на вопросы:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
Без Touch ID, но с Bitwarden:
|
Первый важный вопрос — как хранить VPN credentials:
|
||||||
|
|
||||||
```sh
|
```text
|
||||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --without-touchid
|
Как хранить VPN credentials?
|
||||||
|
1) Bitwarden -> macOS Keychain
|
||||||
|
2) macOS Keychain: ввести LDAP password и TOTP seed сейчас
|
||||||
|
3) macOS Keychain: настрою вручную позже
|
||||||
|
Выбор [1/2/3, Enter=1]:
|
||||||
```
|
```
|
||||||
|
|
||||||
Минимальная установка без Bitwarden и Touch ID. В macOS Keychain вручную будут записаны LDAP-пароль и TOTP secret. Не текущий 30-секундный TOTP-код, а постоянный seed из настройки 2FA.
|
Что означают варианты:
|
||||||
|
|
||||||
```sh
|
- `1` — использовать Bitwarden как sync-provider: installer поставит/проверит `bw`, а при запуске `vpn` CLI переложит LDAP password и TOTP seed из Bitwarden в macOS Keychain.
|
||||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --minimal --configure-keychain
|
- `2` — бесплатный Keychain-only путь: после установки CLI спросит LDAP password и постоянный TOTP seed или `otpauth://...secret=...`, затем сохранит их в macOS Keychain.
|
||||||
```
|
- `3` — поставить CLI/app сейчас, а credentials настроить позже командой `vpn --configure-keychain`.
|
||||||
|
|
||||||
|
Дальше installer спросит только про системные модули: Touch ID для Bitwarden, sudoers, aliases, Swift Menu Bar app и автозапуск.
|
||||||
|
|
||||||
|
Флаги остаются для CI, повторяемых установок и диагностики. Для обычной установки они не нужны.
|
||||||
|
|
||||||
Проверить действия без изменений:
|
Проверить действия без изменений:
|
||||||
|
|
||||||
@@ -55,6 +65,13 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh |
|
|||||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --dry-run
|
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh -s -- --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Пример неинтерактивного режима для автоматизации:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
|
||||||
|
| sh -s -- --non-interactive --credential-source keychain --configure-keychain
|
||||||
|
```
|
||||||
|
|
||||||
Принудительно включить интерактивные вопросы:
|
Принудительно включить интерактивные вопросы:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -149,17 +166,17 @@ Detected state:
|
|||||||
Keychain TOTP seed: no
|
Keychain TOTP seed: no
|
||||||
```
|
```
|
||||||
|
|
||||||
Если доступен терминал (`/dev/tty`), скрипт спросит только по тому, чего не хватает:
|
Если доступен терминал, скрипт ведёт установку вопросами:
|
||||||
|
|
||||||
- поставить ли Bitwarden CLI, если `bw` не найден;
|
- выбрать credential source: Bitwarden sync, Keychain с вводом credentials сейчас, или Keychain с настройкой позже;
|
||||||
- собрать ли Touch ID helper, если его нет и Bitwarden включён;
|
- поставить ли Bitwarden CLI, если выбран Bitwarden и `bw` не найден;
|
||||||
|
- собрать ли Touch ID helper, если выбран Bitwarden и helper не найден;
|
||||||
- собрать ли Swift Menu Bar app, если `~/Applications/LemanaVPN.app` не найден;
|
- собрать ли Swift Menu Bar app, если `~/Applications/LemanaVPN.app` не найден;
|
||||||
- включить ли автозапуск Menu Bar app при логине;
|
- включить ли автозапуск Menu Bar app при логине;
|
||||||
- настроить ли sudoers для `openconnect` и DNS cleanup;
|
- настроить ли sudoers для `openconnect` и DNS cleanup;
|
||||||
- добавить ли алиасы в `~/.zshrc`;
|
- добавить ли алиасы в `~/.zshrc`.
|
||||||
- записать ли LDAP-пароль и TOTP seed в Keychain, если Bitwarden отключён.
|
|
||||||
|
|
||||||
Флаги имеют приоритет над вопросами. Например, `--without-bitwarden` не будет спрашивать про Bitwarden, а `--no-shell` не будет предлагать алиасы.
|
Флаги имеют приоритет над вопросами и нужны в основном для CI, диагностики или повторяемых unattended installs. Например, `--credential-source keychain --configure-keychain` сразу выберет Keychain-only flow, а `--no-shell` не будет предлагать алиасы.
|
||||||
|
|
||||||
В неинтерактивной среде скрипт не задаёт вопросов и использует выбранные флаги/дефолты. Для CI или повторяемой установки лучше явно указывать `--non-interactive`.
|
В неинтерактивной среде скрипт не задаёт вопросов и использует выбранные флаги/дефолты. Для CI или повторяемой установки лучше явно указывать `--non-interactive`.
|
||||||
|
|
||||||
@@ -194,6 +211,27 @@ LEMANA_VPN_NO_EMOJI=1 sh uninstall.sh
|
|||||||
- `openconnect-lite` config;
|
- `openconnect-lite` config;
|
||||||
- DNS cleanup wrapper.
|
- DNS cleanup wrapper.
|
||||||
|
|
||||||
|
### Как работают credential sources
|
||||||
|
|
||||||
|
У Lemana VPN есть два разных способа подготовить credentials, но один общий runtime-контракт: перед запуском SSO в macOS Keychain должны лежать LDAP-пароль и постоянный TOTP seed для `openconnect-lite`.
|
||||||
|
|
||||||
|
Keychain entries:
|
||||||
|
|
||||||
|
- service `openconnect-lite`, account `<LDAP username>` — корпоративный LDAP/domain пароль;
|
||||||
|
- service `openconnect-lite`, account `totp/<LDAP username>` — постоянный TOTP seed.
|
||||||
|
|
||||||
|
`credential_source=bitwarden` — это sync-режим. CLI открывает Bitwarden vault, читает item `LM LDAP`, берёт из него LDAP password и TOTP seed, нормализует `otpauth://...secret=...` если нужно, затем записывает оба секрета в macOS Keychain. После этого подключение идёт так же, как в остальных режимах: `openconnect-lite` читает данные из Keychain, генерирует текущий одноразовый TOTP-код из seed и заполняет Keycloak форму.
|
||||||
|
|
||||||
|
`credential_source=keychain` — это бесплатный built-in режим без Bitwarden. Пользователь один раз запускает `vpn --configure-keychain` или установку с `--credential-source keychain --configure-keychain`, вводит LDAP password и постоянный TOTP seed. CLI сохраняет их напрямую в macOS Keychain и при следующих подключениях не спрашивает Bitwarden, master password или текущий OTP-код.
|
||||||
|
|
||||||
|
Важно: Lemana VPN не хранит и не принимает текущий 6-значный код как постоянную настройку. Такой код живёт около 30 секунд. Для автоматического SSO нужен именно seed: raw Base32 или `otpauth://totp/...?...secret=BASE32`.
|
||||||
|
|
||||||
|
Если запуск идёт из menu-bar app, интерактивного terminal prompt нет. Поэтому при пустом Keychain приложение покажет ошибку, а настройку нужно один раз выполнить в Terminal:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
vpn --configure-keychain
|
||||||
|
```
|
||||||
|
|
||||||
### Bitwarden
|
### Bitwarden
|
||||||
|
|
||||||
Включён по умолчанию. CLI при каждом запуске `vpn` пытается получить LDAP-пароль и TOTP seed из записи Bitwarden `LM LDAP`, затем записывает их в macOS Keychain для `openconnect-lite`.
|
Включён по умолчанию. CLI при каждом запуске `vpn` пытается получить LDAP-пароль и TOTP seed из записи Bitwarden `LM LDAP`, затем записывает их в macOS Keychain для `openconnect-lite`.
|
||||||
@@ -202,10 +240,12 @@ TOTP seed — это постоянный секрет 2FA. Сам однора
|
|||||||
|
|
||||||
Если vault заблокирован и Touch ID helper не смог его открыть, CLI спросит `Bitwarden master password`. Это пароль от хранилища Bitwarden, а не корпоративный LDAP-пароль. Он нужен только чтобы достать LDAP password/TOTP seed из item `LM LDAP` и переложить их в macOS Keychain.
|
Если vault заблокирован и Touch ID helper не смог его открыть, CLI спросит `Bitwarden master password`. Это пароль от хранилища Bitwarden, а не корпоративный LDAP-пароль. Он нужен только чтобы достать LDAP password/TOTP seed из item `LM LDAP` и переложить их в macOS Keychain.
|
||||||
|
|
||||||
Отключить:
|
Выбрать Keychain вместо Bitwarden можно прямо в installer wizard: пункт `2` вводит LDAP password и TOTP seed сразу после установки, пункт `3` оставляет настройку на потом.
|
||||||
|
|
||||||
|
То же самое можно задать флагами для неинтерактивной установки:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sh install.sh --without-bitwarden
|
sh install.sh --non-interactive --credential-source keychain --configure-keychain
|
||||||
```
|
```
|
||||||
|
|
||||||
В этом режиме credentials нужно положить в Keychain вручную:
|
В этом режиме credentials нужно положить в Keychain вручную:
|
||||||
@@ -220,13 +260,20 @@ vpn-lemanapro.sh --configure-keychain
|
|||||||
|
|
||||||
Bitwarden не обязателен. Без него установка работает как обычный `openconnect-lite` profile с секретами в macOS Keychain.
|
Bitwarden не обязателен. Без него установка работает как обычный `openconnect-lite` profile с секретами в macOS Keychain.
|
||||||
|
|
||||||
Установка:
|
Интерактивная установка:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
|
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh | sh
|
||||||
| sh -s -- --without-bitwarden --without-touchid --configure-keychain
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
В первом вопросе выбери пункт `2`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
2) macOS Keychain: ввести LDAP password и TOTP seed сейчас
|
||||||
|
```
|
||||||
|
|
||||||
|
Это встроенный бесплатный путь: не нужен Bitwarden account, платный Bitwarden TOTP или внешний password manager. Setup prompt спросит корпоративный LDAP-пароль и постоянный TOTP seed. Seed можно вставить как raw Base32 или как `otpauth://totp/...?...secret=BASE32` URI.
|
||||||
|
|
||||||
Что понадобится:
|
Что понадобится:
|
||||||
|
|
||||||
- LDAP username;
|
- LDAP username;
|
||||||
@@ -235,6 +282,8 @@ curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
|
|||||||
|
|
||||||
Важно: вводить нужно не текущие 6 цифр из authenticator-приложения, а постоянный secret. Обычно он есть в QR-коде как `secret=BASE32...` или может быть показан при ручной настройке TOTP.
|
Важно: вводить нужно не текущие 6 цифр из authenticator-приложения, а постоянный secret. Обычно он есть в QR-коде как `secret=BASE32...` или может быть показан при ручной настройке TOTP.
|
||||||
|
|
||||||
|
Не вставляй текущий 6-значный authenticator code в `vpn --configure-keychain`. Lemana VPN сохраняет в Keychain постоянный TOTP seed, а `openconnect-lite` по нему генерирует свежие одноразовые коды во время каждого SSO login.
|
||||||
|
|
||||||
Если запуск идёт из `LemanaVPN.app`, приложение не может безопасно показать интерактивный terminal prompt для ввода LDAP/TOTP. Если Keychain пустой, приложение покажет ошибку. В этом случае один раз выполни в Terminal:
|
Если запуск идёт из `LemanaVPN.app`, приложение не может безопасно показать интерактивный terminal prompt для ввода LDAP/TOTP. Если Keychain пустой, приложение покажет ошибку. В этом случае один раз выполни в Terminal:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -335,12 +384,23 @@ open ~/Applications/LemanaVPN.app # открыть Swift-приложение
|
|||||||
|
|
||||||
```sh
|
```sh
|
||||||
LEMANA_VPN_USERNAME="60103293"
|
LEMANA_VPN_USERNAME="60103293"
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE="bitwarden"
|
||||||
LEMANA_VPN_BW_ITEM="LM LDAP"
|
LEMANA_VPN_BW_ITEM="LM LDAP"
|
||||||
LEMANA_VPN_USE_BITWARDEN="1"
|
LEMANA_VPN_USE_BITWARDEN="1"
|
||||||
LEMANA_VPN_USE_TOUCHID="1"
|
LEMANA_VPN_USE_TOUCHID="1"
|
||||||
LEMANA_VPN_DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
|
LEMANA_VPN_DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Для бесплатного Keychain-only источника:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
LEMANA_VPN_USERNAME="60103293"
|
||||||
|
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"
|
||||||
|
```
|
||||||
|
|
||||||
Для другого логина:
|
Для другого логина:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ struct VPNEvent: Decodable {
|
|||||||
var delay: Int?
|
var delay: Int?
|
||||||
var reason: String?
|
var reason: String?
|
||||||
var message: String?
|
var message: String?
|
||||||
|
var source: String?
|
||||||
var tier: Int?
|
var tier: Int?
|
||||||
var modules: ModuleStatus?
|
var modules: ModuleStatus?
|
||||||
}
|
}
|
||||||
@@ -31,6 +32,11 @@ struct ModuleStatus: Decodable {
|
|||||||
var installed: Bool
|
var installed: Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct Credentials: Decodable {
|
||||||
|
var source: String
|
||||||
|
var keychain_ready: Bool
|
||||||
|
}
|
||||||
|
|
||||||
struct Keychain: Decodable {
|
struct Keychain: Decodable {
|
||||||
var password: Bool
|
var password: Bool
|
||||||
var totp_seed: Bool
|
var totp_seed: Bool
|
||||||
@@ -51,6 +57,7 @@ struct ModuleStatus: Decodable {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var core: Core
|
var core: Core
|
||||||
|
var credentials: Credentials?
|
||||||
var bitwarden: ToggleModule
|
var bitwarden: ToggleModule
|
||||||
var touchid: ToggleModule
|
var touchid: ToggleModule
|
||||||
var keychain: Keychain
|
var keychain: Keychain
|
||||||
@@ -80,6 +87,7 @@ struct ModuleStatus: Decodable {
|
|||||||
|
|
||||||
var summary: String {
|
var summary: String {
|
||||||
let coreState = core.openconnect && core.openconnect_lite && core.openconnect_lite_config ? "✅ core" : "⚠️ core"
|
let coreState = core.openconnect && core.openconnect_lite && core.openconnect_lite_config ? "✅ core" : "⚠️ core"
|
||||||
|
let credentialState = credentials.map { "🔐 \($0.source)" } ?? "🔐 legacy"
|
||||||
let bwState = bitwarden.enabled ? (bitwarden.installed ? "✅ bw" : "⚠️ bw") : "⏭️ bw"
|
let bwState = bitwarden.enabled ? (bitwarden.installed ? "✅ bw" : "⚠️ bw") : "⏭️ bw"
|
||||||
let touchState = touchid.enabled ? (touchid.installed ? "✅ touch" : "⚠️ touch") : "⏭️ touch"
|
let touchState = touchid.enabled ? (touchid.installed ? "✅ touch" : "⚠️ touch") : "⏭️ touch"
|
||||||
let dnsState = dns_cleanup.installed ? "✅ dns" : "⚠️ dns"
|
let dnsState = dns_cleanup.installed ? "✅ dns" : "⚠️ dns"
|
||||||
@@ -87,7 +95,7 @@ struct ModuleStatus: Decodable {
|
|||||||
let autostartState = app.map { $0.autostart ? "✅ autostart" : "⏭️ autostart" } ?? "❔ autostart"
|
let autostartState = app.map { $0.autostart ? "✅ autostart" : "⏭️ autostart" } ?? "❔ autostart"
|
||||||
let patchState = patches.active ? "✅ patches" : "⚠️ patches"
|
let patchState = patches.active ? "✅ patches" : "⚠️ patches"
|
||||||
let keychainState = "\(keychain.password && keychain.totp_seed ? "✅" : "⚠️") kc \(keychain.password ? "pass" : "-")/\(keychain.totp_seed ? "totp" : "-")"
|
let keychainState = "\(keychain.password && keychain.totp_seed ? "✅" : "⚠️") kc \(keychain.password ? "pass" : "-")/\(keychain.totp_seed ? "totp" : "-")"
|
||||||
return [coreState, bwState, touchState, dnsState, appState, autostartState, patchState, keychainState].joined(separator: " | ")
|
return [coreState, credentialState, bwState, touchState, dnsState, appState, autostartState, patchState, keychainState].joined(separator: " | ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,6 +374,13 @@ class VPNManager: ObservableObject {
|
|||||||
switch event.event {
|
switch event.event {
|
||||||
case "modules":
|
case "modules":
|
||||||
return
|
return
|
||||||
|
case "credential_source":
|
||||||
|
if let message = event.message {
|
||||||
|
log(" \(message)")
|
||||||
|
} else if let source = event.source {
|
||||||
|
log(" Credential source: \(source)")
|
||||||
|
}
|
||||||
|
return
|
||||||
case "bw_cached":
|
case "bw_cached":
|
||||||
state = .unlocking(tier: "cached")
|
state = .unlocking(tier: "cached")
|
||||||
case "bw_touchid":
|
case "bw_touchid":
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ CONFIG_FILE="$CONFIG_DIR/env"
|
|||||||
_ENV_LEMANA_VPN_USERNAME="${LEMANA_VPN_USERNAME+x}${LEMANA_VPN_USERNAME-}"
|
_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-}"
|
_ENV_LEMANA_VPN_BW_ITEM="${LEMANA_VPN_BW_ITEM+x}${LEMANA_VPN_BW_ITEM-}"
|
||||||
_ENV_LEMANA_VPN_USE_BITWARDEN="${LEMANA_VPN_USE_BITWARDEN+x}${LEMANA_VPN_USE_BITWARDEN-}"
|
_ENV_LEMANA_VPN_USE_BITWARDEN="${LEMANA_VPN_USE_BITWARDEN+x}${LEMANA_VPN_USE_BITWARDEN-}"
|
||||||
|
_ENV_LEMANA_VPN_CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE+x}${LEMANA_VPN_CREDENTIAL_SOURCE-}"
|
||||||
_ENV_LEMANA_VPN_USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID+x}${LEMANA_VPN_USE_TOUCHID-}"
|
_ENV_LEMANA_VPN_USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID+x}${LEMANA_VPN_USE_TOUCHID-}"
|
||||||
_ENV_LEMANA_VPN_DNS_CLEANUP="${LEMANA_VPN_DNS_CLEANUP+x}${LEMANA_VPN_DNS_CLEANUP-}"
|
_ENV_LEMANA_VPN_DNS_CLEANUP="${LEMANA_VPN_DNS_CLEANUP+x}${LEMANA_VPN_DNS_CLEANUP-}"
|
||||||
|
|
||||||
@@ -18,6 +19,7 @@ fi
|
|||||||
[[ "${_ENV_LEMANA_VPN_USERNAME:0:1}" == "x" ]] && LEMANA_VPN_USERNAME="${_ENV_LEMANA_VPN_USERNAME:1}"
|
[[ "${_ENV_LEMANA_VPN_USERNAME:0:1}" == "x" ]] && LEMANA_VPN_USERNAME="${_ENV_LEMANA_VPN_USERNAME:1}"
|
||||||
[[ "${_ENV_LEMANA_VPN_BW_ITEM:0:1}" == "x" ]] && LEMANA_VPN_BW_ITEM="${_ENV_LEMANA_VPN_BW_ITEM:1}"
|
[[ "${_ENV_LEMANA_VPN_BW_ITEM:0:1}" == "x" ]] && LEMANA_VPN_BW_ITEM="${_ENV_LEMANA_VPN_BW_ITEM:1}"
|
||||||
[[ "${_ENV_LEMANA_VPN_USE_BITWARDEN:0:1}" == "x" ]] && LEMANA_VPN_USE_BITWARDEN="${_ENV_LEMANA_VPN_USE_BITWARDEN:1}"
|
[[ "${_ENV_LEMANA_VPN_USE_BITWARDEN:0:1}" == "x" ]] && LEMANA_VPN_USE_BITWARDEN="${_ENV_LEMANA_VPN_USE_BITWARDEN:1}"
|
||||||
|
[[ "${_ENV_LEMANA_VPN_CREDENTIAL_SOURCE:0:1}" == "x" ]] && LEMANA_VPN_CREDENTIAL_SOURCE="${_ENV_LEMANA_VPN_CREDENTIAL_SOURCE:1}"
|
||||||
[[ "${_ENV_LEMANA_VPN_USE_TOUCHID:0:1}" == "x" ]] && LEMANA_VPN_USE_TOUCHID="${_ENV_LEMANA_VPN_USE_TOUCHID:1}"
|
[[ "${_ENV_LEMANA_VPN_USE_TOUCHID:0:1}" == "x" ]] && LEMANA_VPN_USE_TOUCHID="${_ENV_LEMANA_VPN_USE_TOUCHID:1}"
|
||||||
[[ "${_ENV_LEMANA_VPN_DNS_CLEANUP:0:1}" == "x" ]] && LEMANA_VPN_DNS_CLEANUP="${_ENV_LEMANA_VPN_DNS_CLEANUP:1}"
|
[[ "${_ENV_LEMANA_VPN_DNS_CLEANUP:0:1}" == "x" ]] && LEMANA_VPN_DNS_CLEANUP="${_ENV_LEMANA_VPN_DNS_CLEANUP:1}"
|
||||||
|
|
||||||
@@ -27,8 +29,32 @@ OC_BIN="${LEMANA_VPN_OC_BIN:-$HOME/.local/bin/openconnect-lite}"
|
|||||||
BW_ITEM_NAME="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
|
BW_ITEM_NAME="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
|
||||||
KC_USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
|
KC_USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
|
||||||
KC_FP="${LEMANA_VPN_KEYCHAIN_FINGERPRINT:-$HOME/bin/keychain-fingerprint}"
|
KC_FP="${LEMANA_VPN_KEYCHAIN_FINGERPRINT:-$HOME/bin/keychain-fingerprint}"
|
||||||
USE_BITWARDEN="${LEMANA_VPN_USE_BITWARDEN:-1}"
|
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-}"
|
||||||
|
if [[ -z "$CREDENTIAL_SOURCE" ]]; then
|
||||||
|
if [[ "${LEMANA_VPN_USE_BITWARDEN:-1}" == "1" ]]; then
|
||||||
|
CREDENTIAL_SOURCE="bitwarden"
|
||||||
|
else
|
||||||
|
CREDENTIAL_SOURCE="keychain"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$CREDENTIAL_SOURCE" in
|
||||||
|
bitwarden|keychain) ;;
|
||||||
|
*)
|
||||||
|
printf 'Unknown credential source: %s. Use bitwarden or keychain.\n' "$CREDENTIAL_SOURCE" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
|
||||||
|
USE_BITWARDEN="1"
|
||||||
|
else
|
||||||
|
USE_BITWARDEN="0"
|
||||||
|
fi
|
||||||
USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID:-1}"
|
USE_TOUCHID="${LEMANA_VPN_USE_TOUCHID:-1}"
|
||||||
|
if [[ "$CREDENTIAL_SOURCE" == "keychain" ]]; then
|
||||||
|
USE_TOUCHID="0"
|
||||||
|
fi
|
||||||
CACHE_BW_SESSION="${LEMANA_VPN_CACHE_BW_SESSION:-0}"
|
CACHE_BW_SESSION="${LEMANA_VPN_CACHE_BW_SESSION:-0}"
|
||||||
DNS_CLEANUP="${LEMANA_VPN_DNS_CLEANUP:-/usr/local/sbin/lemana-vpn-dns-cleanup}"
|
DNS_CLEANUP="${LEMANA_VPN_DNS_CLEANUP:-/usr/local/sbin/lemana-vpn-dns-cleanup}"
|
||||||
APP_DIR="${LEMANA_VPN_APP_DIR:-$HOME/Applications/LemanaVPN.app}"
|
APP_DIR="${LEMANA_VPN_APP_DIR:-$HOME/Applications/LemanaVPN.app}"
|
||||||
@@ -70,7 +96,7 @@ Usage: vpn-lemanapro.sh [--auto|--manual] [--debug] [--json] [--status] [--confi
|
|||||||
--manual-sso Compatibility alias for --manual
|
--manual-sso Compatibility alias for --manual
|
||||||
--debug Passthrough debug logs; also shows browser in auto mode
|
--debug Passthrough debug logs; also shows browser in auto mode
|
||||||
--json Emit JSON Lines events for UI wrappers
|
--json Emit JSON Lines events for UI wrappers
|
||||||
--configure-keychain Prompt for LDAP password and TOTP secret, then save them to Keychain
|
--configure-keychain Configure the keychain credential source: LDAP password plus permanent TOTP seed or otpauth:// URI
|
||||||
--patch-only Apply openconnect-lite runtime patches and exit
|
--patch-only Apply openconnect-lite runtime patches and exit
|
||||||
HELP
|
HELP
|
||||||
exit 0
|
exit 0
|
||||||
@@ -196,6 +222,7 @@ _module_status_json() {
|
|||||||
local openconnect_installed openconnect_lite_installed bitwarden_installed touchid_installed dns_cleanup_installed
|
local openconnect_installed openconnect_lite_installed bitwarden_installed touchid_installed dns_cleanup_installed
|
||||||
local app_installed app_autostart
|
local app_installed app_autostart
|
||||||
local config_present oc_config_present patch_backup_present patches_active keychain_password keychain_totp_seed
|
local config_present oc_config_present patch_backup_present patches_active keychain_password keychain_totp_seed
|
||||||
|
local credential_keychain_ready
|
||||||
openconnect_installed="$(_module_bool command -v openconnect)"
|
openconnect_installed="$(_module_bool command -v openconnect)"
|
||||||
openconnect_lite_installed="$(_module_bool test -x "$OC_BIN")"
|
openconnect_lite_installed="$(_module_bool test -x "$OC_BIN")"
|
||||||
bitwarden_installed="$(_module_bool command -v bw)"
|
bitwarden_installed="$(_module_bool command -v bw)"
|
||||||
@@ -209,12 +236,15 @@ _module_status_json() {
|
|||||||
patches_active="$(_module_bool _patches_active)"
|
patches_active="$(_module_bool _patches_active)"
|
||||||
keychain_password="$(_module_bool _keychain_has openconnect-lite "$KC_USERNAME")"
|
keychain_password="$(_module_bool _keychain_has openconnect-lite "$KC_USERNAME")"
|
||||||
keychain_totp_seed="$(_module_bool _keychain_has openconnect-lite "totp/$KC_USERNAME")"
|
keychain_totp_seed="$(_module_bool _keychain_has openconnect-lite "totp/$KC_USERNAME")"
|
||||||
|
credential_keychain_ready="$([[ "$keychain_password" == "true" && "$keychain_totp_seed" == "true" ]] && printf true || printf false)"
|
||||||
|
|
||||||
printf '{"core":{"openconnect":%s,"openconnect_lite":%s,"config":%s,"openconnect_lite_config":%s},"bitwarden":{"enabled":%s,"installed":%s,"item":"%s"},"touchid":{"enabled":%s,"installed":%s},"keychain":{"password":%s,"totp_seed":%s},"dns_cleanup":{"installed":%s},"patches":{"active":%s,"backup":%s},"app":{"installed":%s,"autostart":%s}}' \
|
printf '{"core":{"openconnect":%s,"openconnect_lite":%s,"config":%s,"openconnect_lite_config":%s},"credentials":{"source":"%s","keychain_ready":%s},"bitwarden":{"enabled":%s,"installed":%s,"item":"%s"},"touchid":{"enabled":%s,"installed":%s},"keychain":{"password":%s,"totp_seed":%s},"dns_cleanup":{"installed":%s},"patches":{"active":%s,"backup":%s},"app":{"installed":%s,"autostart":%s}}' \
|
||||||
"$openconnect_installed" \
|
"$openconnect_installed" \
|
||||||
"$openconnect_lite_installed" \
|
"$openconnect_lite_installed" \
|
||||||
"$config_present" \
|
"$config_present" \
|
||||||
"$oc_config_present" \
|
"$oc_config_present" \
|
||||||
|
"$CREDENTIAL_SOURCE" \
|
||||||
|
"$credential_keychain_ready" \
|
||||||
"$([[ "$USE_BITWARDEN" == "1" ]] && printf true || printf false)" \
|
"$([[ "$USE_BITWARDEN" == "1" ]] && printf true || printf false)" \
|
||||||
"$bitwarden_installed" \
|
"$bitwarden_installed" \
|
||||||
"$BW_ITEM_NAME" \
|
"$BW_ITEM_NAME" \
|
||||||
@@ -258,7 +288,7 @@ _module_status_human() {
|
|||||||
app_installed="$(_module_bool test -x "$APP_DIR/Contents/MacOS/LemanaVPN")"
|
app_installed="$(_module_bool test -x "$APP_DIR/Contents/MacOS/LemanaVPN")"
|
||||||
app_autostart="$(_module_bool test -f "$LAUNCH_AGENT")"
|
app_autostart="$(_module_bool test -f "$LAUNCH_AGENT")"
|
||||||
|
|
||||||
printf 'Modules: %s %s, ' "$([[ "$core" == "core=ok" ]] && printf '✅' || printf '⚠️')" "$core"
|
printf 'Modules: %s %s, 🔐 credential_source=%s, ' "$([[ "$core" == "core=ok" ]] && printf '✅' || printf '⚠️')" "$core" "$CREDENTIAL_SOURCE"
|
||||||
_module_human_part "bitwarden" "$USE_BITWARDEN" "$bitwarden_installed"
|
_module_human_part "bitwarden" "$USE_BITWARDEN" "$bitwarden_installed"
|
||||||
printf ', '
|
printf ', '
|
||||||
_module_human_part "touchid" "$USE_TOUCHID" "$touchid_installed"
|
_module_human_part "touchid" "$USE_TOUCHID" "$touchid_installed"
|
||||||
@@ -586,6 +616,34 @@ if totp_secret:
|
|||||||
PY
|
PY
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_normalize_totp_secret() {
|
||||||
|
_VPN_TOTP_INPUT="$1" python3 - <<'PY'
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
value = os.environ.get("_VPN_TOTP_INPUT", "").strip()
|
||||||
|
|
||||||
|
if value.lower().startswith("otpauth://"):
|
||||||
|
parsed = urllib.parse.urlparse(value)
|
||||||
|
query = urllib.parse.parse_qs(parsed.query)
|
||||||
|
value = query.get("secret", [""])[0]
|
||||||
|
|
||||||
|
value = re.sub(r"[\s-]+", "", value).upper()
|
||||||
|
|
||||||
|
if not value:
|
||||||
|
print("", end="")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if not re.fullmatch(r"[A-Z2-7]+=*", value):
|
||||||
|
print("Invalid TOTP seed. Use a BASE32 secret or an otpauth:// URI with secret=BASE32.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(value)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
_can_prompt() {
|
_can_prompt() {
|
||||||
[[ -t 0 ]]
|
[[ -t 0 ]]
|
||||||
}
|
}
|
||||||
@@ -624,6 +682,10 @@ _configure_keychain() {
|
|||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if [[ -n "$totp_secret" ]]; then
|
||||||
|
totp_secret="$(_normalize_totp_secret "$totp_secret")"
|
||||||
|
fi
|
||||||
|
|
||||||
_store_keychain "$password" "$totp_secret"
|
_store_keychain "$password" "$totp_secret"
|
||||||
printf 'Credentials are ready in macOS Keychain for openconnect-lite/%s.\n' "$KC_USERNAME"
|
printf 'Credentials are ready in macOS Keychain for openconnect-lite/%s.\n' "$KC_USERNAME"
|
||||||
}
|
}
|
||||||
@@ -634,18 +696,18 @@ _ensure_keychain_credentials() {
|
|||||||
_keychain_has openconnect-lite "totp/$KC_USERNAME" && totp_present=true
|
_keychain_has openconnect-lite "totp/$KC_USERNAME" && totp_present=true
|
||||||
|
|
||||||
if [[ "$password_present" == "true" && "$totp_present" == "true" ]]; then
|
if [[ "$password_present" == "true" && "$totp_present" == "true" ]]; then
|
||||||
if [[ "$USE_BITWARDEN" == "1" ]]; then
|
if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
|
||||||
_emit '{"event":"keychain_ready","source":"keychain"}' "LDAP credentials are ready in macOS Keychain for $KC_USERNAME."
|
_emit '{"event":"keychain_ready","source":"bitwarden"}' "Bitwarden source synced LDAP credentials into macOS Keychain for $KC_USERNAME."
|
||||||
else
|
else
|
||||||
_emit '{"event":"keychain_ready","source":"keychain","bitwarden":false}' "Bitwarden is disabled. Using saved LDAP password and TOTP seed from macOS Keychain for $KC_USERNAME."
|
_emit '{"event":"keychain_ready","source":"keychain"}' "Keychain source is ready: saved LDAP password and TOTP seed are available for $KC_USERNAME."
|
||||||
fi
|
fi
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$USE_BITWARDEN" == "1" ]]; then
|
if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
|
||||||
_emit '{"event":"keychain_required","bitwarden":true}' "Bitwarden sync did not produce complete Keychain credentials."
|
_emit '{"event":"keychain_required","source":"bitwarden"}' "Bitwarden source did not produce complete Keychain credentials."
|
||||||
else
|
else
|
||||||
_emit '{"event":"keychain_required","bitwarden":false}' "Bitwarden is disabled and saved LDAP credentials are incomplete."
|
_emit '{"event":"keychain_required","source":"keychain"}' "Keychain source is selected and saved LDAP credentials are incomplete."
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! _can_prompt; then
|
if ! _can_prompt; then
|
||||||
@@ -759,6 +821,9 @@ except Exception:
|
|||||||
')"
|
')"
|
||||||
|
|
||||||
if [[ -n "$bw_password" ]]; then
|
if [[ -n "$bw_password" ]]; then
|
||||||
|
if [[ -n "$bw_totp_secret" ]]; then
|
||||||
|
bw_totp_secret="$(_normalize_totp_secret "$bw_totp_secret")"
|
||||||
|
fi
|
||||||
_store_keychain "$bw_password" "$bw_totp_secret"
|
_store_keychain "$bw_password" "$bw_totp_secret"
|
||||||
_emit '{"event":"bw_synced"}' "Credentials synced from Bitwarden to Keychain"
|
_emit '{"event":"bw_synced"}' "Credentials synced from Bitwarden to Keychain"
|
||||||
else
|
else
|
||||||
@@ -766,6 +831,17 @@ except Exception:
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_sync_credentials() {
|
||||||
|
case "$CREDENTIAL_SOURCE" in
|
||||||
|
bitwarden)
|
||||||
|
_sync_bitwarden
|
||||||
|
;;
|
||||||
|
keychain)
|
||||||
|
_emit '{"event":"credential_source","source":"keychain"}' "Credential source: macOS Keychain"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
_dns_cleanup() {
|
_dns_cleanup() {
|
||||||
_emit '{"event":"dns_cleanup"}' "Cleaning up VPN DNS..."
|
_emit '{"event":"dns_cleanup"}' "Cleaning up VPN DNS..."
|
||||||
if [[ -x "$DNS_CLEANUP" ]]; then
|
if [[ -x "$DNS_CLEANUP" ]]; then
|
||||||
@@ -857,7 +933,7 @@ else
|
|||||||
printf '{"event":"modules","modules":%s}\n' "$(_module_status_json)"
|
printf '{"event":"modules","modules":%s}\n' "$(_module_status_json)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
_sync_bitwarden
|
_sync_credentials
|
||||||
_ensure_keychain_credentials
|
_ensure_keychain_credentials
|
||||||
_patch_oc
|
_patch_oc
|
||||||
|
|
||||||
|
|||||||
935
docs/superpowers/plans/2026-05-24-keychain-totp-provider.md
Normal file
935
docs/superpowers/plans/2026-05-24-keychain-totp-provider.md
Normal file
@@ -0,0 +1,935 @@
|
|||||||
|
# Keychain TOTP Provider Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Add a free, non-Bitwarden credential path where the user stores LDAP password plus a permanent TOTP seed in macOS Keychain, and `openconnect-lite` keeps generating one-time login codes from that seed.
|
||||||
|
|
||||||
|
**Architecture:** Introduce an explicit credential-source layer with two first-class sources: `bitwarden` and `keychain`. `bitwarden` keeps the existing vault-sync behavior and writes LDAP password plus TOTP seed into the existing `openconnect-lite` Keychain entries; `keychain` skips vault sync and treats those Keychain entries as the source of truth. Both sources converge before the current `openconnect-lite` SSO/autofill flow, so runtime patches and Keycloak selectors stay unchanged.
|
||||||
|
|
||||||
|
**Tech Stack:** Bash (`bin/vpn-lemanapro.sh`), POSIX shell installer (`install.sh`), macOS Keychain via Python `keyring` inside the `openconnect-lite` venv, Swift menu-bar app JSON decoding, `tests/smoke.sh`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
- Modify `bin/vpn-lemanapro.sh`
|
||||||
|
- Own runtime credential-source selection.
|
||||||
|
- Normalize raw Base32 and `otpauth://` TOTP inputs.
|
||||||
|
- Split source orchestration from source-specific sync.
|
||||||
|
- Keep the existing Keychain service/accounts: `openconnect-lite` / `<username>` and `openconnect-lite` / `totp/<username>`.
|
||||||
|
- Modify `install.sh`
|
||||||
|
- Add `LEMANA_VPN_CREDENTIAL_SOURCE`.
|
||||||
|
- Keep `--with-bitwarden` and `--without-bitwarden` as backward-compatible aliases.
|
||||||
|
- Add a direct `--credential-source bitwarden|keychain` flag.
|
||||||
|
- Disable Touch ID automatically for the `keychain` source because Touch ID currently gates Bitwarden master-password retrieval only.
|
||||||
|
- Modify `tests/smoke.sh`
|
||||||
|
- Assert installer and runtime source separation.
|
||||||
|
- Assert `otpauth://` input is normalized before storing.
|
||||||
|
- Assert no-tty app path still fails with the terminal recovery command when Keychain credentials are missing.
|
||||||
|
- Modify `app/Sources/LemanaVPN/VPNManager.swift`
|
||||||
|
- Decode the new `credentials` module object.
|
||||||
|
- Keep compatibility with older CLI JSON by making the new object optional.
|
||||||
|
- Log source-specific credential events without changing connection state semantics.
|
||||||
|
- Modify `README.md`
|
||||||
|
- Document the two credential sources and the free Keychain/TOTP-seed setup.
|
||||||
|
- Keep update/run/check instructions aligned with `AGENTS.md`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: Add Smoke Tests For Credential Source Separation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/smoke.sh`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing tests**
|
||||||
|
|
||||||
|
Edit `tests/smoke.sh` so the initial dry-run expectations and missing-credential check cover the new `keychain` source. Replace:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
printf '%s\n' "$output" | grep -q 'Modules: bitwarden=0 touchid=0 sudoers=1 shell=1 app=1 autostart=1'
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
printf '%s\n' "$output" | grep -q 'Modules: credential_source=keychain bitwarden=0 touchid=0 sudoers=1 shell=1 app=1 autostart=1'
|
||||||
|
```
|
||||||
|
|
||||||
|
After the existing status JSON assertions:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
status_json="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
|
||||||
|
printf '%s\n' "$status_json" | grep -q '"modules":'
|
||||||
|
printf '%s\n' "$status_json" | grep -q '"app":'
|
||||||
|
```
|
||||||
|
|
||||||
|
insert:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
keychain_status_json="$(LEMANA_VPN_CREDENTIAL_SOURCE=keychain bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
|
||||||
|
printf '%s\n' "$keychain_status_json" | grep -q '"credentials":{"source":"keychain","keychain_ready":false}'
|
||||||
|
printf '%s\n' "$keychain_status_json" | grep -q '"bitwarden":{"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'
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the missing-credentials environment:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
LEMANA_VPN_USE_BITWARDEN=0 \
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
|
```
|
||||||
|
|
||||||
|
After the missing-credentials block, add an `otpauth://` normalization check:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
fake_oc_python="$TMP_DIR/fake-oc-python"
|
||||||
|
captured_totp="$TMP_DIR/captured-totp"
|
||||||
|
cat > "$fake_oc_python" <<'SH'
|
||||||
|
#!/bin/sh
|
||||||
|
cat >/dev/null
|
||||||
|
printf '%s\n' "${_VPN_TOTP_SECRET:-}" > "$LEMANA_VPN_CAPTURE_TOTP"
|
||||||
|
SH
|
||||||
|
chmod +x "$fake_oc_python"
|
||||||
|
|
||||||
|
configure_output="$(
|
||||||
|
printf 'ldap-password\notpauth://totp/Lemana:test?secret=abcd2345efgh6723&issuer=Lemana\n' |
|
||||||
|
HOME="$HOME" \
|
||||||
|
LEMANA_VPN_USERNAME="lemana-configure-$$" \
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
|
LEMANA_VPN_OC_PYTHON="$fake_oc_python" \
|
||||||
|
LEMANA_VPN_CAPTURE_TOTP="$captured_totp" \
|
||||||
|
bash "$ROOT/bin/vpn-lemanapro.sh" --configure-keychain
|
||||||
|
)"
|
||||||
|
|
||||||
|
printf '%s\n' "$configure_output" | grep -q 'Credentials are ready in macOS Keychain'
|
||||||
|
grep -q '^ABCD2345EFGH6723$' "$captured_totp"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL because current installer output does not print `credential_source=keychain`, current status JSON has no `"credentials"` object, and `--configure-keychain` stores the raw `otpauth://` URI instead of the extracted Base32 secret.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit the failing tests**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add tests/smoke.sh
|
||||||
|
git commit -m "test: cover keychain credential source"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: Add Runtime Credential Source And TOTP Normalization
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `bin/vpn-lemanapro.sh`
|
||||||
|
- Test: `tests/smoke.sh`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Capture and restore the new environment variable**
|
||||||
|
|
||||||
|
In `bin/vpn-lemanapro.sh`, add this line after `_ENV_LEMANA_VPN_USE_BITWARDEN=...`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
_ENV_LEMANA_VPN_CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE+x}${LEMANA_VPN_CREDENTIAL_SOURCE-}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this line after the existing `LEMANA_VPN_USE_BITWARDEN` restore line:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
[[ "${_ENV_LEMANA_VPN_CREDENTIAL_SOURCE:0:1}" == "x" ]] && LEMANA_VPN_CREDENTIAL_SOURCE="${_ENV_LEMANA_VPN_CREDENTIAL_SOURCE:1}"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Replace implicit Bitwarden mode with explicit credential source**
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
USE_BITWARDEN="${LEMANA_VPN_USE_BITWARDEN:-1}"
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-}"
|
||||||
|
if [[ -z "$CREDENTIAL_SOURCE" ]]; then
|
||||||
|
if [[ "${LEMANA_VPN_USE_BITWARDEN:-1}" == "1" ]]; then
|
||||||
|
CREDENTIAL_SOURCE="bitwarden"
|
||||||
|
else
|
||||||
|
CREDENTIAL_SOURCE="keychain"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "$CREDENTIAL_SOURCE" in
|
||||||
|
bitwarden|keychain) ;;
|
||||||
|
*)
|
||||||
|
printf 'Unknown credential source: %s. Use bitwarden or keychain.\n' "$CREDENTIAL_SOURCE" >&2
|
||||||
|
exit 2
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
|
||||||
|
USE_BITWARDEN="1"
|
||||||
|
else
|
||||||
|
USE_BITWARDEN="0"
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add a TOTP normalizer**
|
||||||
|
|
||||||
|
Insert this function immediately before `_store_keychain()`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
_normalize_totp_secret() {
|
||||||
|
_VPN_TOTP_INPUT="$1" python3 - <<'PY'
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
value = os.environ.get("_VPN_TOTP_INPUT", "").strip()
|
||||||
|
|
||||||
|
if value.lower().startswith("otpauth://"):
|
||||||
|
parsed = urllib.parse.urlparse(value)
|
||||||
|
query = urllib.parse.parse_qs(parsed.query)
|
||||||
|
value = query.get("secret", [""])[0]
|
||||||
|
|
||||||
|
value = re.sub(r"[\s-]+", "", value).upper()
|
||||||
|
|
||||||
|
if not value:
|
||||||
|
print("", end="")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
if not re.fullmatch(r"[A-Z2-7]+=*", value):
|
||||||
|
print("Invalid TOTP seed. Use a BASE32 secret or an otpauth:// URI with secret=BASE32.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(value)
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Normalize manual setup before storing**
|
||||||
|
|
||||||
|
In `_configure_keychain()`, replace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
_store_keychain "$password" "$totp_secret"
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [[ -n "$totp_secret" ]]; then
|
||||||
|
totp_secret="$(_normalize_totp_secret "$totp_secret")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
_store_keychain "$password" "$totp_secret"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Normalize Bitwarden TOTP before storing**
|
||||||
|
|
||||||
|
In `_sync_bitwarden()`, replace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
_store_keychain "$bw_password" "$bw_totp_secret"
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [[ -n "$bw_totp_secret" ]]; then
|
||||||
|
bw_totp_secret="$(_normalize_totp_secret "$bw_totp_secret")"
|
||||||
|
fi
|
||||||
|
_store_keychain "$bw_password" "$bw_totp_secret"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Update status JSON**
|
||||||
|
|
||||||
|
In `_module_status_json()`, add this local variable line after the existing `keychain_*` locals:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
local credential_keychain_ready
|
||||||
|
```
|
||||||
|
|
||||||
|
After `keychain_totp_seed=...`, add:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
credential_keychain_ready="$([[ "$keychain_password" == "true" && "$keychain_totp_seed" == "true" ]] && printf true || printf false)"
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the `printf` format string:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf '{"core":{"openconnect":%s,"openconnect_lite":%s,"config":%s,"openconnect_lite_config":%s},"bitwarden":{"enabled":%s,"installed":%s,"item":"%s"},"touchid":{"enabled":%s,"installed":%s},"keychain":{"password":%s,"totp_seed":%s},"dns_cleanup":{"installed":%s},"patches":{"active":%s,"backup":%s},"app":{"installed":%s,"autostart":%s}}' \
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf '{"core":{"openconnect":%s,"openconnect_lite":%s,"config":%s,"openconnect_lite_config":%s},"credentials":{"source":"%s","keychain_ready":%s},"bitwarden":{"enabled":%s,"installed":%s,"item":"%s"},"touchid":{"enabled":%s,"installed":%s},"keychain":{"password":%s,"totp_seed":%s},"dns_cleanup":{"installed":%s},"patches":{"active":%s,"backup":%s},"app":{"installed":%s,"autostart":%s}}' \
|
||||||
|
```
|
||||||
|
|
||||||
|
Add these arguments immediately after `"$oc_config_present" \`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
"$CREDENTIAL_SOURCE" \
|
||||||
|
"$credential_keychain_ready" \
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Update human status**
|
||||||
|
|
||||||
|
In `_module_status_human()`, change the prefix:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf 'Modules: %s %s, ' "$([[ "$core" == "core=ok" ]] && printf '✅' || printf '⚠️')" "$core"
|
||||||
|
```
|
||||||
|
|
||||||
|
to:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
printf 'Modules: %s %s, 🔐 credential_source=%s, ' "$([[ "$core" == "core=ok" ]] && printf '✅' || printf '⚠️')" "$core" "$CREDENTIAL_SOURCE"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 8: Run smoke test**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: still FAIL because source orchestration and installer output are not separated yet, but the `otpauth://` normalization assertion now passes.
|
||||||
|
|
||||||
|
- [ ] **Step 9: Commit runtime model**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add bin/vpn-lemanapro.sh tests/smoke.sh
|
||||||
|
git commit -m "feat: add credential source runtime model"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: Split Bitwarden Sync From Keychain Source Flow
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `bin/vpn-lemanapro.sh`
|
||||||
|
- Test: `tests/smoke.sh`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace `_ensure_keychain_credentials()` messages**
|
||||||
|
|
||||||
|
Inside `_ensure_keychain_credentials()`, replace the ready branch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [[ "$password_present" == "true" && "$totp_present" == "true" ]]; then
|
||||||
|
if [[ "$USE_BITWARDEN" == "1" ]]; then
|
||||||
|
_emit '{"event":"keychain_ready","source":"keychain"}' "LDAP credentials are ready in macOS Keychain for $KC_USERNAME."
|
||||||
|
else
|
||||||
|
_emit '{"event":"keychain_ready","source":"keychain","bitwarden":false}' "Bitwarden is disabled. Using saved LDAP password and TOTP seed from macOS Keychain for $KC_USERNAME."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [[ "$password_present" == "true" && "$totp_present" == "true" ]]; then
|
||||||
|
if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
|
||||||
|
_emit '{"event":"keychain_ready","source":"bitwarden"}' "Bitwarden source synced LDAP credentials into macOS Keychain for $KC_USERNAME."
|
||||||
|
else
|
||||||
|
_emit '{"event":"keychain_ready","source":"keychain"}' "Keychain source is ready: saved LDAP password and TOTP seed are available for $KC_USERNAME."
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the required branch:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [[ "$USE_BITWARDEN" == "1" ]]; then
|
||||||
|
_emit '{"event":"keychain_required","bitwarden":true}' "Bitwarden sync did not produce complete Keychain credentials."
|
||||||
|
else
|
||||||
|
_emit '{"event":"keychain_required","bitwarden":false}' "Bitwarden is disabled and saved LDAP credentials are incomplete."
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
if [[ "$CREDENTIAL_SOURCE" == "bitwarden" ]]; then
|
||||||
|
_emit '{"event":"keychain_required","source":"bitwarden"}' "Bitwarden source did not produce complete Keychain credentials."
|
||||||
|
else
|
||||||
|
_emit '{"event":"keychain_required","source":"keychain"}' "Keychain source is selected and saved LDAP credentials are incomplete."
|
||||||
|
fi
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add source orchestration**
|
||||||
|
|
||||||
|
Insert this function after `_sync_bitwarden()`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
_sync_credentials() {
|
||||||
|
case "$CREDENTIAL_SOURCE" in
|
||||||
|
bitwarden)
|
||||||
|
_sync_bitwarden
|
||||||
|
;;
|
||||||
|
keychain)
|
||||||
|
_emit '{"event":"credential_source","source":"keychain"}' "Credential source: macOS Keychain"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Use the source orchestrator**
|
||||||
|
|
||||||
|
Near the bottom of the script, replace:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
_sync_bitwarden
|
||||||
|
_ensure_keychain_credentials
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
_sync_credentials
|
||||||
|
_ensure_keychain_credentials
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Update `--help` text**
|
||||||
|
|
||||||
|
Replace the usage line:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: vpn-lemanapro.sh [--auto|--manual] [--debug] [--json] [--status] [--configure-keychain] [--patch-only]
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Usage: vpn-lemanapro.sh [--auto|--manual] [--debug] [--json] [--status] [--configure-keychain] [--patch-only]
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep the usage line unchanged, and replace the configure text:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--configure-keychain Prompt for LDAP password and TOTP secret, then save them to Keychain
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--configure-keychain Configure the keychain credential source: LDAP password plus permanent TOTP seed or otpauth:// URI
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run smoke test**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL only on installer output and Swift/README checks that have not been updated yet. Runtime JSON checks for `LEMANA_VPN_CREDENTIAL_SOURCE=keychain` and `bitwarden` pass.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Commit provider split**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add bin/vpn-lemanapro.sh tests/smoke.sh
|
||||||
|
git commit -m "feat: split credential sources"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: Update Installer Flags And Config
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `install.sh`
|
||||||
|
- Test: `tests/smoke.sh`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add installer state**
|
||||||
|
|
||||||
|
In `install.sh`, add this variable after `BW_ITEM=...`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-bitwarden}"
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this variable after `BITWARDEN_FORCED=0`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
CREDENTIAL_SOURCE_FORCED=0
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Extend usage**
|
||||||
|
|
||||||
|
Add this option after `--without-bitwarden`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
--credential-source VALUE Credential source: bitwarden or keychain
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this example after `sh install.sh --minimal --configure-keychain`:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sh install.sh --credential-source keychain --configure-keychain
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Parse `--credential-source`**
|
||||||
|
|
||||||
|
Add this case arm before `--with-touchid)`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
--credential-source)
|
||||||
|
shift
|
||||||
|
[ "$#" -gt 0 ] || { echo "--credential-source requires bitwarden or keychain" >&2; exit 1; }
|
||||||
|
CREDENTIAL_SOURCE="$1"
|
||||||
|
CREDENTIAL_SOURCE_FORCED=1
|
||||||
|
case "$CREDENTIAL_SOURCE" in
|
||||||
|
bitwarden)
|
||||||
|
USE_BITWARDEN=1
|
||||||
|
;;
|
||||||
|
keychain)
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "--credential-source requires bitwarden or keychain" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
;;
|
||||||
|
```
|
||||||
|
|
||||||
|
In the `--with-bitwarden)` arm, add:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
CREDENTIAL_SOURCE="bitwarden"
|
||||||
|
```
|
||||||
|
|
||||||
|
In the `--without-bitwarden)` arm, add:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
CREDENTIAL_SOURCE="keychain"
|
||||||
|
USE_TOUCHID=0
|
||||||
|
```
|
||||||
|
|
||||||
|
In the `--minimal)` arm, add:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
CREDENTIAL_SOURCE="keychain"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Keep module decisions consistent**
|
||||||
|
|
||||||
|
At the start of `choose_modules()`, immediately after `print_detected_state`, add:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
case "$CREDENTIAL_SOURCE" in
|
||||||
|
bitwarden)
|
||||||
|
USE_BITWARDEN=1
|
||||||
|
;;
|
||||||
|
keychain)
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "Unknown credential source: $CREDENTIAL_SOURCE"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
if [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
if [ "$CREDENTIAL_SOURCE" = "bitwarden" ] && [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$USE_BITWARDEN" -eq 0 ]; then
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$CREDENTIAL_SOURCE" = "keychain" ]; then
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Write source to config**
|
||||||
|
|
||||||
|
In `install_config()`, change `env_content` to:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
env_content="LEMANA_VPN_USERNAME=\"$USERNAME\"
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=\"$CREDENTIAL_SOURCE\"
|
||||||
|
LEMANA_VPN_BW_ITEM=\"$BW_ITEM\"
|
||||||
|
LEMANA_VPN_USE_BITWARDEN=\"$USE_BITWARDEN\"
|
||||||
|
LEMANA_VPN_USE_TOUCHID=\"$USE_TOUCHID\"
|
||||||
|
LEMANA_VPN_DNS_CLEANUP=\"$DNS_CLEANUP\""
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Update installer logging**
|
||||||
|
|
||||||
|
Replace:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
log_info "Modules: bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
log_info "Modules: credential_source=$CREDENTIAL_SOURCE bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
|
||||||
|
```
|
||||||
|
|
||||||
|
In `maybe_login_bitwarden()`, replace:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
if [ "$USE_BITWARDEN" -ne 1 ]; then
|
||||||
|
log_skip "Bitwarden module disabled; credentials будут браться из macOS Keychain."
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
if [ "$CREDENTIAL_SOURCE" != "bitwarden" ]; then
|
||||||
|
log_skip "Credential source is keychain; пропускаю Bitwarden login."
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7: Run smoke test**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS for installer/runtime shell smoke assertions.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit installer changes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add install.sh tests/smoke.sh
|
||||||
|
git commit -m "feat: configure credential source during install"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: Update Swift Menu-Bar Status Decoding
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `app/Sources/LemanaVPN/VPNManager.swift`
|
||||||
|
- Test: `tests/smoke.sh`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add optional credentials decoding**
|
||||||
|
|
||||||
|
Inside `ModuleStatus`, add this struct after `ToggleModule`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
struct Credentials: Decodable {
|
||||||
|
var source: String
|
||||||
|
var keychain_ready: Bool
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this property before `var bitwarden: ToggleModule`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
var credentials: Credentials?
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Reflect source in the module summary**
|
||||||
|
|
||||||
|
In `var summary: String`, add this line after `let coreState = ...`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let credentialState = credentials.map { "🔐 \($0.source)" } ?? "🔐 legacy"
|
||||||
|
```
|
||||||
|
|
||||||
|
Replace the return line:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
return [coreState, bwState, touchState, dnsState, appState, autostartState, patchState, keychainState].joined(separator: " | ")
|
||||||
|
```
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
return [coreState, credentialState, bwState, touchState, dnsState, appState, autostartState, patchState, keychainState].joined(separator: " | ")
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Log keychain source event**
|
||||||
|
|
||||||
|
In `handleEvent(_:)`, add this switch case before `case "bw_cached":`:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
case "credential_source":
|
||||||
|
if let message = event.message { log(" \(message)") }
|
||||||
|
return
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Add a smoke assertion for Swift source display**
|
||||||
|
|
||||||
|
In `tests/smoke.sh`, after:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
grep -q 'enum VPNLaunchMode' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
```
|
||||||
|
|
||||||
|
add:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
grep -q 'struct Credentials: Decodable' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
grep -q 'credential_source' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Build the app**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
swift build -c release --package-path app
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run smoke test**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit Swift changes**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add app/Sources/LemanaVPN/VPNManager.swift tests/smoke.sh
|
||||||
|
git commit -m "feat: show credential source in menu app"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: Update Documentation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `README.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Update module summary text**
|
||||||
|
|
||||||
|
In `README.md`, replace the current Bitwarden-only module description near the top with:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
**Credential sources:** `bitwarden` syncs LDAP password and TOTP seed from Bitwarden into macOS Keychain; `keychain` stores LDAP password and a permanent TOTP seed directly in macOS Keychain. Both sources use the same `openconnect-lite` SSO/autofill runtime.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add keychain source setup commands**
|
||||||
|
|
||||||
|
In the “Если Bitwarden нет” section, replace the install command block with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
curl -fsSL https://git.dokops.ru/dokril/lemana-vpn/raw/branch/main/install.sh \
|
||||||
|
| sh -s -- --credential-source keychain --configure-keychain
|
||||||
|
```
|
||||||
|
|
||||||
|
Add this paragraph after the command:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
`--credential-source keychain` is the free built-in path: no Bitwarden account, paid Bitwarden TOTP, or external password manager is required. The setup prompt asks for the corporate LDAP password and a permanent TOTP seed. The seed may be pasted as raw Base32 or as an `otpauth://totp/...?...secret=BASE32` URI.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Update config example**
|
||||||
|
|
||||||
|
In the `~/.config/lemana-vpn/env` example, add:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE="bitwarden"
|
||||||
|
```
|
||||||
|
|
||||||
|
For the keychain source, add this separate example:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
LEMANA_VPN_USERNAME="60103293"
|
||||||
|
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"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Document current-code rejection**
|
||||||
|
|
||||||
|
Add this warning in the TOTP explanation:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
Do not paste the current 6-digit authenticator code into `vpn --configure-keychain`. Lemana VPN stores the permanent TOTP seed in Keychain, and `openconnect-lite` uses that seed to generate fresh one-time codes during each SSO login.
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit docs**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add README.md
|
||||||
|
git commit -m "docs: document keychain credential source"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: Full Verification
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Verify: `install.sh`
|
||||||
|
- Verify: `uninstall.sh`
|
||||||
|
- Verify: `bin/vpn-lemanapro.sh`
|
||||||
|
- Verify: `tests/smoke.sh`
|
||||||
|
- Verify: `app/Sources/LemanaVPN/VPNManager.swift`
|
||||||
|
- Verify: `README.md`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Run shell syntax checks**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sh -n install.sh
|
||||||
|
sh -n uninstall.sh
|
||||||
|
bash -n bin/vpn-lemanapro.sh
|
||||||
|
sh -n tests/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: all commands exit 0.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run smoke tests**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tests/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: prints `smoke ok`.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build Swift menu-bar app**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
swift build -c release --package-path app
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: build exits 0.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Check source status JSON**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain bin/vpn-lemanapro.sh --status --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output contains:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"credentials":{"source":"keychain"
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=bitwarden bin/vpn-lemanapro.sh --status --json
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output contains:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"credentials":{"source":"bitwarden"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Check installer dry-runs**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sh install.sh --dry-run --non-interactive --credential-source keychain --configure-keychain --without-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output contains:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Modules: credential_source=keychain bitwarden=0 touchid=0
|
||||||
|
```
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sh install.sh --dry-run --non-interactive --credential-source bitwarden --without-app
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected output contains:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Modules: credential_source=bitwarden bitwarden=1
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Check whitespace**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: no output.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Live VPN decision**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vpn --status
|
||||||
|
vpn --status --json
|
||||||
|
```
|
||||||
|
|
||||||
|
If the installed status is connected, connecting, or reconnecting, do not start another VPN session. Record in the final report that live reconnect was intentionally skipped. If the installed status is disconnected and live validation is approved in the execution session, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
vpn --manual
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: visible browser opens, LDAP password and generated TOTP are filled from Keychain, and submit is not pressed.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Final commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git status --short
|
||||||
|
git add bin/vpn-lemanapro.sh install.sh tests/smoke.sh app/Sources/LemanaVPN/VPNManager.swift README.md
|
||||||
|
git commit -m "feat: add keychain credential source"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
- Spec coverage: The plan covers a free Keychain/TOTP-seed alternative, separates Bitwarden from Keychain source logic, preserves the existing `openconnect-lite` Keychain boundary, updates installer configuration, updates menu-bar status decoding, documents run/check/update flows, and includes non-live plus live-safe verification.
|
||||||
|
- Placeholder scan: The plan contains concrete file paths, exact commands, exact code snippets, and explicit expected outputs.
|
||||||
|
- Type consistency: The runtime source name is consistently `CREDENTIAL_SOURCE` in shell, `LEMANA_VPN_CREDENTIAL_SOURCE` in config/env, `"credentials":{"source":...}` in JSON, and `ModuleStatus.Credentials.source` in Swift.
|
||||||
139
install.sh
139
install.sh
@@ -11,6 +11,7 @@ OC_CONFIG_DIR="${OPENCONNECT_LITE_CONFIG_DIR:-$HOME/.config/openconnect-lite}"
|
|||||||
DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
|
DNS_CLEANUP="/usr/local/sbin/lemana-vpn-dns-cleanup"
|
||||||
USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
|
USERNAME="${LEMANA_VPN_USERNAME:-60103293}"
|
||||||
BW_ITEM="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
|
BW_ITEM="${LEMANA_VPN_BW_ITEM:-LM LDAP}"
|
||||||
|
CREDENTIAL_SOURCE="${LEMANA_VPN_CREDENTIAL_SOURCE:-bitwarden}"
|
||||||
USE_BITWARDEN=1
|
USE_BITWARDEN=1
|
||||||
USE_TOUCHID=1
|
USE_TOUCHID=1
|
||||||
INSTALL_SUDOERS=1
|
INSTALL_SUDOERS=1
|
||||||
@@ -22,6 +23,7 @@ DRY_RUN=0
|
|||||||
FORCE=0
|
FORCE=0
|
||||||
INTERACTIVE=auto
|
INTERACTIVE=auto
|
||||||
BITWARDEN_FORCED=0
|
BITWARDEN_FORCED=0
|
||||||
|
CREDENTIAL_SOURCE_FORCED=0
|
||||||
TOUCHID_FORCED=0
|
TOUCHID_FORCED=0
|
||||||
SUDOERS_FORCED=0
|
SUDOERS_FORCED=0
|
||||||
SHELL_FORCED=0
|
SHELL_FORCED=0
|
||||||
@@ -75,6 +77,7 @@ Usage:
|
|||||||
Options:
|
Options:
|
||||||
--with-bitwarden Install/use Bitwarden CLI module (default)
|
--with-bitwarden Install/use Bitwarden CLI module (default)
|
||||||
--without-bitwarden Do not install/use Bitwarden CLI; use Keychain credentials
|
--without-bitwarden Do not install/use Bitwarden CLI; use Keychain credentials
|
||||||
|
--credential-source VALUE Credential source: bitwarden or keychain
|
||||||
--with-touchid Install/use keychain-fingerprint Touch ID helper (default)
|
--with-touchid Install/use keychain-fingerprint Touch ID helper (default)
|
||||||
--without-touchid Do not install/use Touch ID helper
|
--without-touchid Do not install/use Touch ID helper
|
||||||
--configure-keychain Prompt for LDAP password and TOTP secret after install
|
--configure-keychain Prompt for LDAP password and TOTP secret after install
|
||||||
@@ -97,6 +100,7 @@ Options:
|
|||||||
Examples:
|
Examples:
|
||||||
sh install.sh
|
sh install.sh
|
||||||
sh install.sh --minimal --configure-keychain
|
sh install.sh --minimal --configure-keychain
|
||||||
|
sh install.sh --credential-source keychain --configure-keychain
|
||||||
sh install.sh --without-touchid
|
sh install.sh --without-touchid
|
||||||
USAGE
|
USAGE
|
||||||
}
|
}
|
||||||
@@ -104,12 +108,36 @@ USAGE
|
|||||||
while [ "$#" -gt 0 ]; do
|
while [ "$#" -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--with-bitwarden)
|
--with-bitwarden)
|
||||||
|
CREDENTIAL_SOURCE="bitwarden"
|
||||||
USE_BITWARDEN=1
|
USE_BITWARDEN=1
|
||||||
BITWARDEN_FORCED=1
|
BITWARDEN_FORCED=1
|
||||||
|
CREDENTIAL_SOURCE_FORCED=1
|
||||||
;;
|
;;
|
||||||
--without-bitwarden)
|
--without-bitwarden)
|
||||||
|
CREDENTIAL_SOURCE="keychain"
|
||||||
USE_BITWARDEN=0
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
BITWARDEN_FORCED=1
|
BITWARDEN_FORCED=1
|
||||||
|
CREDENTIAL_SOURCE_FORCED=1
|
||||||
|
;;
|
||||||
|
--credential-source)
|
||||||
|
shift
|
||||||
|
[ "$#" -gt 0 ] || { echo "--credential-source requires bitwarden or keychain" >&2; exit 1; }
|
||||||
|
CREDENTIAL_SOURCE="$1"
|
||||||
|
CREDENTIAL_SOURCE_FORCED=1
|
||||||
|
case "$CREDENTIAL_SOURCE" in
|
||||||
|
bitwarden)
|
||||||
|
USE_BITWARDEN=1
|
||||||
|
;;
|
||||||
|
keychain)
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "--credential-source requires bitwarden or keychain" >&2
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
;;
|
;;
|
||||||
--with-touchid)
|
--with-touchid)
|
||||||
USE_TOUCHID=1
|
USE_TOUCHID=1
|
||||||
@@ -167,9 +195,11 @@ while [ "$#" -gt 0 ]; do
|
|||||||
--interactive) INTERACTIVE=1 ;;
|
--interactive) INTERACTIVE=1 ;;
|
||||||
--non-interactive) INTERACTIVE=0 ;;
|
--non-interactive) INTERACTIVE=0 ;;
|
||||||
--minimal)
|
--minimal)
|
||||||
|
CREDENTIAL_SOURCE="keychain"
|
||||||
USE_BITWARDEN=0
|
USE_BITWARDEN=0
|
||||||
USE_TOUCHID=0
|
USE_TOUCHID=0
|
||||||
BITWARDEN_FORCED=1
|
BITWARDEN_FORCED=1
|
||||||
|
CREDENTIAL_SOURCE_FORCED=1
|
||||||
TOUCHID_FORCED=1
|
TOUCHID_FORCED=1
|
||||||
;;
|
;;
|
||||||
--dry-run) DRY_RUN=1 ;;
|
--dry-run) DRY_RUN=1 ;;
|
||||||
@@ -243,7 +273,7 @@ need_cmd() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
has_tty() {
|
has_tty() {
|
||||||
[ -r /dev/tty ] && [ -w /dev/tty ]
|
{ [ -r /dev/tty ] && [ -w /dev/tty ]; } || [ -t 0 ]
|
||||||
}
|
}
|
||||||
|
|
||||||
interactive_enabled() {
|
interactive_enabled() {
|
||||||
@@ -255,6 +285,22 @@ interactive_enabled() {
|
|||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
|
prompt_printf() {
|
||||||
|
if [ -w /dev/tty ]; then
|
||||||
|
printf "$@" > /dev/tty
|
||||||
|
else
|
||||||
|
printf "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prompt_read_answer() {
|
||||||
|
if [ -r /dev/tty ]; then
|
||||||
|
IFS= read -r answer < /dev/tty || answer=""
|
||||||
|
else
|
||||||
|
IFS= read -r answer || answer=""
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
yes_no() {
|
yes_no() {
|
||||||
prompt="$1"
|
prompt="$1"
|
||||||
default_answer="$2"
|
default_answer="$2"
|
||||||
@@ -272,17 +318,69 @@ yes_no() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
while :; do
|
while :; do
|
||||||
printf '%s%s %s%s ' "$C_BOLD" "$prompt" "$suffix" "$C_RESET" > /dev/tty
|
prompt_printf '%s%s %s%s ' "$C_BOLD" "$prompt" "$suffix" "$C_RESET"
|
||||||
IFS= read -r answer < /dev/tty || answer=""
|
prompt_read_answer
|
||||||
case "$answer" in
|
case "$answer" in
|
||||||
"") [ "$default_answer" = "y" ]; return $? ;;
|
"") [ "$default_answer" = "y" ]; return $? ;;
|
||||||
y|Y|yes|YES|Yes|д|Д|да|Да|ДА) return 0 ;;
|
y|Y|yes|YES|Yes|д|Д|да|Да|ДА) return 0 ;;
|
||||||
n|N|no|NO|No|н|Н|нет|Нет|НЕТ) return 1 ;;
|
n|N|no|NO|No|н|Н|нет|Нет|НЕТ) return 1 ;;
|
||||||
*) printf 'Введите y или n.\n' > /dev/tty ;;
|
*) prompt_printf 'Введите y или n.\n' ;;
|
||||||
esac
|
esac
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
|
choose_credential_source_interactive() {
|
||||||
|
log_step "Настраиваю способ хранения credentials"
|
||||||
|
log_detail "Можно выбрать Bitwarden или бесплатный macOS Keychain flow с ручным вводом LDAP password и TOTP seed."
|
||||||
|
|
||||||
|
while :; do
|
||||||
|
prompt_printf '\n'
|
||||||
|
prompt_printf '%sКак хранить VPN credentials?%s\n' "$C_BOLD" "$C_RESET"
|
||||||
|
prompt_printf ' 1) Bitwarden -> macOS Keychain\n'
|
||||||
|
prompt_printf ' 2) macOS Keychain: ввести LDAP password и TOTP seed сейчас\n'
|
||||||
|
prompt_printf ' 3) macOS Keychain: настрою вручную позже\n'
|
||||||
|
prompt_printf '%sВыбор [1/2/3, Enter=1]:%s ' "$C_BOLD" "$C_RESET"
|
||||||
|
|
||||||
|
prompt_read_answer
|
||||||
|
case "$answer" in
|
||||||
|
""|1)
|
||||||
|
CREDENTIAL_SOURCE=bitwarden
|
||||||
|
USE_BITWARDEN=1
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
CREDENTIAL_SOURCE=keychain
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
CONFIGURE_KEYCHAIN=1
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
CREDENTIAL_SOURCE=keychain
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
CONFIGURE_KEYCHAIN=0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
prompt_printf 'Введите 1, 2 или 3.\n'
|
||||||
|
continue
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
break
|
||||||
|
done
|
||||||
|
|
||||||
|
case "$CREDENTIAL_SOURCE" in
|
||||||
|
bitwarden)
|
||||||
|
log_info "Credential source: Bitwarden sync into macOS Keychain"
|
||||||
|
;;
|
||||||
|
keychain)
|
||||||
|
if [ "$CONFIGURE_KEYCHAIN" -eq 1 ]; then
|
||||||
|
log_info "Credential source: macOS Keychain, credentials будут запрошены после установки"
|
||||||
|
else
|
||||||
|
log_info "Credential source: macOS Keychain, credentials можно настроить позже через vpn --configure-keychain"
|
||||||
|
fi
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
bool_word() {
|
bool_word() {
|
||||||
if "$@" >/dev/null 2>&1; then
|
if "$@" >/dev/null 2>&1; then
|
||||||
printf 'yes'
|
printf 'yes'
|
||||||
@@ -321,19 +419,39 @@ print_detected_state() {
|
|||||||
choose_modules() {
|
choose_modules() {
|
||||||
print_detected_state
|
print_detected_state
|
||||||
|
|
||||||
|
case "$CREDENTIAL_SOURCE" in
|
||||||
|
bitwarden)
|
||||||
|
USE_BITWARDEN=1
|
||||||
|
;;
|
||||||
|
keychain)
|
||||||
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
die "Unknown credential source: $CREDENTIAL_SOURCE"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
if ! interactive_enabled; then
|
if ! interactive_enabled; then
|
||||||
log_skip "Interactive prompts: off"
|
log_skip "Interactive prompts: off"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
log_info "Interactive prompts: on"
|
log_info "Interactive prompts: on"
|
||||||
log_detail "Будут вопросы только по отсутствующим опциональным модулям; флаги командной строки имеют приоритет."
|
log_detail "Установщик проведёт по выбору credential source и отсутствующих опциональных модулей; флаги командной строки имеют приоритет."
|
||||||
|
|
||||||
if [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then
|
if [ "$CREDENTIAL_SOURCE_FORCED" -eq 0 ]; then
|
||||||
|
choose_credential_source_interactive
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$CREDENTIAL_SOURCE" = "bitwarden" ] && [ "$BITWARDEN_FORCED" -eq 0 ] && ! command -v bw >/dev/null 2>&1; then
|
||||||
if yes_no "Bitwarden CLI не найден. Поставить модуль Bitwarden?" y; then
|
if yes_no "Bitwarden CLI не найден. Поставить модуль Bitwarden?" y; then
|
||||||
|
CREDENTIAL_SOURCE=bitwarden
|
||||||
USE_BITWARDEN=1
|
USE_BITWARDEN=1
|
||||||
else
|
else
|
||||||
|
CREDENTIAL_SOURCE=keychain
|
||||||
USE_BITWARDEN=0
|
USE_BITWARDEN=0
|
||||||
|
USE_TOUCHID=0
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
@@ -386,7 +504,7 @@ choose_modules() {
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$USE_BITWARDEN" -eq 0 ]; then
|
if [ "$CONFIGURE_KEYCHAIN_FORCED" -eq 0 ] && [ "$CREDENTIAL_SOURCE" = "keychain" ] && [ "$CONFIGURE_KEYCHAIN" -eq 0 ]; then
|
||||||
if ! keychain_has openconnect-lite "$USERNAME" || ! keychain_has openconnect-lite "totp/$USERNAME"; then
|
if ! keychain_has openconnect-lite "$USERNAME" || ! keychain_has openconnect-lite "totp/$USERNAME"; then
|
||||||
if yes_no "Bitwarden отключён, а Keychain credentials неполные. Записать LDAP-пароль и TOTP seed после установки?" y; then
|
if yes_no "Bitwarden отключён, а Keychain credentials неполные. Записать LDAP-пароль и TOTP seed после установки?" y; then
|
||||||
CONFIGURE_KEYCHAIN=1
|
CONFIGURE_KEYCHAIN=1
|
||||||
@@ -510,6 +628,7 @@ install_config() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
env_content="LEMANA_VPN_USERNAME=\"$USERNAME\"
|
env_content="LEMANA_VPN_USERNAME=\"$USERNAME\"
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=\"$CREDENTIAL_SOURCE\"
|
||||||
LEMANA_VPN_BW_ITEM=\"$BW_ITEM\"
|
LEMANA_VPN_BW_ITEM=\"$BW_ITEM\"
|
||||||
LEMANA_VPN_USE_BITWARDEN=\"$USE_BITWARDEN\"
|
LEMANA_VPN_USE_BITWARDEN=\"$USE_BITWARDEN\"
|
||||||
LEMANA_VPN_USE_TOUCHID=\"$USE_TOUCHID\"
|
LEMANA_VPN_USE_TOUCHID=\"$USE_TOUCHID\"
|
||||||
@@ -757,8 +876,8 @@ EOF
|
|||||||
}
|
}
|
||||||
|
|
||||||
maybe_login_bitwarden() {
|
maybe_login_bitwarden() {
|
||||||
if [ "$USE_BITWARDEN" -ne 1 ]; then
|
if [ "$CREDENTIAL_SOURCE" != "bitwarden" ]; then
|
||||||
log_skip "Bitwarden module disabled; credentials будут браться из macOS Keychain."
|
log_skip "Credential source is keychain; пропускаю Bitwarden login."
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
if ! command -v bw >/dev/null 2>&1; then
|
if ! command -v bw >/dev/null 2>&1; then
|
||||||
@@ -789,7 +908,7 @@ main() {
|
|||||||
|
|
||||||
log_step "Начинаю установку Lemana VPN"
|
log_step "Начинаю установку Lemana VPN"
|
||||||
log_detail "Повторный запуск безопасен: файлы обновляются идемпотентно, существующие credentials не перезаписываются."
|
log_detail "Повторный запуск безопасен: файлы обновляются идемпотентно, существующие credentials не перезаписываются."
|
||||||
log_info "Modules: bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
|
log_info "Modules: credential_source=$CREDENTIAL_SOURCE bitwarden=$USE_BITWARDEN touchid=$USE_TOUCHID sudoers=$INSTALL_SUDOERS shell=$INSTALL_ALIASES app=$INSTALL_APP autostart=$INSTALL_AUTOSTART"
|
||||||
|
|
||||||
install_homebrew_packages
|
install_homebrew_packages
|
||||||
install_openconnect_lite
|
install_openconnect_lite
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ output="$(cd "$ROOT" && sh install.sh --dry-run --non-interactive --minimal)"
|
|||||||
|
|
||||||
printf '%s\n' "$output" | grep -q 'Detected state:'
|
printf '%s\n' "$output" | grep -q 'Detected state:'
|
||||||
printf '%s\n' "$output" | grep -q 'Interactive prompts: off'
|
printf '%s\n' "$output" | grep -q 'Interactive prompts: off'
|
||||||
printf '%s\n' "$output" | grep -q 'Modules: bitwarden=0 touchid=0 sudoers=1 shell=1 app=1 autostart=1'
|
printf '%s\n' "$output" | grep -q 'Modules: credential_source=keychain bitwarden=0 touchid=0 sudoers=1 shell=1 app=1 autostart=1'
|
||||||
printf '%s\n' "$output" | grep -q 'Проверяю Homebrew-зависимости'
|
printf '%s\n' "$output" | grep -q 'Проверяю Homebrew-зависимости'
|
||||||
printf '%s\n' "$output" | grep -q 'Swift build может занять минуту'
|
printf '%s\n' "$output" | grep -q 'Swift build может занять минуту'
|
||||||
printf '%s\n' "$output" | grep -q 'sudo install -d -m 755 -o root -g wheel /usr/local/sbin'
|
printf '%s\n' "$output" | grep -q 'sudo install -d -m 755 -o root -g wheel /usr/local/sbin'
|
||||||
@@ -29,9 +29,38 @@ if printf '%s\n' "$output" | grep -q "$esc"; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
if command -v expect >/dev/null 2>&1; then
|
||||||
|
interactive_output="$(
|
||||||
|
ROOT="$ROOT" expect <<'EXPECT'
|
||||||
|
set timeout 30
|
||||||
|
spawn sh $env(ROOT)/install.sh --dry-run --interactive --without-app --no-shell --no-sudoers
|
||||||
|
expect "Как хранить VPN credentials?"
|
||||||
|
expect "Выбор"
|
||||||
|
send "2\r"
|
||||||
|
expect eof
|
||||||
|
EXPECT
|
||||||
|
)"
|
||||||
|
|
||||||
|
printf '%s\n' "$interactive_output" | grep -q 'Как хранить VPN credentials?'
|
||||||
|
printf '%s\n' "$interactive_output" | grep -q '1) Bitwarden'
|
||||||
|
printf '%s\n' "$interactive_output" | grep -q '2) macOS Keychain: ввести LDAP password и TOTP seed сейчас'
|
||||||
|
printf '%s\n' "$interactive_output" | grep -q '3) macOS Keychain: настрою вручную позже'
|
||||||
|
printf '%s\n' "$interactive_output" | grep -q 'Modules: credential_source=keychain bitwarden=0 touchid=0 sudoers=0 shell=0 app=0 autostart=0'
|
||||||
|
printf '%s\n' "$interactive_output" | grep -q "vpn-lemanapro.sh --configure-keychain"
|
||||||
|
fi
|
||||||
|
|
||||||
status_json="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
|
status_json="$(bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
|
||||||
printf '%s\n' "$status_json" | grep -q '"modules":'
|
printf '%s\n' "$status_json" | grep -q '"modules":'
|
||||||
printf '%s\n' "$status_json" | grep -q '"app":'
|
printf '%s\n' "$status_json" | grep -q '"app":'
|
||||||
|
|
||||||
|
keychain_status_json="$(LEMANA_VPN_CREDENTIAL_SOURCE=keychain bash "$ROOT/bin/vpn-lemanapro.sh" --status --json)"
|
||||||
|
printf '%s\n' "$keychain_status_json" | grep -q '"credentials":{"source":"keychain","keychain_ready":false}'
|
||||||
|
printf '%s\n' "$keychain_status_json" | grep -q '"bitwarden":{"enabled":false'
|
||||||
|
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'
|
||||||
grep -q 'LemanaVPN-openconnect-lite.log' "$ROOT/bin/vpn-lemanapro.sh"
|
grep -q 'LemanaVPN-openconnect-lite.log' "$ROOT/bin/vpn-lemanapro.sh"
|
||||||
grep -q '"event":"waiting"' "$ROOT/bin/vpn-lemanapro.sh"
|
grep -q '"event":"waiting"' "$ROOT/bin/vpn-lemanapro.sh"
|
||||||
grep -q -- '--patch-only' "$ROOT/bin/vpn-lemanapro.sh"
|
grep -q -- '--patch-only' "$ROOT/bin/vpn-lemanapro.sh"
|
||||||
@@ -45,6 +74,8 @@ grep -q 'vpn-manual' "$ROOT/install.sh"
|
|||||||
grep -q 'connect(mode: .auto)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
grep -q 'connect(mode: .auto)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
||||||
grep -q 'connect(mode: .manual)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
grep -q 'connect(mode: .manual)' "$ROOT/app/Sources/LemanaVPN/LemanaVPNApp.swift"
|
||||||
grep -q 'enum VPNLaunchMode' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
grep -q 'enum VPNLaunchMode' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
grep -q 'struct Credentials: Decodable' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
grep -q 'credential_source' "$ROOT/app/Sources/LemanaVPN/VPNManager.swift"
|
||||||
|
|
||||||
fake_webengine="$TMP_DIR/webengine_process.py"
|
fake_webengine="$TMP_DIR/webengine_process.py"
|
||||||
fake_authenticator="$TMP_DIR/authenticator.py"
|
fake_authenticator="$TMP_DIR/authenticator.py"
|
||||||
@@ -190,7 +221,7 @@ set +e
|
|||||||
manual_output="$(
|
manual_output="$(
|
||||||
HOME="$HOME" \
|
HOME="$HOME" \
|
||||||
LEMANA_VPN_USERNAME="$missing_user" \
|
LEMANA_VPN_USERNAME="$missing_user" \
|
||||||
LEMANA_VPN_USE_BITWARDEN=0 \
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
bash "$ROOT/bin/vpn-lemanapro.sh" --json 2>&1
|
bash "$ROOT/bin/vpn-lemanapro.sh" --json 2>&1
|
||||||
)"
|
)"
|
||||||
manual_code=$?
|
manual_code=$?
|
||||||
@@ -204,6 +235,28 @@ if printf '%s\n' "$manual_output" | grep -q 'Cleaning up VPN DNS'; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
fake_oc_python="$TMP_DIR/fake-oc-python"
|
||||||
|
captured_totp="$TMP_DIR/captured-totp"
|
||||||
|
cat > "$fake_oc_python" <<'SH'
|
||||||
|
#!/bin/sh
|
||||||
|
cat >/dev/null
|
||||||
|
printf '%s\n' "${_VPN_TOTP_SECRET:-}" > "$LEMANA_VPN_CAPTURE_TOTP"
|
||||||
|
SH
|
||||||
|
chmod +x "$fake_oc_python"
|
||||||
|
|
||||||
|
configure_output="$(
|
||||||
|
printf 'ldap-password\notpauth://totp/Lemana:test?secret=abcd2345efgh6723&issuer=Lemana\n' |
|
||||||
|
HOME="$HOME" \
|
||||||
|
LEMANA_VPN_USERNAME="lemana-configure-$$" \
|
||||||
|
LEMANA_VPN_CREDENTIAL_SOURCE=keychain \
|
||||||
|
LEMANA_VPN_OC_PYTHON="$fake_oc_python" \
|
||||||
|
LEMANA_VPN_CAPTURE_TOTP="$captured_totp" \
|
||||||
|
bash "$ROOT/bin/vpn-lemanapro.sh" --configure-keychain
|
||||||
|
)"
|
||||||
|
|
||||||
|
printf '%s\n' "$configure_output" | grep -q 'Credentials are ready in macOS Keychain'
|
||||||
|
grep -q '^ABCD2345EFGH6723$' "$captured_totp"
|
||||||
|
|
||||||
fake_pwd="$TMP_DIR/fake-pwd"
|
fake_pwd="$TMP_DIR/fake-pwd"
|
||||||
mkdir -p "$fake_pwd/bin"
|
mkdir -p "$fake_pwd/bin"
|
||||||
printf 'stale local cli\n' > "$fake_pwd/bin/vpn-lemanapro.sh"
|
printf 'stale local cli\n' > "$fake_pwd/bin/vpn-lemanapro.sh"
|
||||||
|
|||||||
Reference in New Issue
Block a user