#!/usr/bin/env bash
#
# join-ad.sh
#
# Join Ubuntu or RHEL-family Linux systems to Microsoft Active Directory
# using realmd + SSSD.
#
# Features:
#   - Ubuntu/Debian and RHEL/Rocky/Alma support
#   - Installs required packages
#   - Joins Active Directory
#   - Allows short usernames (username instead of username@domain)
#   - Restricts login to a specified AD group
#   - Grants that AD group sudo access
#   - Automatically creates home directories
#   - Performs post-join validation
#
# Run as root:
#   sudo ./join-ad.sh
#

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
read -rp "AD group permitted to log in and use sudo: " AD_GROUP

[[ -n "$AD_GROUP" ]] || die "An AD group is required."

echo
echo "------------------------------------------------------------"
echo "Configuration"
echo "------------------------------------------------------------"
echo "Domain          : $AD_DOMAIN"
echo "Kerberos realm  : $AD_REALM"
echo "Join account    : $JOIN_USER"
echo "Access group    : $AD_GROUP"
echo "Short usernames : enabled"
echo "Home directories: /home/<username>"
echo "Sudo            : $AD_GROUP"
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-tools \
        libnss-sss \
        libpam-sss \
        adcli \
        krb5-user \
        samba-common-bin \
        packagekit \
        libpam-mkhomedir \
        dnsutils

else

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

    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 _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

###############################################################################
# Locate SSSD domain 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"

###############################################################################
# Configure short usernames
###############################################################################

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

path = sys.argv[1]

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

def set_domain_option(data, key, value):

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

    def modify(match):
        section = match.group(1)

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

        if option.search(section):
            section = option.sub(f'{key} = {value}', section)
        else:
            section = section.rstrip() + f'\n{key} = {value}\n'

        return section

    return pattern.sub(modify, data)

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

for key, value in settings.items():
    data = set_domain_option(data, key, value)

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

chmod 600 "$SSSD_CONF"

success "Short AD usernames enabled."
success "AD home directories will use /home/<username>."

###############################################################################
# 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
systemctl restart sssd

###############################################################################
# Resolve AD group
###############################################################################

info "Resolving AD group"

# Give SSSD a moment after configuration changes.
sleep 2

sss_cache -E || true

GROUP_ENTRY=""

for attempt in {1..5}; do

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

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

    sleep 2
done

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

    echo
    warn "Unable to resolve AD group using the short name:"
    warn "    $AD_GROUP"
    echo
    echo "Trying fully-qualified group name..."

    GROUP_ENTRY="$(getent group "${AD_GROUP}@${AD_DOMAIN}" || true)"

fi

if [[ -z "$GROUP_ENTRY" ]]; then
    die "Unable to resolve AD group '$AD_GROUP' through SSSD."
fi

success "AD group resolved:"
echo "    $GROUP_ENTRY"

###############################################################################
# Configure realm access
###############################################################################

info "Restricting Linux access to AD group"

# Start from a clean permit policy.
realm deny --all

if realm permit -g "$AD_GROUP"; then

    success "Login permitted for AD group: $AD_GROUP"

elif realm permit -g "${AD_GROUP}@${AD_DOMAIN}"; then

    success "Login permitted for AD group: ${AD_GROUP}@${AD_DOMAIN}"

else

    die "Unable to configure realm access for AD group."
fi

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

info "Granting sudo access to AD group"

SUDOERS_FILE="/etc/sudoers.d/ad-domain-admins"

#
# Sudo group syntax:
#
#     %group ALL=(ALL) ALL
#
# Spaces in group names must be escaped.
#

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

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

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

if ! visudo -cf "$SUDOERS_FILE"; then

    rm -f "$SUDOERS_FILE"
    die "Generated sudoers configuration failed validation."

fi

success "Sudo access granted to AD group: $AD_GROUP"

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

info "Refreshing SSSD"

sss_cache -E || true

systemctl restart sssd

###############################################################################
# Validate realm
###############################################################################

info "Validating Active Directory configuration"

echo
realm list

###############################################################################
# Validate group
###############################################################################

echo
info "Validating AD group"

if getent group "$AD_GROUP"; then
    success "AD group lookup succeeded."
else
    warn "Short-name group lookup did not return a result."
fi

###############################################################################
# Validate SSSD
###############################################################################

echo
info "Validating SSSD"

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

###############################################################################
# Validate configuration
###############################################################################

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

    echo
    info "Checking SSSD configuration"

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

fi

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

echo
echo "============================================================"
echo " Active Directory configuration complete"
echo "============================================================"
echo
echo "Domain:"
echo "    $AD_DOMAIN"
echo
echo "Login group:"
echo "    $AD_GROUP"
echo
echo "Sudo group:"
echo "    $AD_GROUP"
echo
echo "Username format:"
echo "    username"
echo
echo "Home directory:"
echo "    /home/username"
echo
echo "Examples:"
echo
echo "    id username"
echo "    getent passwd username"
echo "    getent group \"$AD_GROUP\""
echo
echo "SSH:"
echo
echo "    ssh username@$(hostname -f 2>/dev/null || hostname)"
echo
echo "After logging in:"
echo
echo "    sudo -i"
echo
echo "============================================================"