Última atividade 1 month ago

identity.command.sh Bruto
1#!/usr/bin/env bash
2# identity.command.sh — manage peer identities
3#
4# Subcommands:
5# wgctl identity list
6# wgctl identity show --name <name>
7# wgctl identity add --name <name> --peer <peer>
8# wgctl identity remove --name <name>
9# wgctl identity migrate [--dry-run]
10
11# ============================================
12# Lifecycle
13# ============================================
14
15function cmd::identity::on_load() {
16 load_module identity
17 load_module policy
18
19 flag::register --name
20 flag::register --peer
21 flag::register --dry-run
22 flag::register --force
23 flag::register --rule
24 flag::register --policy
25 flag::register --set-strict-rule
26 flag::register --unset-strict-rule
27 flag::register --set-auto-apply
28 flag::register --unset-auto-apply
29 flag::register --field
30 flag::register --value
31 flag::register --migrate
32
33 command::mixin json_output
34}
35
36# ============================================
37# Help
38# ============================================
39
40function cmd::identity::help() {
41 cat <<EOF
42Usage: wgctl identity <subcommand> [options]
43
44Manage peer identities — group peers by person/device owner.
45
46Subcommands:
47 list List all identities
48 show --name <n> Show identity details with peers and rule tree
49 add --name <n> Create a new identity
50 remove --name <n> Remove an identity
51 migrate Migrate peers to identities
52
53 rule assign --name <n> --rule <r> Assign rule to identity
54 Blocked if peer already has rule directly
55 [--migrate] Remove conflicting direct peer rules first
56 rule unassign --name <n> --rule <r> Remove rule from identity
57 rule unassign --name <n> --all Remove all rules from identity
58
59 options --name <n> --strict-rule <bool> Set strict rule mode
60 options --name <n> --auto-apply <bool> Set auto apply
61
62Examples:
63 wgctl identity list
64 wgctl identity show --name nuno
65 wgctl identity add --name alice
66 wgctl identity rule assign --name nuno --rule admin
67 wgctl identity rule assign --name nuno --rule user --migrate
68 wgctl identity rule unassign --name nuno --rule admin
69 wgctl identity options --name nuno --strict-rule true
70EOF
71}
72
73# ============================================
74# Run
75# ============================================
76
77function cmd::identity::run() {
78 local subcmd="${1:-list}"
79 shift || true
80
81 if command::json && [[ "$subcmd" == "list" ]]; then
82 cmd::identity::_output_json
83 return 0
84 fi
85
86 case "$subcmd" in
87 list) cmd::identity::_list "$@" ;;
88 show) cmd::identity::_show "$@" ;;
89 add) cmd::identity::_add "$@" ;;
90 remove) cmd::identity::_remove "$@" ;;
91 migrate) cmd::identity::_migrate "$@" ;;
92 rule) cmd::identity::_rule "$@" ;;
93 options) cmd::identity::_options "$@" ;;
94 --help) cmd::identity::help ;;
95 *)
96 log::error "Unknown subcommand '${subcmd}'. Available: list, show, add, remove, migrate, rule, options"
97 return 1
98 ;;
99 esac
100}
101
102# ============================================
103# Subcommands
104# ============================================
105
106function cmd::identity::_list() {
107 local data
108 data=$(identity::list_data | ui::sort_rows 1)
109
110 if [[ -z "$data" ]]; then
111 log::info "No identities found. Run 'wgctl identity migrate' to create from existing peers."
112 return 0
113 fi
114
115 if display::is_table "identity_list"; then
116 cmd::identity::_render_table "$data"
117 return 0
118 fi
119
120 echo ""
121 while IFS='|' read -r name peer_count types rules policy; do
122 local rules_display
123 rules_display=$(echo "$rules" | sed 's/,/, /g')
124 ui::identity::list_row_compact "$name" "$peer_count" "$rules_display" "$policy"
125 done <<< "$data"
126 echo ""
127}
128
129function cmd::identity::_show() {
130 local name=""
131 while [[ $# -gt 0 ]]; do
132 case "$1" in
133 --name) name="$2"; shift 2 ;;
134 --help) cmd::identity::help; return ;;
135 *) log::error "Unknown flag: $1"; return 1 ;;
136 esac
137 done
138
139 [[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
140 identity::require_exists "$name" || return 1
141
142 # Gather identity-level metadata
143 local policy strict auto rules_list peer_count
144 policy=$(identity::policy "$name")
145 strict=$(identity::rule_flags "$name" "strict_rule")
146 auto=$(identity::rule_flags "$name" "auto_apply")
147 rules_list=$(identity::rules "$name" | tr '\n' ',' | sed 's/,$//' | sed 's/,/, /g')
148
149 local data
150 data=$(identity::show_data "$name")
151 peer_count=$(echo "$data" | grep '^peer_count|' | cut -d'|' -f2)
152
153 # Precompute handshakes once for all peers
154 declare -A _id_handshakes=()
155 while IFS=$'\t' read -r pk ts; do
156 [[ -n "$pk" ]] && _id_handshakes["$pk"]="$ts"
157 done < <(wg show "$(config::interface)" latest-handshakes 2>/dev/null)
158
159 # Header
160 echo ""
161 ui::row "Identity" "$name"
162 ui::row "Policy" "$policy"
163 ui::row "Rules" "${rules_list:-}"
164 ui::row "Strict rule" "$(ui::bool "$strict")"
165 ui::row "Auto apply" "$(ui::bool "$auto")"
166 ui::row "Peers" "$peer_count"
167 echo ""
168
169 # Device list
170 while IFS='|' read -r key val type_val index_val; do
171 case "$key" in
172 name|peer_count) ;;
173 device)
174 local status=""
175 status=$(cmd::identity::_device_status "$val" _id_handshakes)
176 ui::identity::device_row "$val" "$type_val" "$index_val" "$status"
177 ;;
178 esac
179 done <<< "$data"
180
181 # Rules tree
182 local identity_rules
183 identity_rules=$(identity::rules "$name")
184 if [[ -n "$identity_rules" ]]; then
185 printf "\n \033[2m── Rules \033[0m%s\n\n" \
186 "$(printf '\033[2m─%.0s' {1..38})"
187 ui::rule::identity_block "$name" "$strict" --no-header
188 fi
189
190 echo ""
191}
192
193function cmd::identity::_render_table() {
194 local data="${1:-}"
195 [[ -z "$data" ]] && return 0
196
197 printf "\n %-20s %-8s %-20s %s\n" "NAME" "PEERS" "RULES" "POLICY"
198 printf " %s\n" "$(printf '─%.0s' {1..65})"
199 while IFS='|' read -r name peer_count types rules policy; do
200 [[ -z "$name" ]] && continue
201 local rules_display
202 rules_display=$(echo "$rules" | sed 's/,/, /g')
203 ui::identity::list_row_table "$name" "$peer_count" "$rules_display" "$policy"
204 done <<< "$data"
205 printf " %s\n\n" "$(printf '─%.0s' {1..65})"
206}
207
208function cmd::identity::_device_status() {
209 local peer_name="${1:-}"
210 local -n _handshakes="${2:-__empty_map}"
211
212 local peer_ip
213 peer_ip=$(peers::get_ip "$peer_name" 2>/dev/null) || return 0
214 [[ -z "$peer_ip" ]] && return 0
215
216 local is_blocked is_restricted pubkey handshake_ts
217 peers::is_blocked "$peer_name" && is_blocked="true" || is_blocked="false"
218 peers::is_restricted "$peer_name" && is_restricted="true" || is_restricted="false"
219
220 pubkey="$(keys::public "$peer_name")"
221 handshake_ts="${_handshakes[$pubkey]:-0}"
222
223 local last_ts
224 last_ts=$(peers::get_meta "$peer_name" "last_ts" 2>/dev/null) || last_ts=""
225
226 local status
227 status=$(peers::format_status_verbose \
228 "$peer_name" "$pubkey" "$is_blocked" "$is_restricted" "$handshake_ts" "$last_ts")
229 echo "${status}"
230}
231
232function cmd::identity::_add() {
233 local name="" peer=""
234 while [[ $# -gt 0 ]]; do
235 case "$1" in
236 --name) name="$2"; shift 2 ;;
237 --peer) peer="$2"; shift 2 ;;
238 --help) cmd::identity::help; return ;;
239 *) log::error "Unknown flag: $1"; return 1 ;;
240 esac
241 done
242
243 [[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
244 [[ -z "$peer" ]] && { log::error "Missing required flag: --peer"; return 1; }
245
246 cmd::identity::_require_peer_exists "$peer" || return 1
247
248 local peer_type index
249 peer_type=$(cmd::identity::_resolve_peer_type "$peer" "$name")
250 index=$(identity::next_index "$name" "$peer_type")
251
252 local id_file
253 id_file=$(ctx::identity::path "${name}.identity")
254 json::identity_add_peer "$id_file" "$name" "$peer" "$peer_type" "$index" </dev/null
255 log::ok "Added '${peer}' to identity '${name}' (${peer_type} #${index})"
256}
257
258function cmd::identity::_require_peer_exists() {
259 local peer="${1:-}"
260 if [[ ! -f "$(ctx::clients)/${peer}.conf" ]]; then
261 log::error "Peer '${peer}' not found"
262 return 1
263 fi
264}
265
266function cmd::identity::_resolve_peer_type() {
267 local peer="${1:-}" identity_name="${2:-}"
268 local inferred
269 inferred=$(identity::infer "$peer")
270 if [[ -n "$inferred" ]]; then
271 echo "$inferred" | cut -d'|' -f2
272 else
273 peers::get_meta "$peer" "type" 2>/dev/null || echo "none"
274 fi
275}
276
277function cmd::identity::_remove() {
278 local name="" force=false
279 while [[ $# -gt 0 ]]; do
280 case "$1" in
281 --name) name="$2"; shift 2 ;;
282 --force) force=true; shift ;;
283 --help) cmd::identity::help; return ;;
284 *) log::error "Unknown flag: $1"; return 1 ;;
285 esac
286 done
287
288 [[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
289 identity::require_exists "$name" || return 1
290
291 local peers
292 peers=$(identity::peers "$name")
293
294 if [[ -n "$peers" ]]; then
295 local peer_list="${peers//$'\n'/, }"
296 log::warn "This will permanently remove identity '${name}' and ALL associated peers:"
297 log::warn " ${peer_list}"
298
299 if ! $force; then
300 ui::confirm "Continue?" || { log::info "Aborted"; return 0; }
301 fi
302
303 cmd::identity::_remove_all_peers "$peers"
304 peers::reload || return 1
305 fi
306
307 local id_file
308 id_file=$(ctx::identity::path "${name}.identity")
309 json::identity_remove "$id_file" </dev/null
310 log::ok "Identity '${name}' removed"
311}
312
313function cmd::identity::_remove_all_peers() {
314 local peers="${1:-}"
315 while IFS= read -r peer_name; do
316 [[ -z "$peer_name" ]] && continue
317 cmd::identity::_remove_peer "$peer_name"
318 done <<< "$peers"
319}
320
321function cmd::identity::_remove_peer() {
322 local peer_name="${1:-}"
323 local client_ip was_blocked=false
324
325 client_ip=$(peers::get_ip "$peer_name" 2>/dev/null) || client_ip=""
326 peers::is_blocked "$peer_name" && was_blocked=true
327
328 peers::purge "$peer_name" "$client_ip" "$was_blocked" || return 1
329 log::ok "Removed peer '${peer_name}'"
330}
331
332function cmd::identity::_migrate() {
333 local dry_run="false"
334 while [[ $# -gt 0 ]]; do
335 case "$1" in
336 --dry-run) dry_run="true"; shift ;;
337 --help) cmd::identity::help; return ;;
338 *) log::error "Unknown flag: $1"; return 1 ;;
339 esac
340 done
341
342 [[ "$dry_run" == "true" ]] && log::info "Dry run — no files will be written"
343 echo ""
344
345 [[ "$dry_run" == "false" ]] && mkdir -p "$(ctx::identities)"
346
347 local created=0 skipped=0 output
348 output=$(json::identity_migrate \
349 "$(ctx::identities)" \
350 "$(ctx::clients)" \
351 "$(ctx::meta)" \
352 "$dry_run")
353
354 while IFS='|' read -r action identity_name peer_name peer_type index; do
355 case "$action" in
356 create)
357 ui::identity::migrate_create "$peer_name" "$identity_name" "$peer_type" "$index"
358 (( created++ )) || true
359 ;;
360 skip)
361 ui::identity::migrate_skip "$peer_name"
362 (( skipped++ )) || true
363 ;;
364 esac
365 done <<< "$output"
366
367 ui::identity::migrate_summary "$created" "$skipped" "$dry_run"
368}
369
370function cmd::identity::_rule() {
371 local subcmd="${1:-show}"
372 shift || true
373
374 case "$subcmd" in
375 assign) cmd::identity::_rule_assign "$@" ;;
376 unassign) cmd::identity::_rule_unassign "$@" ;;
377 show) cmd::identity::_rule_show "$@" ;;
378 *)
379 log::error "Unknown rule subcommand '${subcmd}'. Available: assign, unassign, show"
380 return 1
381 ;;
382 esac
383}
384
385function cmd::identity::_rule_assign() {
386 local name="" rule="" migrate=false
387 while [[ $# -gt 0 ]]; do
388 case "$1" in
389 --name) name="$2"; shift 2 ;;
390 --rule) rule="$2"; shift 2 ;;
391 --migrate) migrate=true; shift ;;
392 *) log::error "Unknown flag: $1"; return 1 ;;
393 esac
394 done
395
396 [[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
397 [[ -z "$rule" ]] && { log::error "Missing required flag: --rule"; return 1; }
398 identity::require_exists "$name" || return 1
399 rule::exists "$rule" || { log::error "Rule not found: ${rule}"; return 1; }
400
401 local conflicts=()
402 while IFS= read -r peer_name; do
403 [[ -z "$peer_name" ]] && continue
404 local peer_rule
405 peer_rule=$(peers::get_meta "$peer_name" "rule" 2>/dev/null)
406 [[ "$peer_rule" == "$rule" ]] && conflicts+=("$peer_name")
407 done < <(identity::peers "$name")
408
409 if [[ ${#conflicts[@]} -gt 0 ]]; then
410 if ! $migrate; then
411 log::error "The following peers have '${rule}' as a direct rule: ${conflicts[*]}"
412 log::error "Use --migrate to remove direct rules and let the identity rule take over."
413 return 1
414 fi
415 # Migrate — remove direct rules from conflicting peers
416 for peer_name in "${conflicts[@]}"; do
417 local ip
418 ip=$(peers::get_ip "$peer_name")
419 rule::unapply "$rule" "$ip"
420 log::wg "Migrated '${rule}' from peer '${peer_name}' to identity '${name}'"
421 done
422 fi
423
424 local exit_code=0
425 identity::add_rule "$name" "$rule" || exit_code=$?
426
427 if [[ $exit_code -eq 2 ]]; then
428 log::warn "Rule '${rule}' is already assigned to identity '${name}'"
429 return 0
430 fi
431
432 log::ok "Rule '${rule}' assigned to identity '${name}'"
433
434 # Reapply rules if auto_apply
435 local auto
436 auto=$(identity::rule_flags "$name" "auto_apply")
437 if [[ "$auto" != "false" ]]; then
438 log::info "Reapplying rules for all peers in identity '${name}'..."
439 identity::reapply_rules "$name"
440 log::ok "Rules reapplied"
441 fi
442
443 # Warn about strict_rule
444 if policy::strict_rule "$(identity::policy "$name")"; then
445 log::warn "Strict rule is enabled for identity '${name}' — peer rules will not be additive"
446 fi
447}
448
449function cmd::identity::_rule_unassign() {
450 local name="" rule="" all=false
451 while [[ $# -gt 0 ]]; do
452 case "$1" in
453 --name) name="$2"; shift 2 ;;
454 --rule) rule="$2"; shift 2 ;;
455 --all) all=true; shift ;;
456 *) log::error "Unknown flag: $1"; return 1 ;;
457 esac
458 done
459
460 [[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
461 identity::require_exists "$name" || return 1
462
463 if $all; then
464 local rules
465 rules=$(identity::rules "$name")
466 if [[ -z "$rules" ]]; then
467 log::warn "Identity '${name}' has no rules assigned"
468 return 0
469 fi
470 identity::clear_rules "$name"
471 log::ok "All rules removed from identity '${name}'"
472 cmd::identity::_reapply_after_unassign "$name"
473 return 0
474 fi
475
476 [[ -z "$rule" ]] && {
477 log::error "Missing required flag: --rule (or use --all to remove all)"
478 return 1
479 }
480
481 identity::remove_rule "$name" "$rule"
482 local exit_code=$?
483 if [[ $exit_code -ne 0 ]]; then
484 log::error "Rule '${rule}' is not assigned to identity '${name}'"
485 return 1
486 fi
487
488 log::ok "Rule '${rule}' removed from identity '${name}'"
489 cmd::identity::_reapply_after_unassign "$name"
490}
491
492function cmd::identity::_reapply_after_unassign() {
493 local name="${1:-}"
494 local auto
495 auto=$(identity::rule_flags "$name" "auto_apply")
496 if [[ "$auto" != "false" ]]; then
497 log::info "Reapplying rules for all peers in identity '${name}'..."
498 identity::reapply_rules "$name"
499 log::ok "Rules reapplied"
500 else
501 log::info "Note: auto_apply is disabled — run 'wgctl audit --fix' to update fw rules"
502 fi
503}
504
505function cmd::identity::_rule_show() {
506 local name=""
507 while [[ $# -gt 0 ]]; do
508 case "$1" in
509 --name) name="$2"; shift 2 ;;
510 *) log::error "Unknown flag: $1"; return 1 ;;
511 esac
512 done
513
514 [[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
515 identity::require_exists "$name" || return 1
516
517 local rules policy strict auto
518 rules=$(identity::rules "$name")
519 policy=$(identity::policy "$name")
520 strict=$(identity::rule_flags "$name" "strict_rule")
521 auto=$(identity::rule_flags "$name" "auto_apply")
522
523 echo ""
524 ui::row "Identity" "$name"
525 ui::row "Policy" "$policy"
526 ui::row "Strict rule" "$(ui::bool "$strict")"
527 ui::row "Auto apply" "$(ui::bool "$auto")"
528 echo ""
529
530 if [[ -z "$rules" ]]; then
531 ui::row "Rules" "— none assigned"
532 else
533 printf " %-20s\n" "Rules:"
534 while IFS= read -r rule_name; do
535 [[ -z "$rule_name" ]] && continue
536 printf " · %s\n" "$rule_name"
537 done <<< "$rules"
538 fi
539 echo ""
540}
541
542function cmd::identity::_options() {
543 local name="" new_policy=""
544 local set_strict="" set_auto=""
545
546 while [[ $# -gt 0 ]]; do
547 case "$1" in
548 --name) name="$2"; shift 2 ;;
549 --policy) new_policy="$2"; shift 2 ;;
550 --set-strict-rule) set_strict="true"; shift ;;
551 --unset-strict-rule) set_strict="false"; shift ;;
552 --set-auto-apply) set_auto="true"; shift ;;
553 --unset-auto-apply) set_auto="false"; shift ;;
554 *) log::error "Unknown flag: $1"; return 1 ;;
555 esac
556 done
557
558 [[ -z "$name" ]] && { log::error "Missing required flag: --name"; return 1; }
559 identity::require_exists "$name" || return 1
560
561 local changed=false
562
563 if [[ -n "$new_policy" ]]; then
564 policy::require_exists "$new_policy" || return 1
565 identity::set_policy "$name" "$new_policy"
566 log::ok "Policy set to '${new_policy}' for identity '${name}'"
567 changed=true
568 fi
569
570 if [[ -n "$set_strict" ]]; then
571 identity::set_rule_flag "$name" "strict_rule" "$set_strict"
572 if [[ "$set_strict" == "true" ]]; then
573 log::ok "Strict rule enabled for identity '${name}' — peer rules will not be additive"
574 else
575 log::ok "Strict rule disabled for identity '${name}' — peer rules will be additive"
576 fi
577 changed=true
578 fi
579
580 if [[ -n "$set_auto" ]]; then
581 identity::set_rule_flag "$name" "auto_apply" "$set_auto"
582 if [[ "$set_auto" == "true" ]]; then
583 log::ok "Auto apply enabled for identity '${name}'"
584 else
585 log::ok "Auto apply disabled for identity '${name}'"
586 fi
587 changed=true
588 fi
589
590 if ! $changed; then
591 cmd::identity::_rule_show --name "$name"
592 fi
593}
594
595function cmd::identity::_output_json() {
596 local data
597 data=$(identity::list_data 2>/dev/null)
598
599 local -a identities=()
600 while IFS='|' read -r name peer_count types rules policy; do
601 [[ -z "$name" ]] && continue
602
603 # Build rules array
604 local rules_json="[]"
605 if [[ -n "$rules" ]]; then
606 local rules_array
607 rules_array=$(echo "$rules" | tr ',' '\n' | \
608 while IFS= read -r r; do [[ -n "$r" ]] && printf '"%s",' "$r"; done | sed 's/,$//')
609 rules_json="[${rules_array}]"
610 fi
611
612 # Build types array (was comma-separated string)
613 local types_json="[]"
614 if [[ -n "$types" ]]; then
615 local types_array
616 types_array=$(echo "$types" | tr ',' '\n' | \
617 while IFS= read -r t; do [[ -n "$t" ]] && printf '"%s",' "$t"; done | sed 's/,$//')
618 types_json="[${types_array}]"
619 fi
620
621 identities+=("$(printf '{"name":"%s","peer_count":%s,"types":%s,"rules":%s,"policy":"%s"}' \
622 "$name" "$peer_count" "$types_json" "$rules_json" "$policy")")
623 done <<< "$data"
624
625 local count=${#identities[@]}
626 local array
627 array=$(printf '%s\n' "${identities[@]:-}" | paste -sd ',' -)
628 printf '{"identities":[%s]}' "${array:-}" | json::envelope "identity list" "$count"
629}
630