#!/usr/bin/env bash # identity.module.sh — identity file management and peer-name inference # =========================================================================== # Path helpers # =========================================================================== function identity::path() { local name="${1:-}" echo "$(ctx::identities)/${name}.identity" } # =========================================================================== # Existence checks # =========================================================================== function identity::exists() { local name="${1:-}" json::identity_exists "$(identity::path "$name")" 2>/dev/null } function identity::require_exists() { local name="${1:-}" if ! identity::exists "$name"; then log::error "Identity '${name}' not found. Use 'wgctl identity list' to see all identities." return 1 fi } function identity::require_not_exists() { local name="${1:-}" if identity::exists "$name"; then log::error "Identity '${name}' already exists." return 1 fi } # identity::require_exists_for_flag # Used by commands to validate --identity value before proceeding. function identity::require_exists_for_flag() { local identity_name="${1:-}" [[ -z "$identity_name" ]] && { log::error "Missing value for --identity" return 1 } # Identity may not exist yet for add (it will be created) # Only require existence for commands that read from it return 0 } # identity::require_has_peers # Used by block/unblock/list to ensure identity has peers to operate on. function identity::require_has_peers() { local identity_name="${1:-}" local peers peers=$(identity::peers "$identity_name") if [[ -z "$peers" ]]; then log::error "Identity '${identity_name}' has no peers" return 1 fi } # =========================================================================== # Peer name inference # =========================================================================== # identity::infer # Parses a peer name and returns "identity_name|type|index" if it matches # the naming convention, or empty string if not. # phone-nuno -> "nuno|phone|1" # phone-nuno-2 -> "nuno|phone|2" # roboclean -> "" (no type prefix) function identity::infer() { local peer_name="${1:-}" json::identity_infer "$peer_name" 2>/dev/null || true } # identity::next_index # Returns the next available device index for a type within an identity. # If identity doesn't exist yet, returns 1. function identity::next_index() { local identity_name="${1:-}" peer_type="${2:-}" local id_file id_file=$(identity::path "$identity_name") if [[ ! -f "$id_file" ]]; then echo 1 return 0 fi json::identity_next_index "$id_file" "$peer_type" 2>/dev/null || echo 1 } # identity::next_peer_name # Returns the full peer name for the next device of a given type # for an identity. Creates the name with the correct index. # e.g. identity::next_peer_name helena phone → phone-helena-2 # (if phone-helena already exists, index 1 is taken) function identity::next_peer_name() { local identity_name="${1:-}" peer_type="${2:-}" [[ -z "$identity_name" || -z "$peer_type" ]] && return 1 local index index=$(identity::next_index "$identity_name" "$peer_type") if [[ "$index" -eq 1 ]]; then echo "${peer_type}-${identity_name}" else echo "${peer_type}-${identity_name}-${index}" fi } # =========================================================================== # Auto-attach (called from wgctl add) # =========================================================================== # identity::auto_attach # Infers identity from peer name and adds the peer to the identity file. # Creates the identity file if it doesn't exist. # Silent — no output. Logs a note on success, silently skips if no match. function identity::auto_attach() { local peer_name="${1:-}" peer_type="${2:-}" local inferred inferred=$(identity::infer "$peer_name") [[ -z "$inferred" ]] && return 0 local identity_name type_inferred index identity_name=$(echo "$inferred" | cut -d'|' -f1) type_inferred=$(echo "$inferred" | cut -d'|' -f2) index=$(echo "$inferred" | cut -d'|' -f3) # Use the explicit type if provided, otherwise use inferred type local final_type="${peer_type:-$type_inferred}" local id_file id_file=$(identity::path "$identity_name") json::identity_add_peer "$id_file" "$identity_name" "$peer_name" "$final_type" "$index" # Removes a peer from its identity file when the peer is deleted. # If the identity has no remaining peers, removes the identity file too. function identity::auto_detach() { local peer_name="${1:-}" local inferred inferred=$(identity::infer "$peer_name") [[ -z "$inferred" ]] && return 0 local identity_name identity_name=$(echo "$inferred" | cut -d'|' -f1) local id_file id_file=$(identity::path "$identity_name") [[ ! -f "$id_file" ]] && return 0 json::identity_remove_peer "$id_file" "$peer_name" /dev/null) || true if [[ -z "$remaining" ]]; then rm -f "$id_file" log::info "Identity '${identity_name}' removed (no remaining peers)" fi } # =========================================================================== # Peer queries # =========================================================================== # identity::peers [type_filter] # Returns peer names belonging to an identity, one per line. # Optional type_filter limits to peers of a specific type. function identity::peers() { local identity_name="${1:-}" type_filter="${2:-}" local id_file id_file=$(identity::path "$identity_name") json::identity_peers "$id_file" "$type_filter" 2>/dev/null || true } # identity::get_name # Returns the identity name for a given peer (via inference). function identity::get_name() { local peer_name="${1:-}" local inferred inferred=$(identity::infer "$peer_name") [[ -n "$inferred" ]] && echo "${inferred%%|*}" } # =========================================================================== # Data for commands # =========================================================================== function identity::list_data() { json::identity_list "$(ctx::identities)" 2>/dev/null || true } function identity::show_data() { local name="${1:-}" json::identity_show "$(identity::path "$name")" 2>/dev/null } # =========================================================================== # Rename helper (called from rename.command.sh) # =========================================================================== # identity::rename_peer # Updates identity file entry when a peer is renamed. # Re-infers identity from old name, removes old entry, adds new entry. function identity::rename_peer() { local old_name="${1:-}" new_name="${2:-}" local old_inferred old_inferred=$(identity::infer "$old_name") [[ -z "$old_inferred" ]] && return 0 local identity_name old_type old_index identity_name=$(echo "$old_inferred" | cut -d'|' -f1) old_type=$(echo "$old_inferred" | cut -d'|' -f2) old_index=$(echo "$old_inferred" | cut -d'|' -f3) local id_file id_file=$(identity::path "$identity_name") [[ ! -f "$id_file" ]] && return 0 # Infer new identity context from new name local new_inferred new_identity new_type new_index new_inferred=$(identity::infer "$new_name") if [[ -n "$new_inferred" ]]; then new_identity=$(echo "$new_inferred" | cut -d'|' -f1) new_type=$(echo "$new_inferred" | cut -d'|' -f2) new_index=$(echo "$new_inferred" | cut -d'|' -f3) else # New name doesn't match convention — detach cleanly json::identity_remove_peer "$id_file" "$old_name" phone-helena) — move to new identity file local new_id_file new_id_file=$(identity::path "$new_identity") json::identity_add_peer "$new_id_file" "$new_identity" "$new_name" "$new_type" "$new_index" /dev/null) || true if [[ -z "$remaining" ]]; then rm -f "$id_file" fi fi } # identity::rule # Returns the rule assigned to an identity, or empty string if none. function identity::rule() { local identity_name="${1:-}" local id_file id_file=$(ctx::identity::path "${identity_name}.identity") [[ ! -f "$id_file" ]] && return 0 json::get "$id_file" "rule" 2>/dev/null || true } # identity::set_rule # Sets the rule on an identity file. function identity::set_rule() { local identity_name="${1:-}" rule_name="${2:-}" local id_file id_file=$(ctx::identity::path "${identity_name}.identity") if [[ ! -f "$id_file" ]]; then log::error "Identity '${identity_name}' not found" return 1 fi json::set "$id_file" "rule" "$rule_name" } # identity::clear_rule # Removes the rule from an identity file. function identity::clear_rule() { local identity_name="${1:-}" local id_file id_file=$(ctx::identity::path "${identity_name}.identity") [[ ! -f "$id_file" ]] && return 0 json::delete "$id_file" "rule" 2>/dev/null || true } # identity::policy # Returns the policy name assigned to an identity, or "default". function identity::policy() { local identity_name="${1:-}" local id_file id_file=$(ctx::identity::path "${identity_name}.identity") [[ ! -f "$id_file" ]] && echo "default" && return 0 local result result=$(json::get "$id_file" "policy" 2>/dev/null) || true echo "${result:-default}" } # identity::set_policy # Sets the policy on an identity file. function identity::set_policy() { local identity_name="${1:-}" policy_name="${2:-}" local id_file id_file=$(ctx::identity::path "${identity_name}.identity") if [[ ! -f "$id_file" ]]; then log::error "Identity '${identity_name}' not found" return 1 fi json::set "$id_file" "policy" "$policy_name" } # identity::rule_flags # Returns a specific rule_flag from the identity file. # Falls back to the policy's value if not explicitly set on the identity. function identity::rule_flags() { local identity_name="${1:-}" flag="${2:-}" local id_file id_file=$(ctx::identity::path "${identity_name}.identity") local result result=$(json::get_nested "$id_file" "rule_flags" "$flag" 2>/dev/null) || true if [[ -n "$result" ]]; then echo "$result" return 0 fi # Fall back to policy value local policy_name policy_name=$(identity::policy "$identity_name") policy::get "$policy_name" "$flag" } # identity::set_rule_flag # Sets a rule_flag directly on the identity file. function identity::set_rule_flag() { local identity_name="${1:-}" flag="${2:-}" value="${3:-}" local id_file id_file=$(ctx::identity::path "${identity_name}.identity") if [[ ! -f "$id_file" ]]; then log::error "Identity '${identity_name}' not found" return 1 fi json::set_nested "$id_file" "rule_flags" "$flag" "$value" } # identity::reapply_rules # Reapply the identity rule to all peers in this identity. # Respects auto_apply flag — if false, does nothing. function identity::reapply_rules() { local identity_name="${1:-}" # Check auto_apply local auto auto=$(identity::rule_flags "$identity_name" "auto_apply") [[ "$auto" == "false" ]] && return 0 local identity_rule identity_rule=$(identity::rule "$identity_name") [[ -z "$identity_rule" ]] && return 0 local peers peers=$(identity::peers "$identity_name") [[ -z "$peers" ]] && return 0 while IFS= read -r peer_name; do [[ -z "$peer_name" ]] && continue local client_ip client_ip=$(peers::get_ip "$peer_name") || continue rule::full_restore_peer "$peer_name" "$client_ip" done <<< "$peers" }