#!/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: Does NOT delete Forgejo account automatically (too dangerous). # Manual suspension required 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"; } 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 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" log_dry "Would print Forgejo suspension reminder" else log_info "Dev '$username' removed successfully" # Remind admin to handle Forgejo account echo "" log_warn "ACTION REQUIRED: Suspend Forgejo account for '$username'" echo " 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 "$@"