Make installer interactive for credential source selection
This commit is contained in:
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.
|
||||
Reference in New Issue
Block a user