#!/bin/bash
# ansipa-smb-setup.sh — configure Samba shares on the IPA container.
#
# Runs on every container start via ansipa-smb.service so that smb.conf and
# Samba users are always in place after container restarts (ephemeral rootfs).
#
# Password sources (first match wins per variable):
#   1. Environment variable         (first boot / explicit override)
#   2. /data/samba/ansipa-smb.env  (persisted from first boot)
#
# Shares:
#   ansipa-luks-keys    — write-only for 'luks-upload' (Ansible controller);
#                         read for members of the 'KeyAdmin' Linux group.
#                         Add a Samba user to KeyAdmin to grant key-read access:
#                           useradd -r -G KeyAdmin <user>
#                           smbpasswd -a <user>
#   ansipa-policystore  — read-write for 'smb-policystore'; admins in the IPA group
#                         usr_smb_adm_policystore mount this to edit policy files live.
#                         Password auto-generated and stored in the IPA group description:
#                           cifs://<host>:smb-policystore:<password>
#                         The enforcer on clients reads this credential to sync policy files.

set -euo pipefail

LOG_TAG="ansipa-smb-setup"
LUKS_BASE="/data/luks-keys"
SHARES_BASE="/data/smb-shares"
POLICY_STORE_BASE="/data/policy-store"
SMB_CONF="/etc/samba/smb.conf"
LUKS_UPLOAD_USER="luks-upload"
POLICY_STORE_USER="smb-policystore"
KEYADMIN_GROUP="KeyAdmin"
ENV_FILE="/data/samba/ansipa-smb.env"

log()  { echo "[$LOG_TAG] $*"; }
warn() { echo "[$LOG_TAG][WARN] $*" >&2; }
die()  { echo "[$LOG_TAG][ERROR] $*" >&2; exit 1; }

# ── Resolve passwords ─────────────────────────────────────────────────────────
LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-}"
IPA_ADMIN_PASS="${IPA_ADMIN_PASSWORD:-}"

if [[ -f "$ENV_FILE" ]]; then
    # shellcheck source=/dev/null
    source "$ENV_FILE"
    LUKS_PASS="${LUKS_KEY_UPLOAD_PASSWORD:-$LUKS_PASS}"
    IPA_ADMIN_PASS="${IPA_ADMIN_PASSWORD:-$IPA_ADMIN_PASS}"
fi

[[ -z "$LUKS_PASS" ]] && die "LUKS_KEY_UPLOAD_PASSWORD not set and $ENV_FILE not present. Set it in .env."

# ── Persist for subsequent restarts ──────────────────────────────────────────
mkdir -p "$(dirname "$ENV_FILE")"
{
    printf 'LUKS_KEY_UPLOAD_PASSWORD=%q\n' "$LUKS_PASS"
    [[ -n "$IPA_ADMIN_PASS" ]] && printf 'IPA_ADMIN_PASSWORD=%q\n' "$IPA_ADMIN_PASS" || true
} > "$ENV_FILE"
chmod 600 "$ENV_FILE"

# ── Directory structure (idempotent) ──────────────────────────────────────────
mkdir -p "$LUKS_BASE"
mkdir -p "$SHARES_BASE"
mkdir -p "$POLICY_STORE_BASE/policies.d"

# ── KeyAdmin group ────────────────────────────────────────────────────────────
if ! getent group "$KEYADMIN_GROUP" &>/dev/null; then
    groupadd -r "$KEYADMIN_GROUP"
    log "Created group: $KEYADMIN_GROUP"
fi

# ── System users ──────────────────────────────────────────────────────────────
if ! id "$POLICY_STORE_USER" &>/dev/null; then
    useradd -r -s /sbin/nologin -d "$POLICY_STORE_BASE" -M "$POLICY_STORE_USER"
    log "Created system user: $POLICY_STORE_USER"
fi

if ! id "$LUKS_UPLOAD_USER" &>/dev/null; then
    useradd -r -s /sbin/nologin -d "$LUKS_BASE" -M -G "$KEYADMIN_GROUP" "$LUKS_UPLOAD_USER"
    log "Created system user: $LUKS_UPLOAD_USER (member of $KEYADMIN_GROUP)"
else
    # Ensure group membership is correct after container recreations
    usermod -aG "$KEYADMIN_GROUP" "$LUKS_UPLOAD_USER"
fi

chown -R "root:$KEYADMIN_GROUP" "$LUKS_BASE"
chmod 2750 "$LUKS_BASE"   # setgid so new files inherit KeyAdmin group
chown -R "$POLICY_STORE_USER:$POLICY_STORE_USER" "$POLICY_STORE_BASE"
chmod 2770 "$POLICY_STORE_BASE"

# ── smb.conf (base only; _setup_user_shares appends stanzas afterwards) ───────
_write_smb_conf() {
    log "Writing $SMB_CONF"
    cat > "$SMB_CONF" <<CONF
[global]
    workgroup = WORKGROUP
    server string = Ansipa Security Server
    security = user
    map to guest = never
    # Store passdb on the persistent volume so passwords survive container restarts.
    passdb backend = tdbsam:/data/samba/passdb.tdb
    log file = /var/log/samba/log.%m
    max log size = 50
    # Disable printing subsystem entirely.
    load printers = no
    printing = bsd
    printcap name = /dev/null
    disable spoolss = yes

[ansipa-luks-keys]
    comment = Ansipa LUKS backup keys — KeyAdmin read, luks-upload write only
    path = $LUKS_BASE
    valid users = @$KEYADMIN_GROUP
    read only = yes
    write list = $LUKS_UPLOAD_USER
    browseable = no
    create mask = 0640
    directory mask = 0750

[ansipa-policystore]
    comment = Ansipa policy store — admin rw; enforcer syncs policy files from here
    path = $POLICY_STORE_BASE
    valid users = $POLICY_STORE_USER
    read only = no
    browseable = no
    force user = $POLICY_STORE_USER
    force group = $POLICY_STORE_USER
    create mask = 0664
    directory mask = 02775
CONF
}

# ── Samba passwords (idempotent — smbpasswd -a adds or updates) ───────────────
_smb_set_pass() {
    local user="$1" pass="$2"
    log "Setting Samba password for $user"
    printf '%s\n%s\n' "$pass" "$pass" | smbpasswd -a -s "$user" 2>/dev/null || \
    printf '%s\n%s\n' "$pass" "$pass" | smbpasswd    -s "$user" 2>/dev/null || \
    log "WARN: smbpasswd returned non-zero for $user (may already be set correctly)"
}

# ── User-share management (usr_smb_r_<name> / usr_smb_rw_<name> IPA groups) ──
# Each group maps to a Samba share under $SHARES_BASE/<name>/:
#   usr_smb_r_<name>   → read-only  share [usr-<name>-r]
#   usr_smb_rw_<name>  → read-write share [usr-<name>-rw]
#
# Both access levels for the same share use one Linux user (smb-<name>) and one
# password; the difference is read-only vs read-write enforced by Samba.
# Credentials are auto-generated on first setup and stored in the IPA group
# description so clients can retrieve them:
#   cifs://<ipa-host>:<samba-user>:<password>
#
# The server kinits as admin to query/update IPA.  If IPA is not yet configured
# (first-boot race), this function exits gracefully; the next smb.service restart
# will pick up the groups once IPA is ready.

# Appends user share stanzas directly to SMB_CONF; logs go to stdout→journal.
# Split from _write_smb_conf to avoid stdout capture swallowing log lines.
_setup_user_shares() {
    # Require IPA to be configured on this host already.
    if [[ ! -f /etc/ipa/default.conf ]]; then
        warn "IPA not yet configured — skipping user share setup (restart after ipa-first-boot)"
        return 0
    fi
    if [[ -z "$IPA_ADMIN_PASS" ]]; then
        warn "IPA_ADMIN_PASSWORD not set — skipping user share setup"
        return 0
    fi

    local realm
    realm=$(awk -F'[[:space:]]*=[[:space:]]*' '/^realm[[:space:]]*=/{print $2; exit}' \
        /etc/ipa/default.conf 2>/dev/null || echo "")
    # On the IPA server itself the 'server' field points to itself; prefer hostname -f.
    local ipa_host
    ipa_host=$(hostname -f 2>/dev/null || echo "ipa.test.local")

    echo "$IPA_ADMIN_PASS" | kinit "admin@${realm}" &>/dev/null || {
        warn "kinit admin@${realm} failed — skipping user share setup"
        return 0
    }

    # Discover usr_smb_* groups.
    local -a smb_groups
    mapfile -t smb_groups < <(
        ipa group-find --pkey-only 2>/dev/null \
            | awk '/Group name:/ {print $NF}' \
            | grep "^usr_smb_" | sort -u || true
    )
    if [[ ${#smb_groups[@]} -eq 0 ]]; then
        log "No usr_smb_* groups found — no user shares to configure"
        kdestroy &>/dev/null || true
        return 0
    fi

    # Collect unique share names and track which access levels exist.
    declare -A _HAS_R _HAS_RW _DONE
    for _g in "${smb_groups[@]}"; do
        if   [[ "$_g" =~ ^usr_smb_r_(.+)$  ]]; then _HAS_R["${BASH_REMATCH[1]}"]=1
        elif [[ "$_g" =~ ^usr_smb_rw_(.+)$ ]]; then _HAS_RW["${BASH_REMATCH[1]}"]=1
        fi
    done

    for NAME in "${!_HAS_R[@]}" "${!_HAS_RW[@]}"; do
        [[ -n "${_DONE[$NAME]+x}" ]] && continue
        _DONE["$NAME"]=1

        local SUSER="smb-${NAME}"
        local SDIR="$SHARES_BASE/$NAME"
        log "User share: $NAME  path=$SDIR  user=$SUSER"

        # Create directory and Linux user.
        mkdir -p "$SDIR"
        if ! id "$SUSER" &>/dev/null; then
            useradd -r -s /sbin/nologin -d "$SDIR" -M "$SUSER"
            log "  Created system user: $SUSER"
        fi
        chown "$SUSER:$SUSER" "$SDIR"
        chmod 2770 "$SDIR"   # setgid — new files inherit group; no world access

        # Retrieve existing password from IPA group description or generate one.
        local PASS=""
        local _desc _grp
        for _grp in "usr_smb_r_${NAME}" "usr_smb_rw_${NAME}"; do
            ipa group-show "$_grp" &>/dev/null || continue
            _desc=$(ipa group-show "$_grp" --all 2>/dev/null \
                | awk -F': ' '/Description:/{print $2; exit}' || true)
            # Description format: cifs://<host>:<samba-user>:<password>
            if [[ "$_desc" =~ ^cifs://[^:]*:smb-[^:]+:(.+)$ ]]; then
                PASS="${BASH_REMATCH[1]}"
                break
            fi
        done
        if [[ -z "$PASS" ]]; then
            PASS=$(tr -dc 'A-Za-z0-9' < /dev/urandom 2>/dev/null | head -c 32 \
                   || openssl rand -base64 24 | tr -d '/+=\n')
            log "  Generated new password for $SUSER"
        fi

        # Set Samba password.
        _smb_set_pass "$SUSER" "$PASS"

        # Store credential in both group descriptions so clients can read it.
        local CRED="cifs://${ipa_host}:${SUSER}:${PASS}"
        if [[ -n "${_HAS_R[$NAME]+x}" ]]; then
            ipa group-mod "usr_smb_r_${NAME}" --desc="$CRED" &>/dev/null && \
                log "  Stored credential in usr_smb_r_${NAME}" || true
        fi
        if [[ -n "${_HAS_RW[$NAME]+x}" ]]; then
            ipa group-mod "usr_smb_rw_${NAME}" --desc="$CRED" &>/dev/null && \
                log "  Stored credential in usr_smb_rw_${NAME}" || true
        fi

        # Append share stanzas directly to smb.conf.
        # Clients mount [usr-<name>-r] or [usr-<name>-rw]; folder in ~ is just <name>.
        if [[ -n "${_HAS_R[$NAME]+x}" ]]; then
            cat >> "$SMB_CONF" <<STANZA

[usr-${NAME}-r]
    comment = Ansipa user share: ${NAME} (read-only)
    path = ${SDIR}
    valid users = ${SUSER}
    read only = yes
    browseable = no
    force user = ${SUSER}
    force group = ${SUSER}
STANZA
        fi
        if [[ -n "${_HAS_RW[$NAME]+x}" ]]; then
            cat >> "$SMB_CONF" <<STANZA

[usr-${NAME}-rw]
    comment = Ansipa user share: ${NAME} (read-write)
    path = ${SDIR}
    valid users = ${SUSER}
    read only = no
    write list = ${SUSER}
    browseable = no
    force user = ${SUSER}
    force group = ${SUSER}
    create mask = 0664
    directory mask = 02775
STANZA
        fi
    done
    unset _HAS_R _HAS_RW _DONE _g NAME SUSER SDIR PASS _desc _grp CRED
    kdestroy &>/dev/null || true
}

# ── Policystore share management (usr_smb_adm_policystore IPA user group) ────
# Creates the ansipa-policystore Samba share with auto-generated credentials.
# Credential is stored in the IPA group description so clients can discover it:
#   cifs://<host>:smb-policystore:<password>
# Uses the same kinit/kdestroy pattern as _setup_user_shares.
_setup_policystore() {
    if [[ ! -f /etc/ipa/default.conf ]]; then
        warn "IPA not yet configured — skipping policystore setup (restart after ipa-first-boot)"
        return 0
    fi
    if [[ -z "$IPA_ADMIN_PASS" ]]; then
        warn "IPA_ADMIN_PASSWORD not set — skipping policystore setup"
        return 0
    fi

    local realm ipa_host
    realm=$(awk -F'[[:space:]]*=[[:space:]]*' '/^realm[[:space:]]*=/{print $2; exit}' \
        /etc/ipa/default.conf 2>/dev/null || echo "")
    ipa_host=$(hostname -f 2>/dev/null || echo "ipa.test.local")

    echo "$IPA_ADMIN_PASS" | kinit "admin@${realm}" &>/dev/null || {
        warn "kinit admin@${realm} failed — skipping policystore setup"
        return 0
    }

    # Retrieve existing password from the IPA group description or generate one.
    local PASS=""
    local _desc
    if ipa group-show usr_smb_adm_policystore &>/dev/null; then
        _desc=$(ipa group-show usr_smb_adm_policystore --all 2>/dev/null \
            | awk -F': ' '/Description:/{print $2; exit}' || true)
        if [[ "$_desc" =~ ^cifs://[^:]*:smb-policystore:(.+)$ ]]; then
            PASS="${BASH_REMATCH[1]}"
        fi
    fi
    if [[ -z "$PASS" ]]; then
        PASS=$(tr -dc 'A-Za-z0-9' < /dev/urandom 2>/dev/null | head -c 32 \
               || openssl rand -base64 24 | tr -d '/+=\n')
        log "Generated new password for $POLICY_STORE_USER"
    fi

    _smb_set_pass "$POLICY_STORE_USER" "$PASS"

    # Create the IPA group if absent, then store/update the credential in its description.
    ipa group-add usr_smb_adm_policystore \
        --desc="cifs://${ipa_host}:${POLICY_STORE_USER}:${PASS}" 2>/dev/null \
    || ipa group-mod usr_smb_adm_policystore \
        --desc="cifs://${ipa_host}:${POLICY_STORE_USER}:${PASS}" 2>/dev/null \
    || true
    log "Stored policystore credential in IPA group usr_smb_adm_policystore"

    unset PASS _desc
    kdestroy &>/dev/null || true
}

# ── Write smb.conf, set passwords, configure shares ──────────────────────────
_smb_set_pass "$LUKS_UPLOAD_USER"  "$LUKS_PASS"

# Write base smb.conf first, then append dynamic share stanzas.
_write_smb_conf
_setup_user_shares
_setup_policystore

# Reload smbd if it is already running (picks up new user shares without restart).
smbcontrol smbd reload-config 2>/dev/null || true

log "Samba setup complete. Shares: ansipa-luks-keys, ansipa-policystore ($POLICY_STORE_USER)"
