Dotfiles/setup/modules/FreeipaAnsible/ansible/lib/ansipa-policy.sh

162 lines
8.5 KiB
Bash

#!/usr/bin/env bash
# ansipa-policy.sh — shared library for ansipa policy enforcement.
#
# Sourced by ansipa-enforce-policies.sh before any policy file is loaded.
# Provides: constants, logging, group discovery, policystore sync,
# and the _smb_parse_cred() helper (shared across usr_smb and usr_policystore).
#
# After _ansipa_discover() the following variables are exported for policy files:
# RAW_GROUPS (comma-sep host-group string from ipa host-show)
# _ALL_USER_GROUPS (newline-sep full list from one ipa group-find call)
# ACTIVE_DAEMON_ENABLE ACTIVE_DAEMON_DISABLE (arrays, unit name suffixes)
# WANT_TIMESHIFT_BACKUP WANT_CLAMSCAN WANT_NO_LOCAL_USERS (booleans)
# ACTIVE_LOCAL_SUDO_USERS ACTIVE_SSH_USERS ACTIVE_MON_DEV (arrays)
# WANT_USR_ADMIN (boolean)
# ACTIVE_BLOCK_BINARIES ACTIVE_BLOCK_IPA_GROUPS (parallel arrays)
# ACTIVE_PRT_GROUPS ACTIVE_PRT_PRINTERS (parallel arrays)
# ACTIVE_MON_USR (array, usr_mon_* suffixes)
# ── Constants ─────────────────────────────────────────────────────────────────
STATE_DIR="/var/lib/ansipa-policies"
BLOCK_DIR="/usr/local/bin"
CRON_DIR="/etc/cron.d"
LOG_TAG="ansipa-policies"
# Policy source resolution: /run/ansipa/current is a tmpfs workdir populated
# fresh on every ansipa-pull-apply.sh run with content whose commit has ALREADY
# been GPG-signature-verified and fast-forward-checked (see ansipa-pull-apply.sh
# and deploy-ansipa-git-pull.yml) — it is the trusted, live source once the git
# pull system is bootstrapped. Before that first successful pull (or if a node
# is never enrolled in it), fall back to the Ansible-deployed baseline at
# /usr/local/lib/ansipa/policies.d. There is no third, unverified source —
# the old SMB-policystore auto-sync (which sourced whatever unsigned .sh files
# sat on the share, with no integrity check) has been removed.
if [[ -d /run/ansipa/current/policies.d ]]; then
POLICY_DIR="/run/ansipa/current/policies.d"
else
POLICY_DIR="/usr/local/lib/ansipa/policies.d"
fi
# ── Logging ───────────────────────────────────────────────────────────────────
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; }
# ── Hostname resolution ───────────────────────────────────────────────────────
# The `hostname` binary (from inetutils) is NOT installed on a minimal Arch
# system, so `hostname -f` aborts the enforcer with "command not found". Resolve
# the FQDN without depending on it: prefer hostnamectl, then the FQDN via getent
# on the static hostname, then /etc/hostname, then the kernel hostname. Every
# enrolled host has an FQDN (ipa-client-install sets it), so this resolves.
ansipa_fqdn() {
local h=""
if command -v hostname &>/dev/null; then
h=$(hostname -f 2>/dev/null) || h=""
fi
[[ -z "$h" ]] && h=$(hostnamectl --static 2>/dev/null || true)
[[ -z "$h" ]] && h=$(cat /etc/hostname 2>/dev/null || true)
[[ -z "$h" ]] && h=$(cat /proc/sys/kernel/hostname 2>/dev/null || true)
# Upgrade a short name to the FQDN via NSS if resolvable.
if [[ "$h" != *.* ]] && command -v getent &>/dev/null; then
local fqdn; fqdn=$(getent hosts "$h" 2>/dev/null | awk '{for(i=2;i<=NF;i++) if($i ~ /\./){print $i; exit}}')
[[ -n "$fqdn" ]] && h="$fqdn"
fi
printf '%s' "$h"
}
# ── Group discovery ───────────────────────────────────────────────────────────
# Called once by the orchestrator. Sets all policy-state variables needed by
# every policy file, using exactly two IPA queries (one host-show, one group-find).
_ansipa_discover() {
# ── Host groups (device policies) ─────────────────────────────────────────
RAW_GROUPS=$(ipa host-show "$HOST_FQDN" --all 2>/dev/null \
| grep -i "Member of host-groups:" | sed 's/.*: //' || true)
ACTIVE_DAEMON_ENABLE=()
ACTIVE_DAEMON_DISABLE=()
ACTIVE_LOCAL_SUDO_USERS=()
ACTIVE_SSH_USERS=()
ACTIVE_MON_DEV=()
WANT_TIMESHIFT_BACKUP=false
WANT_CLAMSCAN=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
dev_daemon-enable-*) ACTIVE_DAEMON_ENABLE+=("${g#dev_daemon-enable-}") ;;
dev_daemon-disable-*) ACTIVE_DAEMON_DISABLE+=("${g#dev_daemon-disable-}") ;;
dev_timeshift-backup) WANT_TIMESHIFT_BACKUP=true ;;
dev_no-local-users) WANT_NO_LOCAL_USERS=true ;;
dev_local-sudo-*) ACTIVE_LOCAL_SUDO_USERS+=("${g#dev_local-sudo-}") ;;
dev_ssh_*) ACTIVE_SSH_USERS+=("${g#dev_ssh_}") ;;
# dev_mon_clamscan runs the scan + emits its own CheckMK check;
# match it BEFORE the generic dev_mon_* catch-all so it sets
# WANT_CLAMSCAN instead of being treated as a dev_mon.sh check.
dev_mon_clamscan) WANT_CLAMSCAN=true ;;
dev_mon_*) ACTIVE_MON_DEV+=("${g#dev_mon_}") ;;
esac
done
done <<< "$RAW_GROUPS"
fi
# ── User groups (one call, all prefixes) ──────────────────────────────────
_ALL_USER_GROUPS=$(ipa group-find --pkey-only 2>/dev/null \
| awk '/Group name:/ {print $NF}' | sort -u || true)
WANT_USR_ADMIN=false
ACTIVE_BLOCK_BINARIES=()
ACTIVE_BLOCK_IPA_GROUPS=()
ACTIVE_PRT_GROUPS=()
ACTIVE_PRT_PRINTERS=()
ACTIVE_MON_USR=()
while IFS= read -r _grp; do
[[ -z "$_grp" ]] && continue
case "$_grp" in
usr_admin) WANT_USR_ADMIN=true ;;
usr_block-binary-*)
local _raw="${_grp#usr_block-binary-}"
ACTIVE_BLOCK_BINARIES+=("${_raw//__/.}")
ACTIVE_BLOCK_IPA_GROUPS+=("$_grp") ;;
usr_prt_*)
ACTIVE_PRT_GROUPS+=("$_grp")
ACTIVE_PRT_PRINTERS+=("${_grp#usr_prt_}") ;;
usr_mon_*)
ACTIVE_MON_USR+=("${_grp#usr_mon_}") ;;
esac
done <<< "$_ALL_USER_GROUPS"
unset _grp _raw
log "Device policies — daemon-enable: ${ACTIVE_DAEMON_ENABLE[*]:-none}" \
"| daemon-disable: ${ACTIVE_DAEMON_DISABLE[*]:-none}" \
"| timeshift-backup: $WANT_TIMESHIFT_BACKUP | clamscan: $WANT_CLAMSCAN" \
"| no-local-users: $WANT_NO_LOCAL_USERS | local-sudo: ${ACTIVE_LOCAL_SUDO_USERS[*]:-none}" \
"| ssh-keys: ${ACTIVE_SSH_USERS[*]:-none} | mon-dev: ${ACTIVE_MON_DEV[*]:-none}"
log "User policies — admin: $WANT_USR_ADMIN" \
"| block-binary: ${ACTIVE_BLOCK_BINARIES[*]:-none}" \
"| printers: ${ACTIVE_PRT_PRINTERS[*]:-none}" \
"| mon-usr: ${ACTIVE_MON_USR[*]:-none}"
}
# ── SMB credential parser ─────────────────────────────────────────────────────
# Used by both usr_smb.sh and usr_policystore.sh.
# Outputs "host samba_user password" when the IPA group description contains
# a cifs:// credential (written by ansipa-smb-setup.sh).
_smb_parse_cred() {
local _desc
_desc=$(ipa group-show "$1" --all 2>/dev/null \
| awk -F': ' '/Description:/{print $2; exit}' || true)
[[ "$_desc" =~ ^cifs://([^:]+):([^:]+):(.+)$ ]] && \
echo "${BASH_REMATCH[1]} ${BASH_REMATCH[2]} ${BASH_REMATCH[3]}"
}
# NOTE: this file previously had an _ansipa_sync_policystore() here, which
# synced *.sh files from the ansipa-policystore SMB share directly into
# $POLICY_DIR with no integrity check before they were sourced as root. That
# blind-trust path has been removed; policy content now only reaches
# $POLICY_DIR via ansipa-pull-apply.sh, which requires a valid GPG signature
# and a fast-forward history before anything is applied. See
# docs/md/freeipa-ansible.md, "Git-based policy distribution".