#!/usr/bin/env bash
# ansipa-enforce-policies.sh — enforce FreeIPA host-group-driven policies on this client.
#
# Policies are idempotent and reversible: joining a group applies the policy;
# leaving the group removes it on the next run (every 30 min via systemd timer).
#
# Host-group naming conventions (device policies — applied to the whole machine):
#   policy-daemon-enable-<unit>   Ensure <unit> is enabled and running (systemctl enable --now).
#                                 Leaving the group reverts: service is disabled and stopped.
#   policy-daemon-disable-<unit>  Ensure <unit> is disabled and stopped (systemctl disable --now).
#                                 Leaving the group reverts: service is re-enabled and started.
#                                 <unit> may omit the .service suffix; all systemd unit types work.
#                                 If a unit appears in both enable and disable groups it is skipped.
#   policy-timeshift-backup       Enforce a daily Timeshift snapshot (requires timeshift installed)
#   policy-security-scan          Enforce daily ClamAV + rkhunter + chkrootkit scans
#   policy-scan-notify            (see User-group section below — treated as a user group)
#   no_local_users                Lock passwords for root and all local users (UID >= 1000) so
#                                 that only FreeIPA domain accounts with centrally-managed sudo
#                                 rules can authenticate and gain elevated privileges.
#                                 Leaving the group reverts: every account locked by this policy
#                                 is unlocked on the next run.
#   local_sudo_<username>         Grant <username> full sudo on this specific device by adding
#                                 them to the local sudoers drop-in.  Leaving the group removes
#                                 the drop-in on the next run.
#
# User-group naming conventions (per-user policies — follow the user across devices):
#   policy-block-binary-<name>    Prevent members of this FreeIPA user group from running <name>
#                                 on any enrolled host.  Use __ in place of . to support Flatpak
#                                 app IDs (e.g. policy-block-binary-org__gimp__Gimp blocks the
#                                 Flatpak org.gimp.Gimp).  Enforced via a PATH-priority wrapper
#                                 that checks group membership at runtime via SSSD/id(1).
#                                 Removing the user group from FreeIPA reverts the wrapper.
#   policy-scan-notify            Members receive scan alert notifications on every enrolled device
#                                 they log into.  The fetch-alerts timer is installed fleet-wide
#                                 when the group exists; the notification daemon starts on login
#                                 only for group members (checked via id(1) / SSSD).
#                                 Deleting the IPA user group removes the timer and profile.d
#                                 snippet on the next enforcer run.
#
# Notes:
#   - Install scan tools first: add the host to ansipa-module-anti-malware.
#   - Configure Timeshift (type + target device) before enabling policy-timeshift-backup.

set -euo pipefail

LOG_TAG="ansipa-policies"
STATE_DIR="/var/lib/ansipa-policies"
BLOCK_DIR="/usr/local/bin"
CRON_DIR="/etc/cron.d"

log()  { echo "[$LOG_TAG] $*"; logger -t "$LOG_TAG" "$*" 2>/dev/null || true; }
warn() { echo "[$LOG_TAG][WARN] $*" >&2; logger -t "$LOG_TAG" "WARN: $*" 2>/dev/null || true; }

HOST_FQDN=$(hostname -f 2>/dev/null || hostname)

if ! command -v ipa &>/dev/null; then
    warn "ipa command not found — host not enrolled in FreeIPA. Exiting."
    exit 0
fi

kinit -k "host/$HOST_FQDN" &>/dev/null || true
mkdir -p "$STATE_DIR"

# ── Fetch host group membership ───────────────────────────────────────────────
RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \
    | grep -i "Member of host-groups:" | sed 's/.*: //' || true)

# ── Parse active host-group (device) policies ─────────────────────────────────
ACTIVE_DAEMON_ENABLE=()
ACTIVE_DAEMON_DISABLE=()
ACTIVE_LOCAL_SUDO_USERS=()
WANT_TIMESHIFT_BACKUP=false
WANT_SECURITY_SCAN=false
WANT_SCAN_NOTIFY=false
WANT_NO_LOCAL_USERS=false

if [[ -n "$RAW_GROUPS" ]]; then
    while IFS=',' read -ra GRP_ARRAY; do
        for g in "${GRP_ARRAY[@]}"; do
            g="${g// /}"
            case "$g" in
                policy-daemon-enable-*)  ACTIVE_DAEMON_ENABLE+=("${g#policy-daemon-enable-}") ;;
                policy-daemon-disable-*) ACTIVE_DAEMON_DISABLE+=("${g#policy-daemon-disable-}") ;;
                policy-timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;;
                policy-security-scan)    WANT_SECURITY_SCAN=true ;;
                no_local_users)          WANT_NO_LOCAL_USERS=true ;;
                local_sudo_*)            ACTIVE_LOCAL_SUDO_USERS+=("${g#local_sudo_}") ;;
            esac
        done
    done <<< "$RAW_GROUPS"
fi

# ── Fetch user-group-based binary block policies from FreeIPA ─────────────────
# policy-block-binary-<name> groups are FreeIPA *user* groups — membership follows
# the user to every enrolled host rather than being tied to a device.
ACTIVE_BLOCK_BINARIES=()
ACTIVE_BLOCK_IPA_GROUPS=()

_BLOCK_LIST=$(ipa group-find --pkey-only 2>/dev/null \
    | awk '/Group name:/ {print $NF}' \
    | grep "^policy-block-binary-" | sort -u || true)

while IFS= read -r _grp; do
    [[ -z "$_grp" ]] && continue
    _raw="${_grp#policy-block-binary-}"
    ACTIVE_BLOCK_BINARIES+=("${_raw//__/.}")
    ACTIVE_BLOCK_IPA_GROUPS+=("$_grp")
done <<< "$_BLOCK_LIST"
unset _BLOCK_LIST _grp _raw

# ── Fetch user-group-based scan-notify policy from FreeIPA ────────────────────
# policy-scan-notify is a FreeIPA *user* group — membership follows the user to
# every enrolled host.  Install the fetch-alerts timer on any device where the
# group exists; the profile.d snippet gates daemon start on runtime membership.
if ipa group-show policy-scan-notify >/dev/null 2>&1; then
    WANT_SCAN_NOTIFY=true
fi

log "Device policies — daemon-enable: ${ACTIVE_DAEMON_ENABLE[*]:-none}" \
    "| daemon-disable: ${ACTIVE_DAEMON_DISABLE[*]:-none}" \
    "| timeshift-backup: $WANT_TIMESHIFT_BACKUP | security-scan: $WANT_SECURITY_SCAN" \
    "| no-local-users: $WANT_NO_LOCAL_USERS | local-sudo: ${ACTIVE_LOCAL_SUDO_USERS[*]:-none}"
log "User policies  — block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \
    "| scan-notify: $WANT_SCAN_NOTIFY"

# ── Helpers ───────────────────────────────────────────────────────────────────
_in_block_list() {
    local needle="$1"
    for b in "${ACTIVE_BLOCK_BINARIES[@]}"; do
        [[ "$b" == "$needle" ]] && return 0
    done
    return 1
}

# ── Binary blocking (user-based) ──────────────────────────────────────────────
# A PATH-priority wrapper is installed in /usr/local/bin/ for every binary named
# by a policy-block-binary-* FreeIPA *user* group.  The wrapper checks the
# caller's group membership at runtime (via id + SSSD) and only blocks members;
# non-members are transparently passed through to the real binary.
# __ in the group suffix decodes to . so Flatpak app IDs are fully supported.
# Removing the IPA user group causes the wrapper to be cleaned up on the next run.

BLOCK_STATE="$STATE_DIR/blocked-binaries"
[[ -f "$BLOCK_STATE" ]] || touch "$BLOCK_STATE"

for _idx in "${!ACTIVE_BLOCK_BINARIES[@]}"; do
    BIN="${ACTIVE_BLOCK_BINARIES[$_idx]}"
    IPA_GRP="${ACTIVE_BLOCK_IPA_GROUPS[$_idx]}"
    WRAPPER="$BLOCK_DIR/$BIN"
    # Write (or refresh) the wrapper when it is absent, not ours, or the group name changed.
    if [[ ! -f "$WRAPPER" ]] \
        || ! grep -q "blocked by ansipa policy" "$WRAPPER" 2>/dev/null \
        || ! grep -qF "$IPA_GRP" "$WRAPPER" 2>/dev/null; then
        log "Installing user-aware block wrapper: $BIN (group: $IPA_GRP)"
        cat > "$WRAPPER" <<WRAPPER
#!/bin/bash
# blocked by ansipa policy (user-based)
if id -Gn 2>/dev/null | tr ' ' '\n' | grep -qxF "${IPA_GRP}"; then
    echo "[ansipa-policies] '${BIN}' is blocked by system policy for your account." >&2
    exit 1
fi
_real=\$(PATH="/usr/bin:/usr/sbin:/bin:/sbin:/usr/local/sbin:/opt/bin:/var/lib/flatpak/exports/bin:/usr/share/flatpak/exports/bin" command -v "${BIN}" 2>/dev/null)
[[ -n "\$_real" ]] && exec "\$_real" "\$@"
command -v flatpak &>/dev/null && exec flatpak run "${BIN}" "\$@" 2>/dev/null
echo "${BIN}: command not found" >&2
exit 127
WRAPPER
        chmod 755 "$WRAPPER"
    fi
done
unset _idx

# Remove wrappers whose IPA user group no longer exists.
while IFS= read -r OLD_BIN; do
    [[ -z "$OLD_BIN" ]] && continue
    if ! _in_block_list "$OLD_BIN"; then
        WRAPPER="$BLOCK_DIR/$OLD_BIN"
        if [[ -f "$WRAPPER" ]] && grep -q "blocked by ansipa policy" "$WRAPPER" 2>/dev/null; then
            rm -f "$WRAPPER"
            log "Removed binary block wrapper: $OLD_BIN"
        fi
    fi
done < "$BLOCK_STATE"

# Persist current blocked list.
if [[ ${#ACTIVE_BLOCK_BINARIES[@]} -gt 0 ]]; then
    printf '%s\n' "${ACTIVE_BLOCK_BINARIES[@]}" | sort -u > "$BLOCK_STATE"
else
    > "$BLOCK_STATE"
fi

# ── Timeshift daily backup ─────────────────────────────────────────────────────
TIMESHIFT_CRON="$CRON_DIR/ansipa-timeshift-backup"

if [[ "$WANT_TIMESHIFT_BACKUP" == true ]]; then
    if [[ ! -f "$TIMESHIFT_CRON" ]]; then
        if ! command -v timeshift &>/dev/null; then
            warn "timeshift not found — add host to ansipa-module-timeshift first. Cron will be installed anyway."
        fi
        log "Enabling daily Timeshift backups"
        cat > "$TIMESHIFT_CRON" <<'CRON'
# ansipa-policy-timeshift-backup: managed by ansipa-enforce-policies — do not edit manually.
# Timeshift must be configured on this host (type + target device) before snapshots work.
0 3 * * * root /usr/bin/timeshift --create --comments "ansipa-daily" --tags D 2>&1 | logger -t timeshift-backup
CRON
        chmod 644 "$TIMESHIFT_CRON"
    fi
else
    if [[ -f "$TIMESHIFT_CRON" ]]; then
        rm -f "$TIMESHIFT_CRON"
        log "Removed Timeshift backup cron (host left policy-timeshift-backup group)"
    fi
fi

# ── Security scan ─────────────────────────────────────────────────────────────
SCAN_CRON="$CRON_DIR/ansipa-security-scan"
SCAN_SCRIPT="/usr/local/bin/ansipa-security-scan.sh"

if [[ "$WANT_SECURITY_SCAN" == true ]]; then
    # (Re-)write the scan script so it stays current with this version of the enforcer.
    cat > "$SCAN_SCRIPT" <<'SCAN'
#!/bin/bash
# ansipa-security-scan — daily ClamAV / rkhunter / chkrootkit run + SMB upload.
# Managed by ansipa-enforce-policies — do not edit manually.
LOG=/var/log/ansipa-security-scan.log
HOSTNAME=$(hostname -f 2>/dev/null || hostname)
DATE=$(date +%Y-%m-%d)
{
    echo "=== ansipa-security-scan: $DATE $HOSTNAME ==="

    if command -v freshclam &>/dev/null; then
        freshclam --quiet 2>/dev/null || true
    fi
    if command -v clamscan &>/dev/null; then
        clamscan -r --infected --quiet /home /etc /tmp /var/tmp 2>/dev/null || true
    fi
    if command -v rkhunter &>/dev/null; then
        rkhunter --update --quiet 2>/dev/null || true
        rkhunter --check --skip-keypress --quiet 2>/dev/null || true
    fi
    if command -v chkrootkit &>/dev/null; then
        chkrootkit 2>/dev/null || true
    fi

    echo "=== scan complete ==="
} >> "$LOG" 2>&1

# ── Upload to server SMB share ────────────────────────────────────────────────
IPA_SERVER=$(awk '/^server[[:space:]]*=/{print $3}' /etc/ipa/default.conf 2>/dev/null || echo "")
if [[ -n "$IPA_SERVER" ]] && [[ -f /etc/ansipa-smb.creds ]] && command -v smbclient &>/dev/null; then
    # Create host archive dir (mkdir is idempotent; errors suppressed).
    smbclient "//$IPA_SERVER/ansipa-scans" -A /etc/ansipa-smb.creds \
        -c "mkdir archive; mkdir archive\\$HOSTNAME; put $LOG archive\\$HOSTNAME\\$DATE.log" \
        >> "$LOG" 2>&1 \
        && echo "[ansipa] Scan results uploaded to $IPA_SERVER/ansipa-scans/archive/$HOSTNAME/$DATE.log" >> "$LOG" \
        || echo "[ansipa][WARN] SMB upload failed — results remain local at $LOG" >> "$LOG"
else
    echo "[ansipa] SMB upload skipped (no credentials or smbclient not found)." >> "$LOG"
fi
SCAN
    chmod 755 "$SCAN_SCRIPT"

    if [[ ! -f "$SCAN_CRON" ]]; then
        log "Enabling daily security scans (ClamAV / rkhunter / chkrootkit)"
        cat > "$SCAN_CRON" <<'CRON'
# ansipa-policy-security-scan: managed by ansipa-enforce-policies — do not edit manually.
# Install scan tools by adding the host to the ansipa-module-anti-malware group.
0 2 * * * root /usr/local/bin/ansipa-security-scan.sh
CRON
        chmod 644 "$SCAN_CRON"
    fi
else
    if [[ -f "$SCAN_CRON" ]]; then
        rm -f "$SCAN_CRON"
        rm -f "$SCAN_SCRIPT"
        log "Removed security scan policy (host left policy-security-scan group)"
    fi
fi

# ── Scan notification daemon ──────────────────────────────────────────────────
# policy-scan-notify is a FreeIPA *user* group (not a host group).  The fetch-
# alerts timer runs fleet-wide on any host where the group exists; the profile.d
# snippet starts the notification daemon on login only for group members
# (checked via id(1) / SSSD so no IPA query is needed at login time).
#   - Root timer (every 10 min): ansipa-fetch-alerts.sh downloads alerts from the
#     server SMB share and places them in ~/administration/<hostname>/ per active user.
#   - profile.d snippet: starts ansipa-scan-notify.sh as a user daemon on login;
#     the daemon sends notify-send every 10 min while *.alert files remain.
#     Deleting a file from ~/administration/ counts as acknowledgment.
#
# Requires: ansipa-fetch-alerts.sh and ansipa-scan-notify.sh deployed by
# deploy-ansipa-policies.yml (static scripts — not written inline here).

FETCH_SVC="/etc/systemd/system/ansipa-fetch-alerts.service"
FETCH_TIMER="/etc/systemd/system/ansipa-fetch-alerts.timer"
NOTIFY_PROFILED="/etc/profile.d/ansipa-notify.sh"

if [[ "$WANT_SCAN_NOTIFY" == true ]]; then
    if [[ ! -x /usr/local/bin/ansipa-fetch-alerts.sh ]]; then
        warn "ansipa-fetch-alerts.sh not found — run deploy-ansipa-policies.yml first."
    fi

    if [[ ! -f "$FETCH_SVC" ]]; then
        log "Installing ansipa-fetch-alerts systemd service + timer"
        cat > "$FETCH_SVC" <<'UNIT'
[Unit]
Description=Fetch Ansipa security alerts from the server SMB share
After=network-online.target sssd.service
Wants=network-online.target

[Service]
Type=oneshot
ExecStart=/usr/local/bin/ansipa-fetch-alerts.sh
StandardOutput=journal
StandardError=journal
UNIT

        cat > "$FETCH_TIMER" <<'UNIT'
[Unit]
Description=Periodic ansipa security alert fetch

[Timer]
OnBootSec=2min
OnUnitActiveSec=10min

[Install]
WantedBy=timers.target
UNIT
        systemctl daemon-reload
        systemctl enable --now ansipa-fetch-alerts.timer
        log "ansipa-fetch-alerts.timer enabled"
    fi

    if [[ ! -f "$NOTIFY_PROFILED" ]]; then
        log "Installing /etc/profile.d/ansipa-notify.sh"
        cat > "$NOTIFY_PROFILED" <<'PROFILED'
# ansipa-notify: launch the scan alert notification daemon on login for
# members of the policy-scan-notify FreeIPA user group.
# Managed by ansipa-enforce-policies — do not edit manually.
_NOTIFY_DAEMON=/usr/local/bin/ansipa-scan-notify.sh
if [[ -x "$_NOTIFY_DAEMON" ]] && \
   id -nG 2>/dev/null | grep -qw "policy-scan-notify" && \
   ! pgrep -u "$(id -u)" -f "ansipa-scan-notify" >/dev/null 2>&1; then
    "$_NOTIFY_DAEMON" &
    disown
fi
unset _NOTIFY_DAEMON
PROFILED
        chmod 644 "$NOTIFY_PROFILED"
    fi
else
    if [[ -f "$FETCH_TIMER" ]]; then
        systemctl disable --now ansipa-fetch-alerts.timer 2>/dev/null || true
        rm -f "$FETCH_SVC" "$FETCH_TIMER"
        systemctl daemon-reload
        log "Removed ansipa-fetch-alerts timer (policy-scan-notify user group no longer exists)"
    fi
    if [[ -f "$NOTIFY_PROFILED" ]]; then
        rm -f "$NOTIFY_PROFILED"
        log "Removed /etc/profile.d/ansipa-notify.sh"
    fi
fi

# ── Daemon enable / disable ───────────────────────────────────────────────────
# policy-daemon-enable-<unit>:  ensure the unit is enabled and running.
#   Leaving the group reverts: unit is disabled and stopped.
# policy-daemon-disable-<unit>: ensure the unit is disabled and stopped.
#   Leaving the group reverts: unit is re-enabled and started.
# <unit> may omit the .service suffix; systemd accepts both forms.
# Conflicts (unit in both lists): logged as a warning, unit is left untouched.

DAEMON_ENABLE_STATE="$STATE_DIR/daemon-enabled"
DAEMON_DISABLE_STATE="$STATE_DIR/daemon-disabled"
[[ -f "$DAEMON_ENABLE_STATE"  ]] || touch "$DAEMON_ENABLE_STATE"
[[ -f "$DAEMON_DISABLE_STATE" ]] || touch "$DAEMON_DISABLE_STATE"

# Append .service only when the name has no unit-type suffix already.
_svc_unit() { [[ "$1" == *.* ]] && echo "$1" || echo "${1}.service"; }

_in_enable_list()  { local n="$1"; for s in "${ACTIVE_DAEMON_ENABLE[@]}";  do [[ "$s" == "$n" ]] && return 0; done; return 1; }
_in_disable_list() { local n="$1"; for s in "${ACTIVE_DAEMON_DISABLE[@]}"; do [[ "$s" == "$n" ]] && return 0; done; return 1; }

# Apply enable policies
for _SVC in "${ACTIVE_DAEMON_ENABLE[@]}"; do
    if _in_disable_list "$_SVC"; then
        warn "Conflict: '$_SVC' is in both daemon-enable and daemon-disable groups — skipped"
        continue
    fi
    _UNIT=$(_svc_unit "$_SVC")
    _EN=$(systemctl is-enabled "$_UNIT" 2>/dev/null || echo "not-found")
    _AC=$(systemctl is-active  "$_UNIT" 2>/dev/null || echo "inactive")
    if [[ "$_EN" != "enabled" || "$_AC" != "active" ]]; then
        log "Enabling service: $_UNIT (enabled=$_EN active=$_AC)"
        systemctl enable --now "$_UNIT" 2>/dev/null \
            && log "Service enabled: $_UNIT" \
            || warn "Failed to enable $_UNIT — unit may not exist on this host"
    fi
done

# Apply disable policies
for _SVC in "${ACTIVE_DAEMON_DISABLE[@]}"; do
    if _in_enable_list "$_SVC"; then
        continue  # conflict already warned above
    fi
    _UNIT=$(_svc_unit "$_SVC")
    _EN=$(systemctl is-enabled "$_UNIT" 2>/dev/null || echo "not-found")
    _AC=$(systemctl is-active  "$_UNIT" 2>/dev/null || echo "inactive")
    if [[ "$_EN" == "enabled" || "$_AC" == "active" ]]; then
        log "Disabling service: $_UNIT (enabled=$_EN active=$_AC)"
        systemctl disable --now "$_UNIT" 2>/dev/null \
            && log "Service disabled: $_UNIT" \
            || warn "Failed to disable $_UNIT — unit may not exist on this host"
    fi
done

# Revert: host left a daemon-enable group → disable and stop the service
while IFS= read -r _OLD; do
    [[ -z "$_OLD" ]] && continue
    if ! _in_enable_list "$_OLD"; then
        _UNIT=$(_svc_unit "$_OLD")
        log "Reverting enable policy: disabling $_UNIT (host left daemon-enable group)"
        systemctl disable --now "$_UNIT" 2>/dev/null \
            || warn "Failed to disable (revert) $_UNIT"
    fi
done < "$DAEMON_ENABLE_STATE"

# Revert: host left a daemon-disable group → re-enable and start the service
while IFS= read -r _OLD; do
    [[ -z "$_OLD" ]] && continue
    if ! _in_disable_list "$_OLD"; then
        _UNIT=$(_svc_unit "$_OLD")
        log "Reverting disable policy: enabling $_UNIT (host left daemon-disable group)"
        systemctl enable --now "$_UNIT" 2>/dev/null \
            || warn "Failed to enable (revert) $_UNIT"
    fi
done < "$DAEMON_DISABLE_STATE"

# Persist current state
if [[ ${#ACTIVE_DAEMON_ENABLE[@]} -gt 0 ]]; then
    printf '%s\n' "${ACTIVE_DAEMON_ENABLE[@]}" | sort -u > "$DAEMON_ENABLE_STATE"
else
    > "$DAEMON_ENABLE_STATE"
fi
if [[ ${#ACTIVE_DAEMON_DISABLE[@]} -gt 0 ]]; then
    printf '%s\n' "${ACTIVE_DAEMON_DISABLE[@]}" | sort -u > "$DAEMON_DISABLE_STATE"
else
    > "$DAEMON_DISABLE_STATE"
fi

# ── Per-device local sudo grants ──────────────────────────────────────────────
# local_sudo_<username>: write a sudoers drop-in granting <username> full sudo on
#   this specific device.  The drop-in is removed when the host leaves the group.

LOCAL_SUDO_DIR="/etc/sudoers.d"
LOCAL_SUDO_STATE="$STATE_DIR/local-sudo-users"
[[ -f "$LOCAL_SUDO_STATE" ]] || touch "$LOCAL_SUDO_STATE"

for _USER in "${ACTIVE_LOCAL_SUDO_USERS[@]}"; do
    _DROPIN="$LOCAL_SUDO_DIR/ansipa-local-sudo-${_USER}"
    if [[ ! -f "$_DROPIN" ]]; then
        log "Granting local sudo to $_USER on this device"
        echo "$_USER ALL=(ALL) ALL" > "$_DROPIN"
        chmod 440 "$_DROPIN"
    fi
    grep -qxF "$_USER" "$LOCAL_SUDO_STATE" 2>/dev/null || echo "$_USER" >> "$LOCAL_SUDO_STATE"
done

# Revoke sudo for users no longer in any active local_sudo_* group.
while IFS= read -r _OLD_USER; do
    [[ -z "$_OLD_USER" ]] && continue
    _still_active=false
    for _U in "${ACTIVE_LOCAL_SUDO_USERS[@]}"; do
        [[ "$_U" == "$_OLD_USER" ]] && _still_active=true && break
    done
    if [[ "$_still_active" == false ]]; then
        _DROPIN="$LOCAL_SUDO_DIR/ansipa-local-sudo-${_OLD_USER}"
        if [[ -f "$_DROPIN" ]]; then
            rm -f "$_DROPIN"
            log "Revoked local sudo for $_OLD_USER (host left local_sudo_$_OLD_USER group)"
        fi
    fi
done < "$LOCAL_SUDO_STATE"

# Persist current local sudo users.
if [[ ${#ACTIVE_LOCAL_SUDO_USERS[@]} -gt 0 ]]; then
    printf '%s\n' "${ACTIVE_LOCAL_SUDO_USERS[@]}" | sort -u > "$LOCAL_SUDO_STATE"
else
    > "$LOCAL_SUDO_STATE"
fi
unset _USER _DROPIN _OLD_USER _still_active _U

# ── No-local-users policy ──────────────────────────────────────────────────────
# no_local_users: lock the passwords of root and all local users (UID >= 1000)
#   so that only FreeIPA domain accounts with centrally-managed sudo rules can
#   authenticate and gain elevated privileges.
# Leaving the group reverts: every account locked by this policy is unlocked.

NO_LOCAL_USERS_STATE="$STATE_DIR/no-local-users"

_apply_no_local_users() {
    log "Applying no_local_users policy — locking local account passwords"
    [[ -f "$NO_LOCAL_USERS_STATE" ]] || touch "$NO_LOCAL_USERS_STATE"

    while IFS=: read -r uname _ uid _; do
        [[ "$uid" =~ ^[0-9]+$ ]] || continue
        { [[ "$uid" == "0" ]] || [[ "$uid" -ge 1000 ]]; } || continue

        # Skip accounts already tracked (locked on a previous run)
        grep -qxF "$uname" "$NO_LOCAL_USERS_STATE" 2>/dev/null && continue

        # Lock only accounts that currently have a real (unlocked) password hash
        local hash
        hash=$(getent shadow "$uname" 2>/dev/null | cut -d: -f2 || true)
        [[ -z "$hash" || "$hash" == '!'* || "$hash" == '*'* ]] && continue

        if passwd -l "$uname" &>/dev/null; then
            echo "$uname" >> "$NO_LOCAL_USERS_STATE"
            log "Locked local account: $uname"
        else
            warn "Failed to lock local account: $uname"
        fi
    done < /etc/passwd
}

_revert_no_local_users() {
    [[ -f "$NO_LOCAL_USERS_STATE" ]] || return 0
    log "Reverting no_local_users policy — unlocking previously locked accounts"
    while IFS= read -r uname; do
        [[ -z "$uname" ]] && continue
        if passwd -u "$uname" &>/dev/null; then
            log "Unlocked local account: $uname"
        else
            warn "Failed to unlock local account: $uname (may have been removed)"
        fi
    done < "$NO_LOCAL_USERS_STATE"
    > "$NO_LOCAL_USERS_STATE"
}

if [[ "$WANT_NO_LOCAL_USERS" == true ]]; then
    _apply_no_local_users
else
    if [[ -f "$NO_LOCAL_USERS_STATE" ]] && [[ -s "$NO_LOCAL_USERS_STATE" ]]; then
        _revert_no_local_users
    fi
fi

log "Policy enforcement complete."
