Ostatnio aktywny 1 month ago

nuno zrewidował ten Gist 1 month ago. Przejdź do rewizji

1 file changed, 630 insertions

identity.command.sh(stworzono plik)

@@ -0,0 +1,630 @@
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 +
15 + function 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 +
40 + function cmd::identity::help() {
41 + cat <<EOF
42 + Usage: wgctl identity <subcommand> [options]
43 +
44 + Manage peer identities — group peers by person/device owner.
45 +
46 + Subcommands:
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 +
62 + Examples:
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
70 + EOF
71 + }
72 +
73 + # ============================================
74 + # Run
75 + # ============================================
76 +
77 + function 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 +
106 + function 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 +
129 + function 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 +
193 + function 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 +
208 + function 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 +
232 + function 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 +
258 + function 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 +
266 + function 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 +
277 + function 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 +
313 + function 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 +
321 + function 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 +
332 + function 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 +
370 + function 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 +
385 + function 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 +
449 + function 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 +
492 + function 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 +
505 + function 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 +
542 + function 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 +
595 + function 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 +
Nowsze Starsze