bytelyst-devops-tools/remove_user_interactive.sh

713 lines
21 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 User Removal Tool for GitHub Repositories
# Supports wildcard matching and handles both user and organization repositories
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_PATTERN=""
CONFIRMATION_MODE="ask" # ask, yes_all, no_all
TOTAL_REPOS=0
PROCESSED_REPOS=0
SUCCESSFUL_REMOVALS=0
FAILED_REMOVALS=0
SKIPPED_REMOVALS=0
NON_INTERACTIVE=false
# Required tools
REQUIRED_TOOLS=(curl jq)
# 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_progress() {
echo -e "${CYAN}🔄 $1${NC}"
}
log_header() {
echo -e "\n${BOLD}${PURPLE}$1${NC}"
echo -e "${PURPLE}$(printf '=%.0s' {1..50})${NC}"
}
# Check required tools
check_dependencies() {
log_header "Checking Dependencies"
for tool in "${REQUIRED_TOOLS[@]}"; do
if ! command -v "$tool" &>/dev/null; then
log_error "Required tool '$tool' is not installed. Please install it and try again."
exit 1
fi
log_success "$tool is available"
done
}
# Input collection functions
collect_github_token() {
log_header "GitHub Authentication"
echo -e "${YELLOW}Enter your GitHub Personal Access Token:${NC}"
echo -e "${CYAN}(Token needs 'repo' and 'admin:org' permissions)${NC}"
read -s -r -p "Token: " GITHUB_TOKEN
echo
if [[ -z "$GITHUB_TOKEN" ]]; then
log_error "GitHub token cannot be empty"
exit 1
fi
# Validate token
log_progress "Validating GitHub token..."
local response
response=$(curl -s -w "%{http_code}" -H "Authorization: token $GITHUB_TOKEN" https://api.github.com/user)
local http_code="${response: -3}"
if [[ "$http_code" != "200" ]]; then
log_error "Invalid GitHub token or insufficient permissions (HTTP: $http_code)"
if [[ "$http_code" == "401" ]]; then
log_error "Token is invalid or expired"
elif [[ "$http_code" == "403" ]]; then
log_error "Token lacks required permissions (need 'repo' and 'admin:org')"
fi
exit 1
fi
# Get user info for confirmation
local user_info
user_info=$(echo "${response%???}" | jq -r '.login // "unknown"' 2>/dev/null)
log_success "GitHub token validated successfully (authenticated as: $user_info)"
}
collect_root_user() {
log_header "Root Account Information"
echo -e "${YELLOW}Enter the root GitHub username/organization:${NC}"
read -r -p "Root user: " ROOT_USER
if [[ -z "$ROOT_USER" ]]; then
log_error "Root user cannot be empty"
exit 1
fi
log_success "Root user set to: $ROOT_USER"
}
collect_user_to_remove() {
log_header "User Removal Target"
echo -e "${YELLOW}Enter the username to remove from repositories:${NC}"
read -r -p "User to remove: " USER_TO_REMOVE
if [[ -z "$USER_TO_REMOVE" ]]; then
log_error "User to remove cannot be empty"
exit 1
fi
log_success "Target user: $USER_TO_REMOVE"
}
collect_repo_pattern() {
log_header "Repository Pattern"
echo -e "${YELLOW}Enter repository pattern (supports wildcards):${NC}"
echo -e "${CYAN}Examples: *-go-api*, frontend-*, *-service, specific-repo${NC}"
read -r -p "Repository pattern: " REPO_PATTERN
if [[ -z "$REPO_PATTERN" ]]; then
log_error "Repository pattern cannot be empty"
exit 1
fi
log_success "Repository pattern: $REPO_PATTERN"
}
# GitHub API functions
fetch_user_repos() {
local user="$1"
local page=1
local all_repos=()
# Don't log here to avoid capturing log messages as repo names
while true; do
local response
response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/users/$user/repos?per_page=100&page=$page&type=all")
# Check if response is valid JSON and contains repositories
if ! echo "$response" | jq empty 2>/dev/null; then
log_error "Invalid JSON response from GitHub API"
break
fi
# Check for authentication errors
local error_message
error_message=$(echo "$response" | jq -r '.message // empty' 2>/dev/null)
if [[ "$error_message" == "Bad credentials" ]]; then
log_error "GitHub token is invalid or expired"
log_error "Please check your GITHUB_TOKEN environment variable"
exit 1
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
# Validate repository name format (should be owner/repo)
if [[ "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then
all_repos+=("$repo")
else
log_warning "Skipping invalid repository name: $repo"
fi
done <<< "$repos"
((page++))
done
# Only print if there are repos to avoid unbound variable error
if [[ ${#all_repos[@]} -gt 0 ]]; then
printf '%s\n' "${all_repos[@]}"
fi
}
fetch_org_repos() {
local org="$1"
local page=1
local all_repos=()
# Don't log here to avoid capturing log messages as repo names
while true; do
local response
response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/orgs/$org/repos?per_page=100&page=$page&type=all")
# Check if response is valid JSON and contains repositories
if ! echo "$response" | jq empty 2>/dev/null; then
log_error "Invalid JSON response from GitHub API for organization: $org"
break
fi
# Check for authentication errors
local error_message
error_message=$(echo "$response" | jq -r '.message // empty' 2>/dev/null)
if [[ "$error_message" == "Bad credentials" ]]; then
log_error "GitHub token is invalid or expired"
log_error "Please check your GITHUB_TOKEN environment variable"
exit 1
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
# Validate repository name format (should be owner/repo)
if [[ "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then
all_repos+=("$repo")
else
log_warning "Skipping invalid repository name: $repo"
fi
done <<< "$repos"
((page++))
done
# Only print if there are repos to avoid unbound variable error
if [[ ${#all_repos[@]} -gt 0 ]]; then
printf '%s\n' "${all_repos[@]}"
fi
}
fetch_user_orgs() {
local user="$1"
local response
response=$(curl -s -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/users/$user/orgs")
# Check if response is valid JSON
if ! echo "$response" | jq empty 2>/dev/null; then
log_error "Invalid JSON response from GitHub API for user organizations"
log_error "Response: $response"
return 1
fi
# Check for authentication errors
local error_message
error_message=$(echo "$response" | jq -r '.message // empty' 2>/dev/null)
if [[ "$error_message" == "Bad credentials" ]]; then
log_error "GitHub token is invalid or expired"
log_error "Please check your GITHUB_TOKEN environment variable"
exit 1
fi
# Check if response is an array
local org_count
org_count=$(echo "$response" | jq 'length' 2>/dev/null)
if [[ "$org_count" == "0" ]] || [[ "$org_count" == "null" ]]; then
return 0
fi
echo "$response" | jq -r '.[].login // empty' 2>/dev/null
}
# Check if user is a collaborator on a repository
check_user_collaboration() {
local repo="$1"
local user="$2"
# Validate repository name format
if [[ ! "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then
log_warning "Invalid repository name format: $repo"
return 1
fi
local response
response=$(curl -s -w "%{http_code}" -H "Authorization: token $GITHUB_TOKEN" \
"https://api.github.com/repos/$repo/collaborators/$user")
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_warning "Access forbidden for $repo - may not have admin permissions"
return 1
;;
*)
log_warning "Failed to check collaboration status for $user on $repo (HTTP: $http_code)"
return 1
;;
esac
}
# Get repositories where user is actually a collaborator
get_user_collaborations() {
local repos=("$@")
local user="$1"
local collaborator_repos=()
log_header "Checking User Collaborations"
log_progress "Checking which repositories $user is actually a collaborator on..."
local total=${#repos[@]}
local checked=0
for repo in "${repos[@]}"; do
((checked++))
printf "\r${CYAN}Checking collaborations: [%3d%%] %d/%d repositories checked${NC}" \
$((checked * 100 / total)) "$checked" "$total"
if check_user_collaboration "$repo" "$user"; then
collaborator_repos+=("$repo")
log_info "$user is a collaborator on $repo"
fi
done
echo # New line after progress
return 0
}
# Repository matching function
match_repo_pattern() {
local repo_name="$1"
local pattern="$2"
# Convert shell wildcard pattern to regex
local regex_pattern
regex_pattern="${pattern//\*/.*}"
if [[ "$repo_name" =~ $regex_pattern ]]; then
return 0
else
return 1
fi
}
# User confirmation functions
get_confirmation() {
local repo="$1"
local current="$2"
local total="$3"
case "$CONFIRMATION_MODE" in
"yes_all")
return 0
;;
"no_all")
return 1
;;
"ask")
echo -e "\n${YELLOW}[$current/$total] Remove user '$USER_TO_REMOVE' from '$repo'?${NC}"
echo -e "${CYAN}Options: [y]es, [n]o, [a]ll yes, [q]uit, [s]kip all${NC}"
read -p "Choice: " -n 1 -r choice
echo
case "$choice" in
y|Y)
return 0
;;
n|N)
return 1
;;
a|A)
CONFIRMATION_MODE="yes_all"
log_info "Switched to 'yes to all' mode"
return 0
;;
s|S)
CONFIRMATION_MODE="no_all"
log_info "Switched to 'skip all' mode"
return 1
;;
q|Q)
log_warning "Operation cancelled by user"
exit 0
;;
*)
log_warning "Invalid choice, skipping..."
return 1
;;
esac
;;
esac
}
# User removal function
remove_user_from_repo() {
local repo="$1"
log_progress "Removing $USER_TO_REMOVE 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_TO_REMOVE")
local http_code="${response: -3}"
case "$http_code" in
204)
log_success "Successfully removed $USER_TO_REMOVE from $repo"
((SUCCESSFUL_REMOVALS++))
return 0
;;
404)
log_warning "$USER_TO_REMOVE is not a collaborator on $repo or repo not found"
((SKIPPED_REMOVALS++))
return 1
;;
*)
log_error "Failed to remove $USER_TO_REMOVE from $repo (HTTP: $http_code)"
((FAILED_REMOVALS++))
return 1
;;
esac
}
# Progress display
show_progress() {
local current="$1"
local total="$2"
local percentage=$((current * 100 / total))
printf "\r${CYAN}Progress: [%3d%%] %d/%d repositories processed${NC}" "$percentage" "$current" "$total"
}
# Summary report
show_summary() {
log_header "Operation Summary"
echo -e "${BOLD}Total repositories processed: $PROCESSED_REPOS${NC}"
echo -e "${GREEN}Successful removals: $SUCCESSFUL_REMOVALS${NC}"
echo -e "${RED}Failed removals: $FAILED_REMOVALS${NC}"
echo -e "${YELLOW}Skipped removals: $SKIPPED_REMOVALS${NC}"
echo -e "${CYAN}Success rate: $(( SUCCESSFUL_REMOVALS * 100 / (SUCCESSFUL_REMOVALS + FAILED_REMOVALS + SKIPPED_REMOVALS) ))%${NC}"
}
# Parse command line arguments
parse_arguments() {
while [[ $# -gt 0 ]]; do
case $1 in
-u|--user)
USER_TO_REMOVE="$2"
shift 2
;;
-r|--root)
ROOT_USER="$2"
shift 2
;;
-p|--pattern)
REPO_PATTERN="$2"
shift 2
;;
-t|--token)
GITHUB_TOKEN="$2"
shift 2
;;
--non-interactive)
NON_INTERACTIVE=true
CONFIRMATION_MODE="yes_all"
shift
;;
-h|--help)
echo "Usage: $0 [OPTIONS]"
echo "Options:"
echo " -u, --user USER Username to remove from repositories"
echo " -r, --root USER Root GitHub username/organization"
echo " -p, --pattern PATTERN Repository pattern (supports wildcards)"
echo " -t, --token TOKEN GitHub Personal Access Token"
echo " --non-interactive Run without prompts (auto-confirm all)"
echo " -h, --help Show this help message"
echo ""
echo "Example:"
echo " $0 -u i-ayushh18 -r saravanakumardb -p '*' -t \$GITHUB_TOKEN --non-interactive"
exit 0
;;
*)
log_error "Unknown option: $1"
exit 1
;;
esac
done
}
# Main execution function
main() {
log_header "GitHub User Removal Tool"
# Parse command line arguments
parse_arguments "$@"
# Check dependencies
check_dependencies
# Collect inputs (interactive if not provided via command line)
if [[ -z "$GITHUB_TOKEN" ]]; then
collect_github_token
else
log_success "Using GitHub token from command line"
fi
if [[ -z "$ROOT_USER" ]]; then
collect_root_user
else
log_success "Root user: $ROOT_USER"
fi
if [[ -z "$USER_TO_REMOVE" ]]; then
collect_user_to_remove
else
log_success "Target user: $USER_TO_REMOVE"
fi
if [[ -z "$REPO_PATTERN" ]]; then
collect_repo_pattern
else
log_success "Repository pattern: $REPO_PATTERN"
fi
# Collect all repositories
log_header "Repository Discovery"
local all_repos=()
# Fetch root user repositories
log_info "Fetching repositories for user: $ROOT_USER"
local user_repos_output
user_repos_output=$(fetch_user_repos "$ROOT_USER")
local user_repos_exit_code=$?
log_info "User repos fetch exit code: $user_repos_exit_code"
log_info "User repos output: '$user_repos_output'"
if [[ -n "$user_repos_output" ]]; then
while IFS= read -r repo; do
if [[ -n "$repo" ]] && [[ "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then
all_repos+=("$repo")
log_info "Found user repo: $repo"
elif [[ -n "$repo" ]]; then
log_warning "Skipping invalid repository name: $repo"
fi
done <<< "$user_repos_output"
else
log_info "No repositories found for user: $ROOT_USER"
fi
# Fetch organization repositories
log_info "Fetching organizations for user: $ROOT_USER"
local orgs=()
local org_output
org_output=$(fetch_user_orgs "$ROOT_USER")
local org_exit_code=$?
log_info "Organization fetch exit code: $org_exit_code"
log_info "Organization output: '$org_output'"
if [[ $org_exit_code -eq 0 ]] && [[ -n "$org_output" ]]; then
while IFS= read -r org; do
if [[ -n "$org" ]] && [[ "$org" =~ ^[a-zA-Z0-9._-]+$ ]]; then
orgs+=("$org")
log_info "Found organization: $org"
elif [[ -n "$org" ]]; then
log_warning "Skipping invalid organization name: $org"
fi
done <<< "$org_output"
else
log_info "No organizations found for user: $ROOT_USER"
fi
# Fetch repositories for each organization
if [[ ${#orgs[@]} -gt 0 ]]; then
for org in "${orgs[@]}"; do
log_info "Fetching repositories for organization: $org"
while IFS= read -r repo; do
if [[ -n "$repo" ]] && [[ "$repo" =~ ^[a-zA-Z0-9._-]+/[a-zA-Z0-9._-]+$ ]]; then
all_repos+=("$repo")
log_info "Found org repo: $repo"
elif [[ -n "$repo" ]]; then
log_warning "Skipping invalid repository name: $repo"
fi
done < <(fetch_org_repos "$org")
done
fi
# Filter repositories by pattern
log_header "Repository Filtering"
local matching_repos=()
log_info "Total repositories found: ${#all_repos[@]}"
for repo in "${all_repos[@]}"; do
local repo_name
repo_name=$(basename "$repo")
if match_repo_pattern "$repo_name" "$REPO_PATTERN"; then
matching_repos+=("$repo")
log_info "Matched: $repo"
fi
done
TOTAL_REPOS=${#matching_repos[@]}
if [[ $TOTAL_REPOS -eq 0 ]]; then
log_warning "No repositories match the pattern '$REPO_PATTERN'"
log_info "Available repositories:"
for repo in "${all_repos[@]}"; do
log_info "$repo"
done
exit 0
fi
log_success "Found $TOTAL_REPOS matching repositories"
# Check which repositories the user is actually a collaborator on
log_header "Checking User Collaborations"
log_progress "Checking which repositories '$USER_TO_REMOVE' is actually a collaborator on..."
local collaborator_repos=()
local checked=0
for repo in "${matching_repos[@]}"; do
((checked++))
printf "\r${CYAN}Checking collaborations: [%3d%%] %d/%d repositories checked${NC}" \
$((checked * 100 / TOTAL_REPOS)) "$checked" "$TOTAL_REPOS"
if check_user_collaboration "$repo" "$USER_TO_REMOVE"; then
collaborator_repos+=("$repo")
fi
done
echo # New line after progress
local collaborator_count=${#collaborator_repos[@]}
if [[ $collaborator_count -eq 0 ]]; then
log_warning "User '$USER_TO_REMOVE' is not a collaborator on any of the $TOTAL_REPOS matching repositories"
log_info "No action needed - user is already not a collaborator on any matching repositories"
exit 0
fi
log_success "User '$USER_TO_REMOVE' is a collaborator on $collaborator_count out of $TOTAL_REPOS matching repositories"
# Show preview of repositories where user will be removed
log_header "Preview: Repositories where '$USER_TO_REMOVE' will be removed"
for repo in "${collaborator_repos[@]}"; do
log_info "$repo"
done
# Confirmation before proceeding
if [[ "$NON_INTERACTIVE" == "false" ]]; then
echo -e "\n${YELLOW}About to remove user '$USER_TO_REMOVE' from $collaborator_count repositories${NC}"
read -p "Continue? [y/N]: " -n 1 -r
echo
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
log_warning "Operation cancelled"
exit 0
fi
else
log_info "Non-interactive mode: proceeding with removal from $collaborator_count repositories"
fi
# Process repositories
log_header "User Removal Process"
for repo in "${collaborator_repos[@]}"; do
((PROCESSED_REPOS++))
show_progress "$PROCESSED_REPOS" "$collaborator_count"
if get_confirmation "$repo" "$PROCESSED_REPOS" "$collaborator_count"; then
remove_user_from_repo "$repo"
else
log_info "Skipped: $repo"
((SKIPPED_REMOVALS++))
fi
done
echo # New line after progress
# Show final summary
show_summary
log_success "Operation completed successfully!"
}
# Script execution
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi