#!/bin/sh

# Simple script to convert a Postfix ldap_table(5) configuration file
# to a hash dump (stdout).

#  Copyright (C) 2020 Ganael LAPLANCHE - Centralesupelec
#
#  This program is free software; you can redistribute it and/or
#  modify it under the terms of the GNU General Public License
#  as published by the Free Software Foundation; either version 2
#  of the License, or (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307,
#  USA.

# XXX Limitations :
# - Only options appearing in the following config file extract are supported:
# ------------------------------------------
##Sample .cf input file (see ldap_table(5))
#server_host = ldaps://myhost.centralesupelec.fr:636
#search_base = dc=centralesupelec,dc=fr
#bind_dn = cn=xxx,dc=centralesupelec,dc=fr
#bind_pw = xxx
#query_filter = (&(mail=%s)(mailHost=*))
#result_attribute = mailHost
#result_format = smtp:[%s]
# ------------------------------------------
# Also:
# - No variable is supported in 'search_base'
# - Query (key) attribute is deduced from the first '%s' found in 'query_filter'
# - You cannot specify multiple attributes in 'result_attribute'
# - No other variable than '%s' is supported in 'result_format'

#### Configuration ####

#debug=1

# Miller tool, available from:
# https://github.com/johnkerl/miller
# See also: https://miller.readthedocs.io/en/latest/
_mlr_bin="/bin/mlr"

# OpenLDAP client tools, available from:
# https://www.openldap.org
_ldapsearch_bin="/bin/ldapsearch"

# Temporary password file used by ldapsearch(1)
_passwd_file="/tmp/.ldapsearch_pwd"

#### Helper functions ####

# Print help
# Returns : 0
usage () {
  echo "Usage : $0 <postfix_ldap_map_file>" 1>&2
}

# Echoes a string to STDERR
# Input : string to echo
# Returns : 0
warn () {
  [ -n "$1" ] && echo "$1" 1>&2
}

# Echoes $1, exits and returns 0
# Input : string to echo ($1)
# Returns : 0
end_ok () {
  [ -n "$1" ] && echo "$1"
  clean_pwd
  exit 0
}

# Echoes $1, exits and returns 1
# Input : string to echo ($1)
# Returns : 1
end_die () {
  [ -n "$1" ] && warn "$1"
  clean_pwd
  exit 1
}

# Get value from configuration file and key
# Input  : file ($1)
#          key ($2)
# Output : value (stdout)
get_value () {
  if [ -r "$1" ] && [ -n "$2" ]
  then
    grep "$2" "$1" | grep -v '^#' | head -n 1 | \
      cut -d '=' -f 2- | sed -E -e 's/^[[:space:]]+//' -e 's/[[:space:]]+$//'
  fi
}

# Remove temporary password file
clean_pwd () {
  rm -f "${_passwd_file}"
}

#### Program start ####

if [ -z "$1" ] || [ "$1" = "-h" ] || [ "$1" = "--help" ]
then
  usage
  end_die
fi

# Our config (map) file
_conf_file="$1"

# Check configuration file access
{ [ ! -r "${_conf_file}" ] || [ ! -f "${_conf_file}" ] ;} && \
  end_die "Unable to read configuration file: ${_conf_file}"

# Check for needed binaries
if [ ! -x "${_mlr_bin}" ] || [ ! -x "${_ldapsearch_bin}" ]
then
  end_die "You must have OpenLDAP client commands and miller installed before running this script"
fi

# Get mandatory options
# See: http://www.postfix.org/ldap_table.5.html
_server_host=$(get_value "${_conf_file}" server_host)
_search_base=$(get_value "${_conf_file}" search_base)
_bind_dn=$(get_value "${_conf_file}" bind_dn)
_bind_pw=$(get_value "${_conf_file}" bind_pw)
_query_filter=$(get_value "${_conf_file}" query_filter)
_result_attribute=$(get_value "${_conf_file}" result_attribute)
_result_format=$(get_value "${_conf_file}" result_format)

# Get query attribute.
# It is supposed to be the first matching attribute used to filter '%s'
_query_attribute=$(echo "${_query_filter}" | grep -Eo '[^(]+=%s' | cut -d '=' -f 1 | head -n 1)
# Globalize query filter. As we don't know what we are looking for, we want to
# fetch all possible matching values from LDAP.
_glob_query_filter=$(echo "${_query_filter}" | sed 's/%./*/g')
# Build our put filter to create a pseudo attribute (val) that will hold the
# result attribute. This is needed because if the query attribute and the result
# attribute are the same, ldapsearch will only return 1 attribute and miller
# will only generate a single-column output.
# By duplicating the results to a pseudo column and presenting it instead of
# our result attribute column, we ensure to always get two columns, even if
# query and result attributes are the same. The name of that pseudo-attribute
# is hardcoded here as 'val'.
_mlr_put_filter="\$val = \"$(echo ${_result_format} | sed "s/%s/\" . \${${_result_attribute}} . \"/g")\""

if [ -n "${debug}" ]
then
  warn "======== Debug ========"
  warn "Values extracted from: ${_conf_file}"
  warn "  server_host = ${_server_host}"
  warn "  search_base = ${_search_base}"
  warn "  bind_dn = ${_bind_dn}"
  warn "  bind_pw = ${_bind_pw}"
  warn "  query_filter = ${_query_filter}"
  warn "  result_attribute = ${_result_attribute}"
  warn "  result_format = ${_result_format}"
  warn "Values computed on our side:"
  warn "  query_attribute = ${_query_attribute}"
  warn "  glob_query_filter = ${_glob_query_filter}"
  warn "  mlr_put_filter = ${_mlr_put_filter}"
  warn "======================="
fi

# Trap SIGINT and install password file removal handler
trap 'clean_pwd' 2
# Generate temporary password file
{ :>"${_passwd_file}" && chmod 400 "${_passwd_file}" && echo -n "${_bind_pw}" > "${_passwd_file}" ;} || \
  end_die "Cannot generate temporary password file"

# Check we have everything needed
if [ -z "${_server_host}" ] || [ -z "${_search_base}" ] || \
  [ -z "${_bind_dn}" ] || [ -z "${_bind_pw}" ] || \
  [ -z "${_glob_query_filter}" ] || \
  [ -z "${_query_attribute}" ] || [ -z "${_result_attribute}" ]
then
  end_die "Missing values to perform LDAP query"
fi

# Perform search and conversion
echo "# Generated by postfix-ldap2hash.sh script -- DO NOT EDIT !"
echo "# Source map: ${_conf_file}"
ldapsearch -LLL -o ldif-wrap=no \
  -y "${_passwd_file}" -D "${_bind_dn}" -xH "${_server_host}" \
  -b "${_search_base}" "${_glob_query_filter}" \
  "${_query_attribute}" "${_result_attribute}" | \
    grep -v '^dn: ' | \
      mlr --x2c --headerless-csv-output --ofs '\t' --ips ': ' \
        cat \
        then unsparsify \
        then put -S "${_mlr_put_filter}" \
        then reorder -f "${_query_attribute}" \
        then cut -f "${_query_attribute},val" | \
          tr 'A-Z' 'a-z' | sort

[ $? != 0 ] && \
  end_die "Error querying LDAP"

end_ok
