#!/usr/bin/env bash # dev-remove.sh - Remove a dev account # Usage: dev-remove.sh [--archive] [--dry-run] # # Removes: # - Unix user account # - Home directory (or archives if --archive flag) # - Maubot plugin symlinks # # NOTE: Revokes Forgejo SSH keys automatically (prevents zombie access). # Does NOT delete Forgejo account (too dangerous - may delete repos). # Manual account suspension still recommended via Forgejo admin UI. set -euo pipefail MAUBOT_PLUGINS_DIR="/var/lib/maubot/plugins" ARCHIVE_DIR="/var/backups/devs" DRY_RUN=false # Colors for output RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' # No Color log_info() { echo -e "${GREEN}[INFO]${NC} $1"; } log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } log_error() { echo -e "${RED}[ERROR]${NC} $1" >&2; } log_dry() { echo -e "${CYAN}[DRY-RUN]${NC} $1"; } revoke_forgejo_keys() { local username="$1" local token_file="/run/secrets/forgejo-api-token" local forgejo_url="http://localhost:3000" if [[ "$DRY_RUN" == true ]]; then log_dry "Would revoke Forgejo SSH keys for '$username'" return 0 fi # Check if token file exists if [[ ! -r "$token_file" ]]; then log_warn "Forgejo API token not found - cannot revoke keys automatically" log_warn "MANUAL ACTION REQUIRED: Delete SSH keys for '$username' in Forgejo" return 1 fi local token token=$(cat "$token_file") log_info "Revoking Forgejo SSH keys for '$username'..." # Get list of user's keys local keys_json keys_json=$(curl -s --connect-timeout 5 --max-time 30 \ -H "Authorization: token $token" \ "$forgejo_url/api/v1/admin/users/$username/keys" 2>/dev/null) || true if [[ -z "$keys_json" || "$keys_json" == "null" ]]; then log_info "No Forgejo keys found for '$username'" return 0 fi # Parse key IDs and delete each one # Using grep/sed since jq may not be available local key_ids key_ids=$(echo "$keys_json" | grep -o '"id":[0-9]*' | sed 's/"id"://' || true) if [[ -z "$key_ids" ]]; then log_info "No Forgejo keys found for '$username'" return 0 fi local revoked=0 local failed=0 for key_id in $key_ids; do local http_code http_code=$(curl -s -o /dev/null -w "%{http_code}" \ --connect-timeout 5 --max-time 30 \ -X DELETE "$forgejo_url/api/v1/admin/users/$username/keys/$key_id" \ -H "Authorization: token $token" 2>/dev/null) || true if [[ "$http_code" == "204" ]]; then ((revoked++)) || true else log_warn "Failed to revoke key $key_id (HTTP $http_code)" ((failed++)) || true fi done if [[ $revoked -gt 0 ]]; then log_info "Revoked $revoked Forgejo SSH key(s)" fi if [[ $failed -gt 0 ]]; then log_warn "$failed key(s) failed to revoke - manual cleanup may be needed" return 1 fi return 0 } usage() { echo "Usage: $0 [--archive] [--dry-run]" echo "" echo "Arguments:" echo " username - Dev's username to remove" echo " --archive - Archive home directory instead of deleting" echo " --dry-run - Show what would be done without making changes" echo "" echo "Example:" echo " $0 alice # Delete user and home directory" echo " $0 alice --archive # Archive home directory before deleting" echo " $0 alice --dry-run # Preview what would be removed" exit 1 } validate_username() { local username="$1" # Check if user exists if ! id "$username" &>/dev/null; then log_error "User '$username' does not exist" exit 1 fi # Safety check - don't delete system users local uid uid=$(id -u "$username") if [[ $uid -lt 1000 ]]; then log_error "Refusing to delete system user '$username' (UID $uid < 1000)" exit 1 fi # Safety check - don't delete important users case "$username" in root|dan|admin|maubot|postgres|nginx) log_error "Refusing to delete protected user '$username'" exit 1 ;; esac } remove_maubot_symlinks() { local username="$1" if [[ "$DRY_RUN" == true ]]; then log_dry "Checking maubot plugin symlinks for '$username'..." else log_info "Removing maubot plugin symlinks for '$username'..." fi # Remove any symlinks that point to user's home directory local count=0 for symlink in "$MAUBOT_PLUGINS_DIR"/*; do if [[ -L "$symlink" ]]; then local target target=$(readlink "$symlink") if [[ "$target" == "/home/$username/"* ]]; then if [[ "$DRY_RUN" == true ]]; then log_dry "Would remove symlink: $symlink -> $target" else rm "$symlink" log_info "Removed symlink: $symlink" fi ((count++)) || true fi fi done # Also remove any plugins named after user for symlink in "$MAUBOT_PLUGINS_DIR/${username}-"*; do if [[ -e "$symlink" || -L "$symlink" ]]; then if [[ "$DRY_RUN" == true ]]; then log_dry "Would remove: $symlink" else rm -f "$symlink" log_info "Removed: $symlink" fi ((count++)) || true fi done if [[ $count -eq 0 ]]; then log_info "No maubot symlinks found for '$username'" fi } archive_home() { local username="$1" local home_dir="/home/$username" if [[ ! -d "$home_dir" ]]; then log_warn "Home directory does not exist: $home_dir" return fi local archive_name archive_name="${username}_$(date +%Y%m%d_%H%M%S).tar.gz" local archive_path="$ARCHIVE_DIR/$archive_name" if [[ "$DRY_RUN" == true ]]; then log_dry "Would archive home directory to: $archive_path" else log_info "Archiving home directory..." mkdir -p "$ARCHIVE_DIR" tar -czf "$archive_path" -C /home "$username" chmod 600 "$archive_path" log_info "Archived to: $archive_path" fi } remove_user() { local username="$1" if [[ "$DRY_RUN" == true ]]; then log_dry "Would kill processes for user '$username'" log_dry "Would remove user '$username' and home directory" else log_info "Removing user '$username'..." # Kill any running processes pkill -u "$username" 2>/dev/null || true sleep 1 # Remove user and home directory userdel -r "$username" 2>/dev/null || { # If userdel -r fails, try without -r and manually remove home userdel "$username" 2>/dev/null || true rm -rf "/home/${username:?}" } log_info "User removed" fi } main() { if [[ $# -lt 1 ]]; then usage fi local username="$1" local archive=false # Parse flags shift while [[ $# -gt 0 ]]; do case "$1" in --archive) archive=true shift ;; --dry-run) DRY_RUN=true shift ;; *) log_error "Unknown option: $1" usage ;; esac done # Must run as root if [[ $EUID -ne 0 ]]; then log_error "This script must be run as root" exit 1 fi validate_username "$username" if [[ "$DRY_RUN" == true ]]; then echo "" log_dry "Preview of removing user '$username':" if [[ "$archive" == true ]]; then log_dry "Home directory would be archived to $ARCHIVE_DIR" else log_dry "Home directory would be PERMANENTLY DELETED" fi echo "" else # Confirm deletion echo "" log_warn "This will remove user '$username' and all their data!" if [[ "$archive" == true ]]; then echo "Home directory will be archived to $ARCHIVE_DIR" else echo "Home directory will be PERMANENTLY DELETED" fi echo "" read -rp "Are you sure? (yes/no): " confirm if [[ "$confirm" != "yes" ]]; then log_info "Aborted" exit 0 fi fi # Revoke Forgejo SSH keys BEFORE removing user (prevents zombie access) revoke_forgejo_keys "$username" remove_maubot_symlinks "$username" if [[ "$archive" == true ]]; then archive_home "$username" fi remove_user "$username" if [[ "$DRY_RUN" == true ]]; then log_dry "Dry run complete - no changes made" else log_info "Dev '$username' removed successfully" # Remind admin to handle Forgejo account (keys already revoked) echo "" log_warn "RECOMMENDED: Suspend Forgejo account for '$username'" echo " SSH keys have been revoked automatically." echo " To fully disable the account, visit: https://git.clarun.xyz/admin/users" echo " Find user '$username' and either:" echo " - Prohibit login (keeps repos/PRs intact)" echo " - Delete user (WARNING: may delete their repos)" echo "" fi } main "$@"