bytelyst-devops-tools/interactive_user_removal.sh

614 lines
18 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/bin/bash
# Interactive GitHub User Removal Script (Robust Version)
# Handles input properly across different terminal environments
set -euo pipefail
# Color definitions
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly BLUE='\033[0;34m'
readonly CYAN='\033[0;36m'
readonly PURPLE='\033[0;35m'
readonly BOLD='\033[1m'
readonly NC='\033[0m' # No Color
# Global variables
GITHUB_TOKEN=""
ROOT_USER=""
USER_TO_REMOVE=""
REPO_PREFIX=""
DRY_RUN=false
VERBOSE=false
# Statistics
TOTAL_REPOS_FOUND=0
MATCHING_REPOS=0
COLLABORATOR_REPOS=0
SUCCESSFUL_REMOVALS=0
FAILED_REMOVALS=0
ALREADY_REMOVED=0
# Function to safely read input
safe_read() {
local prompt="$1"
local var_name="$2"
local is_secret="${3:-false}"
if [[ "$is_secret" == "true" ]]; then
read -s -p "$prompt" "$var_name"
echo ""
else
read -p "$prompt" "$var_name"
fi
}
# Utility functions
log_info() {
echo -e "${BLUE} $1${NC}"
}
log_success() {
echo -e "${GREEN}$1${NC}"
}
log_warning() {
echo -e "${YELLOW}⚠️ $1${NC}"
}
log_error() {
echo -e "${RED}$1${NC}"
}
log_verbose() {
if [[ "$VERBOSE" == "true" ]]; then
echo -e "${CYAN}🔍 $1${NC}"
fi
}
log_header() {
echo -e "\n${BOLD}${PURPLE}$1${NC}"
echo -e "${PURPLE}$(printf '=%.0s' {1..60})${NC}"
}
show_welcome() {
clear
cat << 'EOF'
╔══════════════════════════════════════════════════════════╗
║ ║
║ 🚀 GitHub User Removal Tool 🚀 ║
║ ║
║ Remove users from repositories with ease! ║
║ ║
╚══════════════════════════════════════════════════════════╝
EOF
echo ""
log_info "Welcome to the Interactive GitHub User Removal Script!"
echo ""
}
# Check if running interactively
check_interactive() {
if [[ ! -t 0 ]] || [[ ! -t 1 ]]; then
echo "This script requires an interactive terminal."
echo "Please run it directly in your terminal, not through pipes or redirects."
exit 1
fi
}
prompt_github_token() {
log_header "🔑 GitHub Authentication"
echo -e "${YELLOW}Please provide your GitHub Personal Access Token:${NC}"
echo -e "${CYAN}💡 The token should have 'repo' and 'admin:org' permissions${NC}"
echo -e "${CYAN}💡 You can create one at: https://github.com/settings/tokens${NC}"
echo ""
while true; do
safe_read "🔐 Enter your GitHub token: " GITHUB_TOKEN true
if [[ -z "$GITHUB_TOKEN" ]]; then
log_error "Token cannot be empty. Please try again."
echo ""
continue
fi
# Validate token
log_info "Validating token..."
local response
response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" "https://api.github.com/user" 2>/dev/null || echo "")
local login
login=$(echo "$response" | jq -r '.login // empty' 2>/dev/null || echo "")
if [[ -z "$login" ]]; then
log_error "Invalid or expired GitHub token. Please try again."
echo ""
continue
fi
log_success "Token validated successfully! (Authenticated as: $login)"
echo ""
break
done
}
prompt_root_user() {
log_header "👤 Target Organization/User"
echo -e "${YELLOW}Enter the GitHub username or organization name:${NC}"
echo -e "${CYAN}💡 This is where we'll look for repositories${NC}"
echo -e "${CYAN}💡 Examples: 'mycompany', 'john-doe', 'my-organization'${NC}"
echo ""
while true; do
safe_read "🏢 Organization/Username: " ROOT_USER
if [[ -z "$ROOT_USER" ]]; then
log_error "Username/organization cannot be empty. Please try again."
echo ""
continue
fi
# Validate username format
if [[ ! "$ROOT_USER" =~ ^[a-zA-Z0-9._-]+$ ]]; then
log_error "Invalid username format. Use only letters, numbers, dots, underscores, and hyphens."
echo ""
continue
fi
log_success "Target set to: $ROOT_USER"
echo ""
break
done
}
prompt_user_to_remove() {
log_header "🎯 User to Remove"
echo -e "${YELLOW}Enter the username you want to remove from repositories:${NC}"
echo -e "${CYAN}💡 This user will be removed as a collaborator from matching repositories${NC}"
echo -e "${CYAN}💡 They will lose access to private repositories (if applicable)${NC}"
echo ""
while true; do
safe_read "👤 Username to remove: " USER_TO_REMOVE
if [[ -z "$USER_TO_REMOVE" ]]; then
log_error "Username cannot be empty. Please try again."
echo ""
continue
fi
# Validate username format
if [[ ! "$USER_TO_REMOVE" =~ ^[a-zA-Z0-9._-]+$ ]]; then
log_error "Invalid username format. Use only letters, numbers, dots, underscores, and hyphens."
echo ""
continue
fi
# Confirmation prompt
echo ""
echo -e "${YELLOW}⚠️ You are about to remove user '${BOLD}$USER_TO_REMOVE${NC}${YELLOW}' from repositories${NC}"
local confirm
safe_read "Are you sure this is correct? (yes/no): " confirm
if [[ "$confirm" =~ ^[Yy]([Ee][Ss])?$ ]]; then
log_success "User to remove set to: $USER_TO_REMOVE"
echo ""
break
else
echo -e "${YELLOW}Let's try again...${NC}"
echo ""
fi
done
}
prompt_repo_prefix() {
log_header "📁 Repository Filter"
echo -e "${YELLOW}Enter a repository name pattern to filter repositories:${NC}"
echo -e "${CYAN}💡 Pattern examples:${NC}"
echo -e "${CYAN} • '*' - All repositories${NC}"
echo -e "${CYAN} • 'myproject-' - Repos starting with 'myproject-'${NC}"
echo -e "${CYAN} • '*api*' - Repos containing 'api'${NC}"
echo -e "${CYAN} • 'web-app' - Repos starting with 'web-app'${NC}"
echo ""
while true; do
safe_read "🔍 Repository pattern: " REPO_PREFIX
if [[ -z "$REPO_PREFIX" ]]; then
log_error "Pattern cannot be empty. Please try again."
echo ""
continue
fi
# Show what this pattern will match
echo ""
case "$REPO_PREFIX" in
"*")
log_info "This will match ALL repositories under $ROOT_USER"
;;
*"*"*)
log_info "This will match repositories containing: ${REPO_PREFIX//\*/[text]}"
;;
*)
log_info "This will match repositories starting with: $REPO_PREFIX"
;;
esac
echo ""
local confirm
safe_read "Is this pattern correct? (yes/no): " confirm
if [[ "$confirm" =~ ^[Yy]([Ee][Ss])?$ ]]; then
log_success "Repository pattern set to: $REPO_PREFIX"
echo ""
break
fi
done
}
prompt_options() {
log_header "⚙️ Operation Options"
echo -e "${YELLOW}Choose your operation mode:${NC}"
echo ""
echo -e "${CYAN}1) 🔍 Dry Run - Preview what would be done (Recommended)${NC}"
echo -e "${CYAN}2) 🚀 Execute - Perform the actual removal${NC}"
echo ""
while true; do
local option
safe_read "Select option (1 or 2): " option
case "$option" in
1)
DRY_RUN=true
log_info "Dry run mode selected - no changes will be made"
break
;;
2)
DRY_RUN=false
echo ""
echo -e "${YELLOW}⚠️ WARNING: This will make actual changes to repositories!${NC}"
local final_confirm
safe_read "Are you absolutely sure? Type 'YES' to confirm: " final_confirm
if [[ "$final_confirm" == "YES" ]]; then
log_info "Execute mode selected - changes will be made"
break
else
log_info "Switching to dry run mode for safety"
DRY_RUN=true
break
fi
;;
*)
log_error "Please enter 1 or 2"
;;
esac
done
echo ""
local verbose_choice
safe_read "Enable verbose logging? (y/n): " verbose_choice
if [[ "$verbose_choice" =~ ^[Yy]$ ]]; then
VERBOSE=true
log_info "Verbose logging enabled"
else
VERBOSE=false
log_info "Standard logging enabled"
fi
echo ""
}
show_summary() {
log_header "📋 Operation Summary"
echo -e "${BOLD}Configuration:${NC}"
echo -e " 🏢 Organization/User: ${CYAN}$ROOT_USER${NC}"
echo -e " 👤 User to remove: ${CYAN}$USER_TO_REMOVE${NC}"
echo -e " 🔍 Repository pattern: ${CYAN}$REPO_PREFIX${NC}"
echo -e " ⚙️ Mode: ${CYAN}$([ "$DRY_RUN" = true ] && echo "Dry Run" || echo "Execute")${NC}"
echo -e " 📝 Logging: ${CYAN}$([ "$VERBOSE" = true ] && echo "Verbose" || echo "Standard")${NC}"
echo ""
local proceed
safe_read "Ready to proceed? (yes/no): " proceed
if [[ ! "$proceed" =~ ^[Yy]([Ee][Ss])?$ ]]; then
log_warning "Operation cancelled by user"
exit 0
fi
}
# Fetch all repositories for a user or organization
fetch_all_repos() {
local owner="$1"
local all_repos=()
local page=1
log_verbose "Fetching repositories for: $owner"
while true; do
local response
response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/users/$owner/repos?per_page=100&page=$page&type=all" 2>/dev/null)
# Check if response is valid JSON
if ! echo "$response" | jq empty 2>/dev/null; then
log_warning "Invalid response from GitHub API for page $page"
break
fi
# Check for errors
local error_message
error_message=$(echo "$response" | jq -r '.message // empty' 2>/dev/null)
if [[ -n "$error_message" ]]; then
if [[ "$error_message" == "Not Found" ]]; then
# Try as organization
log_verbose "User not found, trying as organization..."
response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/orgs/$owner/repos?per_page=100&page=$page&type=all" 2>/dev/null)
if ! echo "$response" | jq empty 2>/dev/null; then
log_error "Could not fetch repositories for '$owner' (not found as user or organization)"
exit 1
fi
else
log_error "GitHub API error: $error_message"
exit 1
fi
fi
local repos
repos=$(echo "$response" | jq -r '.[].full_name // empty' 2>/dev/null)
if [[ -z "$repos" ]]; then
break
fi
while IFS= read -r repo; do
if [[ -n "$repo" && "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then
all_repos+=("$repo")
log_verbose "Found repository: $repo"
fi
done <<< "$repos"
((page++))
done
TOTAL_REPOS_FOUND=${#all_repos[@]}
if [[ ${#all_repos[@]} -gt 0 ]]; then
printf '%s\n' "${all_repos[@]}"
fi
}
# Check if repository name matches prefix pattern
matches_prefix() {
local repo_full_name="$1"
local pattern="$2"
local repo_name
repo_name=$(basename "$repo_full_name")
# Convert shell wildcard pattern to bash pattern
case "$pattern" in
"*")
return 0 # Match all
;;
*"*"*)
# Pattern contains wildcards
if [[ "$repo_name" == $pattern ]]; then
return 0
fi
;;
*)
# Simple prefix match
if [[ "$repo_name" == "$pattern"* ]]; then
return 0
fi
;;
esac
return 1
}
# Check if user is a collaborator on a repository
is_collaborator() {
local repo="$1"
local user="$2"
local response
response=$(curl -s -w "%{http_code}" -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$repo/collaborators/$user" 2>/dev/null)
local http_code="${response: -3}"
case "$http_code" in
204)
return 0 # User is a collaborator
;;
404)
return 1 # User is not a collaborator
;;
403)
log_verbose "Access forbidden for $repo (insufficient permissions)"
return 1
;;
*)
log_verbose "Failed to check collaboration status for $user on $repo (HTTP: $http_code)"
return 1
;;
esac
}
# Remove user from repository
remove_user_from_repo() {
local repo="$1"
local user="$2"
if [[ "$DRY_RUN" == "true" ]]; then
log_info "[DRY RUN] Would remove $user from $repo"
((SUCCESSFUL_REMOVALS++))
return 0
fi
log_verbose "Removing $user from $repo..."
local response
response=$(curl -s -w "%{http_code}" -X DELETE \
-H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$repo/collaborators/$user" 2>/dev/null)
local http_code="${response: -3}"
case "$http_code" in
204)
log_success "Successfully removed $user from $repo"
((SUCCESSFUL_REMOVALS++))
return 0
;;
404)
log_info "User $user was not a collaborator on $repo (already removed)"
((ALREADY_REMOVED++))
return 0
;;
*)
log_error "Failed to remove $user from $repo (HTTP: $http_code)"
((FAILED_REMOVALS++))
return 1
;;
esac
}
# Show final results
show_final_summary() {
log_header "🎉 Operation Complete"
echo -e "${BOLD}📊 Statistics:${NC}"
echo -e " 📁 Repositories scanned: ${CYAN}$TOTAL_REPOS_FOUND${NC}"
echo -e " 🔍 Repositories matching pattern: ${CYAN}$MATCHING_REPOS${NC}"
echo -e " 👤 Repositories where user was collaborator: ${CYAN}$COLLABORATOR_REPOS${NC}"
echo ""
echo -e " ${GREEN}✅ Successful removals: $SUCCESSFUL_REMOVALS${NC}"
echo -e " ${YELLOW} Already removed: $ALREADY_REMOVED${NC}"
echo -e " ${RED}❌ Failed removals: $FAILED_REMOVALS${NC}"
if [[ $((SUCCESSFUL_REMOVALS + ALREADY_REMOVED + FAILED_REMOVALS)) -gt 0 ]]; then
local success_rate=$((((SUCCESSFUL_REMOVALS + ALREADY_REMOVED) * 100) / (SUCCESSFUL_REMOVALS + ALREADY_REMOVED + FAILED_REMOVALS)))
echo -e " 📈 Success rate: ${CYAN}${success_rate}%${NC}"
fi
echo ""
if [[ "$DRY_RUN" == "true" ]]; then
log_warning "This was a dry run - no actual changes were made"
echo -e "${BLUE}💡 To perform the actual removal, run the script again and select 'Execute' mode${NC}"
else
if [[ $FAILED_REMOVALS -gt 0 ]]; then
log_warning "Some operations failed. Check the logs above for details."
else
log_success "All operations completed successfully!"
fi
fi
echo ""
echo -e "${CYAN}🙏 Thank you for using the GitHub User Removal Tool!${NC}"
}
# Main execution function
main() {
# Check if running interactively
check_interactive
# Show welcome screen
show_welcome
# Interactive prompts
prompt_github_token
prompt_root_user
prompt_user_to_remove
prompt_repo_prefix
prompt_options
# Show summary and confirm
show_summary
# Start processing
log_header "🚀 Processing Repositories"
# Fetch all repositories
log_info "Discovering repositories for $ROOT_USER..."
local repos_output
repos_output=$(fetch_all_repos "$ROOT_USER")
if [[ -z "$repos_output" ]]; then
log_warning "No repositories found for '$ROOT_USER'"
exit 0
fi
log_success "Found $TOTAL_REPOS_FOUND repositories"
# Filter repositories by prefix
log_info "Filtering repositories by pattern '$REPO_PREFIX'..."
local matching_repos=()
while IFS= read -r repo; do
if [[ -n "$repo" ]] && matches_prefix "$repo" "$REPO_PREFIX"; then
matching_repos+=("$repo")
log_verbose "Matched: $repo"
fi
done <<< "$repos_output"
MATCHING_REPOS=${#matching_repos[@]}
if [[ $MATCHING_REPOS -eq 0 ]]; then
log_warning "No repositories match the pattern '$REPO_PREFIX'"
echo ""
log_info "Available repositories:"
while IFS= read -r repo; do
log_info "$(basename "$repo")"
done <<< "$repos_output"
exit 0
fi
log_success "Found $MATCHING_REPOS repositories matching pattern '$REPO_PREFIX'"
# Check collaborator status and remove
log_info "Processing repositories..."
echo ""
local current=0
for repo in "${matching_repos[@]}"; do
((current++))
printf "\r${CYAN}Progress: [%3d%%] %d/%d repositories processed${NC}" \
$((current * 100 / MATCHING_REPOS)) "$current" "$MATCHING_REPOS"
if is_collaborator "$repo" "$USER_TO_REMOVE"; then
((COLLABORATOR_REPOS++))
echo # New line after progress
remove_user_from_repo "$repo" "$USER_TO_REMOVE"
fi
done
echo # New line after progress
# Show final summary
show_final_summary
if [[ $FAILED_REMOVALS -gt 0 ]]; then
exit 1
else
exit 0
fi
}
# Script execution
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi