bytelyst-devops-tools/remove_user_guided.sh

593 lines
18 KiB
Bash
Executable File
Raw 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
# Removes a specified user from all repositories matching a prefix pattern
# under a given username or organization with interactive prompts
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
# 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 ""
}
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
echo -e -n "${BLUE}🔐 Enter your GitHub token: ${NC}"
read -s -r GITHUB_TOKEN < /dev/tty
echo ""
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)
local login
login=$(echo "$response" | jq -r '.login // empty' 2>/dev/null)
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
echo -e -n "${BLUE}🏢 Organization/Username: ${NC}"
read -r ROOT_USER < /dev/tty
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
echo -e -n "${BLUE}👤 Username to remove: ${NC}"
read -r USER_TO_REMOVE < /dev/tty
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}"
echo -e -n "${BLUE}Are you sure this is correct? (yes/no): ${NC}"
read -r confirm < /dev/tty
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
echo -e -n "${BLUE}🔍 Repository pattern: ${NC}"
read -r REPO_PREFIX < /dev/tty
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 ""
echo -e -n "${BLUE}Is this pattern correct? (yes/no): ${NC}"
read -r confirm < /dev/tty
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
echo -e -n "${BLUE}Select option (1 or 2): ${NC}"
read -r option < /dev/tty
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}"
echo -e -n "${BLUE}Are you absolutely sure? Type 'YES' to confirm: ${NC}"
read -r final_confirm < /dev/tty
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 ""
echo -e -n "${BLUE}Enable verbose logging? (y/n): ${NC}"
read -r verbose_choice < /dev/tty
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 ""
echo -e -n "${BLUE}Ready to proceed? (yes/no): ${NC}"
read -r proceed < /dev/tty
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"
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() {
# 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