wgctl/commands/identity.command.sh
Nuno Duque Nunes 8bb1de4976 init feature
2026-05-19 15:26:31 +00:00

284 lines
No EOL
8 KiB
Bash

#!/usr/bin/env bash
# identity.command.sh — manage peer identities
#
# Subcommands:
# wgctl identity list
# wgctl identity show --name <name>
# wgctl identity add --name <name> --peer <peer>
# wgctl identity remove --name <name>
# wgctl identity migrate [--dry-run]
# ============================================
# Lifecycle
# ============================================
function cmd::identity::on_load() {
flag::register --name
flag::register --peer
flag::register --dry-run
flag::register --force
}
# ============================================
# Help
# ============================================
function cmd::identity::help() {
cat <<EOF
Usage: wgctl identity <subcommand> [options]
Manage peer identities.
Subcommands:
list List all identities
show --name <name> Show identity details and device status
add --name <name> Manually attach a peer to an identity
--peer <peer>
remove --name <name> Remove identity and all associated peers
migrate [--dry-run] Create identities from existing peer names
Examples:
wgctl identity list
wgctl identity show --name nuno
wgctl identity add --name nuno --peer roboclean
wgctl identity remove --name zephyr
wgctl identity migrate --dry-run
wgctl identity migrate
EOF
}
# ============================================
# Run
# ============================================
function cmd::identity::run() {
local subcmd="${1:-list}"
shift || true
case "$subcmd" in
list) cmd::identity::_list "$@" ;;
show) cmd::identity::_show "$@" ;;
add) cmd::identity::_add "$@" ;;
remove) cmd::identity::_remove "$@" ;;
migrate) cmd::identity::_migrate "$@" ;;
--help) cmd::identity::help ;;
*)
log::error "Unknown subcommand '${subcmd}'. Available: list, show, add, remove, migrate"
return 1
;;
esac
}
# ============================================
# Subcommands
# ============================================
function cmd::identity::_list() {
local data
data=$(identity::list_data)
if [[ -z "$data" ]]; then
log::info "No identities found. Run 'wgctl identity migrate' to create from existing peers."
return 0
fi
ui::identity::header
while IFS='|' read -r name peer_count types; do
ui::identity::row "$name" "$peer_count" "$types"
done <<< "$data"
}
function cmd::identity::_show() {
local name=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--help) cmd::identity::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
identity::require_exists "$name" || return 1
local data peer_count="0"
data=$(identity::show_data "$name")
while IFS='|' read -r key val type_val index_val; do
case "$key" in
name)
peer_count=$(echo "$data" | grep '^peer_count|' | cut -d'|' -f2)
ui::identity::detail_name "$val" "$peer_count"
;;
peer_count) ;; # consumed above
device)
local status=""
status=$(cmd::identity::_device_status "$val")
ui::identity::device_row "$val" "$type_val" "$index_val" "$status"
;;
esac
done <<< "$data"
echo ""
}
function cmd::identity::_device_status() {
local peer_name="${1:-}"
local peer_ip
peer_ip=$(peers::get_ip "$peer_name" 2>/dev/null) || return 0
[[ -z "$peer_ip" ]] && return 0
local is_blocked is_restricted pubkey handshake_ts
is_blocked=$(peers::is_blocked "$peer_name")
is_restricted=$(peers::is_restricted "$peer_name")
pubkey=$(cat "$(ctx::clients)/${peer_name}.pub" 2>/dev/null) || pubkey=""
handshake_ts=$(wg show wg0 latest-handshakes 2>/dev/null \
| awk -v pk="$pubkey" '$1==pk{print $2}') || handshake_ts=0
local last_ts last_evt
last_ts=$(peers::get_meta "$peer_name" "last_ts" 2>/dev/null) || last_ts=""
last_evt=$(peers::get_meta "$peer_name" "last_evt" 2>/dev/null) || last_evt=""
local status
status=$(peers::format_status \
"$peer_name" "$pubkey" "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
echo "${status}"
}
function cmd::identity::_add() {
local name="" peer=""
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--peer) peer="$2"; shift 2 ;;
--help) cmd::identity::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
[[ -z "$peer" ]] && { log::error "Missing required flag: --peer"; return 1; }
cmd::identity::_require_peer_exists "$peer" || return 1
local peer_type index
peer_type=$(cmd::identity::_resolve_peer_type "$peer" "$name")
index=$(identity::next_index "$name" "$peer_type")
local id_file
id_file=$(ctx::identity::path "${name}.identity")
json::identity_add_peer "$id_file" "$name" "$peer" "$peer_type" "$index" </dev/null
log::ok "Added '${peer}' to identity '${name}' (${peer_type} #${index})"
}
function cmd::identity::_require_peer_exists() {
local peer="${1:-}"
if [[ ! -f "$(ctx::clients)/${peer}.conf" ]]; then
log::error "Peer '${peer}' not found"
return 1
fi
}
function cmd::identity::_resolve_peer_type() {
local peer="${1:-}" identity_name="${2:-}"
local inferred
inferred=$(identity::infer "$peer")
if [[ -n "$inferred" ]]; then
echo "$inferred" | cut -d'|' -f2
else
peers::get_meta "$peer" "type" 2>/dev/null || echo "none"
fi
}
function cmd::identity::_remove() {
local name="" force=false
while [[ $# -gt 0 ]]; do
case "$1" in
--name) name="$2"; shift 2 ;;
--force) force=true; shift ;;
--help) cmd::identity::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
identity::require_exists "$name" || return 1
local peers
peers=$(identity::peers "$name")
if [[ -n "$peers" ]]; then
local peer_list="${peers//$'\n'/, }"
log::warn "This will permanently remove identity '${name}' and ALL associated peers:"
log::warn " ${peer_list}"
if ! $force; then
ui::confirm "Continue?" || { log::info "Aborted"; return 0; }
fi
cmd::identity::_remove_all_peers "$peers"
peers::reload || return 1
fi
local id_file
id_file=$(ctx::identity::path "${name}.identity")
json::identity_remove "$id_file" </dev/null
log::ok "Identity '${name}' removed"
}
function cmd::identity::_remove_all_peers() {
local peers="${1:-}"
while IFS= read -r peer_name; do
[[ -z "$peer_name" ]] && continue
cmd::identity::_remove_peer "$peer_name"
done <<< "$peers"
}
function cmd::identity::_remove_peer() {
local peer_name="${1:-}"
local client_ip was_blocked=false
client_ip=$(peers::get_ip "$peer_name" 2>/dev/null) || client_ip=""
peers::is_blocked "$peer_name" && was_blocked=true
peers::purge "$peer_name" "$client_ip" "$was_blocked" || return 1
log::ok "Removed peer '${peer_name}'"
}
function cmd::identity::_migrate() {
local dry_run="false"
while [[ $# -gt 0 ]]; do
case "$1" in
--dry-run) dry_run="true"; shift ;;
--help) cmd::identity::help; return ;;
*) log::error "Unknown flag: $1"; return 1 ;;
esac
done
[[ "$dry_run" == "true" ]] && log::info "Dry run — no files will be written"
echo ""
[[ "$dry_run" == "false" ]] && mkdir -p "$(ctx::identities)"
local created=0 skipped=0 output
output=$(json::identity_migrate \
"$(ctx::identities)" \
"$(ctx::clients)" \
"$(ctx::meta)" \
"$dry_run")
while IFS='|' read -r action identity_name peer_name peer_type index; do
case "$action" in
create)
ui::identity::migrate_create "$peer_name" "$identity_name" "$peer_type" "$index"
(( created++ )) || true
;;
skip)
ui::identity::migrate_skip "$peer_name"
(( skipped++ )) || true
;;
esac
done <<< "$output"
ui::identity::migrate_summary "$created" "$skipped" "$dry_run"
}