#!/usr/bin/env bash
#
# join-ad.sh
#
# Join Ubuntu or RHEL-family Linux systems to Microsoft Active Directory.
#
# Features:
#   - Ubuntu/Debian and RHEL/Rocky/Alma support
#   - Installs required packages
#   - Joins AD using realmd/adcli/SSSD
#   - Short AD usernames
#   - Comma-separated list of permitted AD groups
#   - SSSD controls AD login authorization
#   - Each permitted AD group receives its own sudoers.d file
#   - Automatic home-directory creation
#   - Re-runnable/idempotent where practical
#

set -Eeuo pipefail

###############################################################################
# Helpers
###############################################################################

info() {
    printf '\n\033[1;34m==> %s\033[0m\n' "$*"
}

success() {
    printf '\033[1;32m    %s\033[0m\n' "$*"
}

warn() {
    printf '\033[1;33mWARNING: %s\033[0m\n' "$*" >&2
}

die() {
    printf '\033[1;31mERROR: %s\033[0m\n' "$*" >&2
    exit 1
}

backup_file() {
    local file="$1"

    if [[ -f "$file" ]]; then
        local backup="${file}.pre-ad-$(date +%Y%m%d-%H%M%S)"
        cp -a "$file" "$backup"
        success "Backup: $backup"
    fi
}

###############################################################################
# Root check
###############################################################################

if [[ $EUID -ne 0 ]]; then
    die "This script must be run as root."
fi

###############################################################################
# Detect operating system
###############################################################################

[[ -r /etc/os-release ]] ||
    die "Unable to identify operating system."

# shellcheck disable=SC1091
source /etc/os-release

DISTRO_ID="${ID,,}"
DISTRO_LIKE="${ID_LIKE:-}"
DISTRO_LIKE="${DISTRO_LIKE,,}"

case "$DISTRO_ID" in

    ubuntu|debian)
        OS_FAMILY="debian"
        ;;

    rhel|rocky|almalinux|centos|ol|fedora)
        OS_FAMILY="rhel"
        ;;

    *)
        if [[ "$DISTRO_LIKE" == *debian* ]]; then
            OS_FAMILY="debian"
        elif [[ "$DISTRO_LIKE" == *rhel* ||
                "$DISTRO_LIKE" == *fedora* ]]; then
            OS_FAMILY="rhel"
        else
            die "Unsupported Linux distribution: ${PRETTY_NAME:-$DISTRO_ID}"
        fi
        ;;
esac

info "Operating system"
echo "    ${PRETTY_NAME:-$DISTRO_ID}"
echo "    Family: $OS_FAMILY"

###############################################################################
# Gather configuration
###############################################################################

echo
read -rp "Active Directory domain (example: example.com): " AD_DOMAIN

[[ -n "$AD_DOMAIN" ]] ||
    die "A domain is required."

AD_DOMAIN="${AD_DOMAIN,,}"
AD_REALM="${AD_DOMAIN^^}"

echo
read -rp "AD account permitted to join computers to the domain: " JOIN_USER

[[ -n "$JOIN_USER" ]] ||
    die "A domain join account is required."

echo
echo "Enter one or more AD groups."
echo
echo "Separate multiple groups with commas."
echo
echo "Example:"
echo
echo "    Domain Admins, Linux Admins, A2K Support"
echo

read -rp "AD groups permitted to log in and use sudo: " AD_GROUP_INPUT

[[ -n "$AD_GROUP_INPUT" ]] ||
    die "At least one AD group is required."

###############################################################################
# Parse group list
###############################################################################

IFS=',' read -ra RAW_GROUPS <<< "$AD_GROUP_INPUT"

AD_GROUPS=()

for group in "${RAW_GROUPS[@]}"; do

    # Trim leading whitespace.
    group="${group#"${group%%[![:space:]]*}"}"

    # Trim trailing whitespace.
    group="${group%"${group##*[![:space:]]}"}"

    [[ -n "$group" ]] || continue

    AD_GROUPS+=("$group")

done

[[ ${#AD_GROUPS[@]} -gt 0 ]] ||
    die "No valid AD groups were supplied."

###############################################################################
# Display configuration
###############################################################################

echo
echo "------------------------------------------------------------"
echo "Configuration"
echo "------------------------------------------------------------"
echo "Domain          : $AD_DOMAIN"
echo "Kerberos realm  : $AD_REALM"
echo "Join account    : $JOIN_USER"
echo "Short usernames : enabled"
echo "Home directories: /home/<username>"
echo
echo "Login + sudo groups:"

for group in "${AD_GROUPS[@]}"; do
    echo "    - $group"
done

echo "------------------------------------------------------------"
echo

read -rp "Continue? [y/N]: " CONFIRM

case "$CONFIRM" in
    y|Y|yes|YES)
        ;;
    *)
        echo "Cancelled."
        exit 0
        ;;
esac

###############################################################################
# Install packages
###############################################################################

info "Installing Active Directory integration packages"

if [[ "$OS_FAMILY" == "debian" ]]; then

    export DEBIAN_FRONTEND=noninteractive

    apt-get update

    apt-get install -y \
        realmd \
        sssd \
        sssd-ad \
        sssd-tools \
        libnss-sss \
        libpam-sss \
        adcli \
        krb5-user \
        samba-common-bin \
        packagekit \
        libpam-mkhomedir \
        dnsutils \
        python3

else

    dnf install -y \
        realmd \
        sssd \
        sssd-ad \
        sssd-tools \
        oddjob \
        oddjob-mkhomedir \
        adcli \
        samba-common-tools \
        krb5-workstation \
        bind-utils \
        python3

    systemctl enable --now oddjobd

fi

success "Required packages installed."

###############################################################################
# Verify DNS
###############################################################################

info "Checking DNS for Active Directory"

if command -v dig >/dev/null 2>&1; then

    if dig +short SRV "_ldap._tcp.dc._msdcs.${AD_DOMAIN}" |
        grep -q .; then

        success "Active Directory LDAP SRV records found."

    else

        warn "Could not find:"
        warn "    _ldap._tcp.dc._msdcs.${AD_DOMAIN}"
        warn "The system may not be using Active Directory DNS."

    fi

    if dig +short SRV "_kerberos._tcp.${AD_DOMAIN}" |
        grep -q .; then

        success "Kerberos SRV records found."

    else

        warn "Could not find Kerberos SRV records."

    fi

fi

###############################################################################
# Discover domain
###############################################################################

info "Discovering Active Directory domain"

if ! realm discover "$AD_DOMAIN"; then
    die "realmd could not discover $AD_DOMAIN. Check DNS and connectivity."
fi

###############################################################################
# Determine whether we're already joined
###############################################################################

ALREADY_JOINED=false

if realm list --name-only 2>/dev/null |
    tr '[:upper:]' '[:lower:]' |
    grep -Fxq "$AD_DOMAIN"; then

    ALREADY_JOINED=true
    success "System is already joined to $AD_DOMAIN."

fi

###############################################################################
# Join domain
###############################################################################

if [[ "$ALREADY_JOINED" == false ]]; then

    info "Joining $AD_DOMAIN"

    echo
    echo "You will now be prompted for the AD password for:"
    echo
    echo "    $JOIN_USER"
    echo

    if ! realm join \
        --verbose \
        --client-software=sssd \
        --membership-software=adcli \
        --user="$JOIN_USER" \
        "$AD_DOMAIN"; then

        die "Domain join failed."

    fi

    success "Domain join completed."

fi

###############################################################################
# SSSD configuration
###############################################################################

info "Configuring SSSD"

SSSD_CONF="/etc/sssd/sssd.conf"

[[ -f "$SSSD_CONF" ]] ||
    die "Expected SSSD configuration does not exist: $SSSD_CONF"

backup_file "$SSSD_CONF"

chmod 600 "$SSSD_CONF"
chown root:root "$SSSD_CONF"

###############################################################################
# Construct SSSD simple_allow_groups
###############################################################################

#
# SSSD's simple access provider provides exactly the behavior wanted here:
#
#   access_provider = simple
#   simple_allow_groups = Group One, Group Two, Group Three
#
# A user belonging to ANY listed group is allowed access.
#

SSSD_ALLOW_GROUPS=""

for group in "${AD_GROUPS[@]}"; do

    if [[ -n "$SSSD_ALLOW_GROUPS" ]]; then
        SSSD_ALLOW_GROUPS+=", "
    fi

    SSSD_ALLOW_GROUPS+="$group"

done

###############################################################################
# Modify SSSD domain section
###############################################################################

python3 - "$SSSD_CONF" "$SSSD_ALLOW_GROUPS" <<'PY'
import sys
import re

path = sys.argv[1]
groups = sys.argv[2]

with open(path, "r") as f:
    data = f.read()

domain_pattern = re.compile(
    r'(\[domain/[^\]]+\].*?)(?=\n\[|\Z)',
    re.S
)

matches = list(domain_pattern.finditer(data))

if not matches:
    print("ERROR: No [domain/...] section found in sssd.conf")
    sys.exit(1)


def set_option(section, key, value):

    option = re.compile(
        rf'(?m)^\s*{re.escape(key)}\s*=.*$'
    )

    if option.search(section):
        return option.sub(
            f'{key} = {value}',
            section
        )

    return section.rstrip() + f'\n{key} = {value}\n'


def modify_domain(match):

    section = match.group(1)

    settings = {
        "use_fully_qualified_names": "False",
        "fallback_homedir": "/home/%u",
        "default_shell": "/bin/bash",
        "access_provider": "simple",
        "simple_allow_groups": groups,
    }

    for key, value in settings.items():
        section = set_option(
            section,
            key,
            value
        )

    return section


data = domain_pattern.sub(
    modify_domain,
    data
)

with open(path, "w") as f:
    f.write(data)
PY

chmod 600 "$SSSD_CONF"
chown root:root "$SSSD_CONF"

success "SSSD configured."
success "Short AD usernames enabled."
success "SSSD group-based login restrictions enabled."

###############################################################################
# Display access configuration
###############################################################################

echo
echo "SSSD access policy:"
echo
echo "    access_provider = simple"
echo "    simple_allow_groups = $SSSD_ALLOW_GROUPS"

###############################################################################
# Configure automatic home-directory creation
###############################################################################

info "Configuring automatic home-directory creation"

if [[ "$OS_FAMILY" == "debian" ]]; then

    pam-auth-update --enable mkhomedir

else

    authselect current >/dev/null 2>&1 ||
        die "Unable to determine current authselect configuration."

    authselect enable-feature with-mkhomedir

    systemctl enable --now oddjobd

fi

success "Automatic home-directory creation enabled."

###############################################################################
# Restart SSSD
###############################################################################

info "Restarting SSSD"

systemctl enable sssd

sss_cache -E 2>/dev/null || true

systemctl restart sssd

###############################################################################
# Resolve AD groups
###############################################################################

info "Resolving AD groups"

sleep 2

GROUP_FAILURE=false

for group in "${AD_GROUPS[@]}"; do

    echo
    echo "Checking:"
    echo "    $group"

    GROUP_ENTRY=""

    for attempt in {1..5}; do

        GROUP_ENTRY="$(getent group "$group" || true)"

        [[ -n "$GROUP_ENTRY" ]] && break

        sleep 2

    done

    if [[ -n "$GROUP_ENTRY" ]]; then

        success "Resolved: $group"
        echo "    $GROUP_ENTRY"

    else

        warn "Unable to resolve AD group: $group"
        GROUP_FAILURE=true

    fi

done

###############################################################################
# Configure sudo
###############################################################################

info "Creating sudo policies"

mkdir -p /etc/sudoers.d

for group in "${AD_GROUPS[@]}"; do

    ###########################################################################
    # Generate safe filename
    ###########################################################################

    #
    # Example:
    #
    #   Domain Admins
    #
    # becomes:
    #
    #   /etc/sudoers.d/ad-domain-admins
    #

    SAFE_NAME="$(
        printf '%s' "$group" |
        tr '[:upper:]' '[:lower:]' |
        sed \
            -e 's/[^a-z0-9._-]/-/g' \
            -e 's/--*/-/g' \
            -e 's/^-//' \
            -e 's/-$//'
    )"

    [[ -n "$SAFE_NAME" ]] ||
        die "Unable to construct sudoers filename for group: $group"

    SUDOERS_FILE="/etc/sudoers.d/ad-${SAFE_NAME}"

    ###########################################################################
    # Escape group for sudo syntax
    ###########################################################################

    #
    # Sudo requires spaces in group names to be escaped.
    #
    # Domain Admins
    #
    # becomes:
    #
    # %Domain\ Admins ALL=(ALL:ALL) ALL
    #

    SUDO_GROUP="${group// /\\ }"

    cat > "$SUDOERS_FILE" <<EOF
# Managed by join-ad.sh
#
# Active Directory group:
# $group
#
%${SUDO_GROUP} ALL=(ALL:ALL) ALL
EOF

    chmod 440 "$SUDOERS_FILE"
    chown root:root "$SUDOERS_FILE"

    if visudo -cf "$SUDOERS_FILE" >/dev/null; then

        success "$group"
        echo "        $SUDOERS_FILE"

    else

        rm -f "$SUDOERS_FILE"

        die "Generated sudoers configuration failed validation for: $group"

    fi

done

###############################################################################
# Clear SSSD cache
###############################################################################

info "Refreshing SSSD"

sss_cache -E 2>/dev/null || true

systemctl restart sssd

###############################################################################
# Validate SSSD configuration
###############################################################################

info "Validating SSSD"

if command -v sssctl >/dev/null 2>&1; then

    if sssctl config-check; then
        success "SSSD configuration passed validation."
    else
        die "SSSD configuration validation failed."
    fi

fi

if systemctl is-active --quiet sssd; then
    success "SSSD is running."
else
    die "SSSD is not running."
fi

###############################################################################
# Display realm
###############################################################################

info "Active Directory realm"

realm list

###############################################################################
# Final group validation
###############################################################################

info "Final AD group validation"

for group in "${AD_GROUPS[@]}"; do

    printf "    %-35s " "$group"

    if getent group "$group" >/dev/null 2>&1; then
        printf '\033[1;32mOK\033[0m\n'
    else
        printf '\033[1;31mNOT FOUND\033[0m\n'
    fi

done

###############################################################################
# Final result
###############################################################################

echo
echo "============================================================"
echo " Active Directory configuration complete"
echo "============================================================"
echo
echo "Domain:"
echo "    $AD_DOMAIN"
echo
echo "Permitted AD groups:"

for group in "${AD_GROUPS[@]}"; do
    echo "    - $group"
done

echo
echo "Username format:"
echo "    username"
echo
echo "Home directories:"
echo "    /home/username"
echo
echo "SSSD access policy:"
echo "    access_provider = simple"
echo
echo "Sudo policies:"

for group in "${AD_GROUPS[@]}"; do

    SAFE_NAME="$(
        printf '%s' "$group" |
        tr '[:upper:]' '[:lower:]' |
        sed \
            -e 's/[^a-z0-9._-]/-/g' \
            -e 's/--*/-/g' \
            -e 's/^-//' \
            -e 's/-$//'
    )"

    echo "    /etc/sudoers.d/ad-${SAFE_NAME}"

done

echo
echo "Useful tests:"
echo
echo "    id username"
echo "    getent passwd username"

for group in "${AD_GROUPS[@]}"; do
    printf '    getent group "%s"\n' "$group"
done

echo
echo "Test authorization:"
echo
echo "    sssctl user-checks username"
echo
echo "Test sudo after logging in:"
echo
echo "    sudo -l"
echo
echo "============================================================"

if [[ "$GROUP_FAILURE" == true ]]; then

    echo
    warn "One or more configured AD groups could not be resolved."
    warn "Review the group validation results above."

fi