#!/bin/bash
##
## OpenLDAP: Backup slapd databases
##
## SPDX-FileCopyrightText: 2012-2025 SATOH Fumiyasu @ OSSTech Corp., Japan
## SPDX-License-Identifier: GPL-3.0-or-later
##

set -u
set -o pipefail
umask 0027
renice 1 "$$" >/dev/null 2>&1 || :
ionice -c 3 -p "$$" >/dev/null 2>&1 || :

pwarn() {
  echo "$0: WARNING: $1" 1>&2
}

pdie()
{
  echo "$0: ERROR: $1" 1>&2
  exit "${2-1}"
}

ldifunwrap() {
  sed \
    -n \
    -e '1{ h; $!d; }' \
    -e '${ x; s/\n //g; p; }' \
    -e '/^ /{ H; d; }' \
    -e '/^ /!{ x; s/\n //g; p; }' \
  ;
}

## Check if slapd-config(5) (cn=config backend) is used or not
slapd_config_backend_p() {
  [[ -f $1/cn=config.ldif ]]
}

## ======================================================================

if [[ -z ${SLAPDBBACKUP_SETUIDGID-} && $(id -u) -eq 0 ]]; then
  export SLAPDBBACKUP_SETUIDGID=set
  user="${SLAPDBBACKUP_USER:-ldap}"
  if type setpriv >/dev/null 2>&1; then
    ## Old setpriv(1) can NOT take textual user and group names
    uid=$(id -u -- "$user")
    [[ -n $uid ]] || pdie "Failed to get the UID of a user: $user"
    gid=$(id -g -- "$user")
    [[ -n $gid ]] || pdie "Failed to get the primary GID of a user: $user"
    exec setpriv --reuid "$uid" --regid "$gid" --clear-groups -- "$0" "$@"
  elif type setuidgid >/dev/null 2>&1; then
    exec setuidgid -s "$user" "$0" "$@"
  else
    pdie 'No setpriv(1) or setuidgid(8) found'
  fi
  pdie "Failed to execute under the oridynary user privilege: $user" $?
fi

## Options
## ======================================================================

slapdbbacup_conf="${SLAPDBBACKUP_CONF:-/opt/osstech/etc/openldap/slapdbbackup.conf}"
slapcat_cmd="/opt/osstech/sbin/slapcat"
ldapsearch_cmd="/opt/osstech/bin/ldapsearch"
slapd_conf_file_default="/opt/osstech/etc/openldap/slapd.conf"
slapd_conf_dir_default="/opt/osstech/etc/openldap/slapd.d"
backup_dir="/opt/osstech/var/backup/ldap"
backup_prefix=""
backup_maxage_limit="1000000"
backup_maxage="30"
network_timeout="3600"
ldapi_uri="ldapi:///"

if [[ -f $slapdbbacup_conf ]]; then
  set -e
  # shellcheck disable=SC1090 # ShellCheck can't follow non-constant source
  . "$slapdbbacup_conf"
  set +e
fi

slapcat_cmd="${SLAPDBBACKUP_SLAPCAT_COMMAND:-$slapcat_cmd}"
ldapsearch_cmd="${SLAPDBBACKUP_LDAPSEARCH_COMMAND:-$ldapsearch_cmd}"
slapd_conf="${SLAPDBBACKUP_SLAPD_CONF:-}"
backup_dir="${SLAPDBBACKUP_BACKUP_DIR:-$backup_dir}"
backup_maxage="${SLAPDBBACKUP_BACKUP_MAXAGE:-$backup_maxage}"

if [[ $backup_maxage -gt $backup_maxage_limit ]]; then
  if [[ -t 2 ]]; then
    pwarn "Limit backup_maxage to backup_maxage_limit: $backup_maxage -> $backup_maxage_limit"
  fi
  backup_maxage="$backup_maxage_limit"
fi

if [[ -z $slapd_conf ]]; then
  if slapd_config_backend_p "$slapd_conf_dir_default"; then
    slapd_conf="$slapd_conf_dir_default"
  else
    slapd_conf="$slapd_conf_file_default"
  fi
fi

## ======================================================================

## Avoid symlink attack via a temporary filename
set -o noclobber

{
  if slapd_config_backend_p "$slapd_conf"; then
    echo "config cn=config"
  fi

  {
    if slapd_config_backend_p "$slapd_conf"; then
      (
        shopt -s nullglob
        cat \
          "$slapd_conf"/cn=config/olcDatabase=\{*\}[bhm]db.ldif \
          "$slapd_conf"/cn=config/olcDatabase=\{*\}wt.ldif \
        </dev/null \
        ;
      ) \
      |ldifunwrap \
      |sed -n \
        -e 's/^dn: .*}/database /p' \
        -e 's/^olcSuffix: /suffix /p' \
      ;
    else
      sed -e 's/"//g' -e 's/ *, */,/g' "$slapd_conf"
    fi
  } \
  |tr -d '\r' \
  |while read -r param value; do
    case "$param" in
    database)
      backend="$value"
      ;;
    suffix)
      if [[ $backend == [bhm]db || $backend == wt ]]; then
        echo "$backend $value"
      fi
      ;;
    esac
  done
} \
|while read -r backend suffix; do
  ldif="$backup_dir/$backup_prefix$suffix.ldif.gz"
  ldif_new="$ldif.$$.tmp"

  rm -f -- "$ldif_new" || exit $?

  (
    ## WiredTiger does not support slapcat online
    if [[ $backend == wt ]] &&
      "$ldapsearch_cmd" -H "$ldapi_uri" -QY external -b '' -s base >&/dev/null
    then
      "$ldapsearch_cmd" \
        -H "$ldapi_uri" \
        -QY external \
        -b "$suffix" \
        -o ldif-wrap=no \
        -o nettimeout="$network_timeout" \
        -l none \
        -z none \
        -LLL \
        '*' + \
      |sed \
        -e '/^entryDN:/d' \
        -e '/^subschemaSubentry:/d' \
        -e '/^hasSubordinates:/d' \
        -e '/^numSubordinates:/d' \
      ;
    else
      "$slapcat_cmd" -b "$suffix"
    fi
  ) |gzip -1 >"$ldif_new"
  backup_rc="$?"
  if [[ $backup_rc -ne 0 ]]; then
    rm -f -- "$ldif_new"
    pdie "Backup command failed: $suffix: Exit status $backup_rc"
  fi

  if [[ -f $ldif ]]; then
    backup_age="$backup_maxage"
    rm -f -- "$ldif.$backup_age" || exit $?
    for ((; backup_age > 0; backup_age--)); do
      ((backup_age_prev = backup_age - 1))
      if [[ -f $ldif.$backup_age_prev ]]; then
        mv -- "$ldif.$backup_age_prev" "$ldif.$backup_age" || exit $?
      fi
    done

    mv -- "$ldif" "$ldif.1" || exit $?
  fi

  mv -- "$ldif_new" "$ldif" || exit $?
done

exit $?
