- Remove stderr suppression from ssh-keygen (show errors) - Add curl timeouts (--connect-timeout 5 --max-time 30) - Add || true to arithmetic increments for set -e safety Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
326 lines
9.2 KiB
Bash
Executable file
326 lines
9.2 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# dev-remove.sh - Remove a dev account
|
|
# Usage: dev-remove.sh <username> [--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 <username> [--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 "$@"
|