#!/bin/bash
# -*- indent-tabs-mode: t; tab-width: 4; -*-

#
# LDAP server (slapd) for user authentication and directory services
#
source setup/functions.sh # load our functions
source setup/functions-ldap.sh # load our ldap-specific functions
source /etc/mailinabox.conf # load global vars

ORGANIZATION="Mail-In-A-Box"
LDAP_DOMAIN="mailinabox"
LDAP_BASE="dc=mailinabox"
LDAP_SERVICES_BASE="ou=Services,$LDAP_BASE"
LDAP_CONFIG_BASE="ou=Config,$LDAP_BASE"
LDAP_DOMAINS_BASE="ou=domains,$LDAP_CONFIG_BASE"
LDAP_PERMITTED_SENDERS_BASE="ou=permitted-senders,$LDAP_CONFIG_BASE"
LDAP_USERS_BASE="ou=Users,${LDAP_BASE}"
LDAP_ALIASES_BASE="ou=aliases,${LDAP_USERS_BASE}"
LDAP_ADMIN_DN="cn=admin,dc=mailinabox"

STORAGE_LDAP_ROOT="$STORAGE_ROOT/ldap"
MIAB_SLAPD_DB_DIR="$STORAGE_LDAP_ROOT/db"
MIAB_SLAPD_CONF="$STORAGE_LDAP_ROOT/slapd.d"
MIAB_INTERNAL_CONF_FILE="$STORAGE_LDAP_ROOT/miab_ldap.conf"

SERVICE_ACCOUNTS=(LDAP_DOVECOT LDAP_POSTFIX LDAP_WEBMAIL LDAP_MANAGEMENT LDAP_NEXTCLOUD)


#
# Helper functions
#

ldap_debug_flag() {
	[ ${verbose:-0} -gt 1 ] && echo "-d 1"
}

wait_slapd_start() {
	# Wait for slapd to start...
	say_verbose -n "Waiting for slapd to start"
	local let elapsed=0
	until nc -z -w 4 127.0.0.1 389
	do
		[ $elapsed -gt 30 ] && die "Giving up waiting for slapd to start!"
		[ $elapsed -gt 0 ] && say_verbose -n "...${elapsed}"
		sleep 2
		let elapsed+=2
	done
	say_verbose "...ok"
}

_add_if_missing() {
	local var="$1"
	local val="$2"
	local conf="$MIAB_INTERNAL_CONF_FILE"
	if [ $(grep -c "^${var}=" "$conf") -eq 0 ]; then
		echo "${var}=\"${val}\"" >> "$conf"
	fi
}

create_miab_conf() {
	# create (if non-existing) or load (existing) ldap/miab_ldap.conf
	if [ ! -e "$MIAB_INTERNAL_CONF_FILE" ]; then
		say_verbose "Generating a new $MIAB_INTERNAL_CONF_FILE"
		mkdir -p "$(dirname $MIAB_INTERNAL_CONF_FILE)"
		touch "$MIAB_INTERNAL_CONF_FILE"
	fi

	# ensure all required values exist, and if not set to default values
	_add_if_missing LDAP_SERVER 127.0.0.1
	_add_if_missing LDAP_SERVER_PORT 389
	_add_if_missing LDAP_SERVER_STARTTLS no
	_add_if_missing LDAP_SERVER_TLS no
	_add_if_missing LDAP_URL ldap://127.0.0.1/
	_add_if_missing LDAP_BASE "${LDAP_BASE}"
	_add_if_missing LDAP_SERVICES_BASE "${LDAP_SERVICES_BASE}"
	_add_if_missing LDAP_CONFIG_BASE "${LDAP_CONFIG_BASE}"
	_add_if_missing LDAP_DOMAINS_BASE "${LDAP_DOMAINS_BASE}"
	_add_if_missing LDAP_PERMITTED_SENDERS_BASE "${LDAP_PERMITTED_SENDERS_BASE}"
	_add_if_missing LDAP_USERS_BASE "${LDAP_USERS_BASE}"
	_add_if_missing LDAP_ALIASES_BASE "${LDAP_ALIASES_BASE}"
	_add_if_missing LDAP_ADMIN_DN "${LDAP_ADMIN_DN}"
	_add_if_missing LDAP_ADMIN_PASSWORD "$(generate_password 64)"

	# add service account credentials
	local prefix
	for prefix in ${SERVICE_ACCOUNTS[*]}
	do
		local cn=$(awk -F_ '{print tolower($2)}' <<< $prefix)
		_add_if_missing "${prefix}_DN" "cn=$cn,$LDAP_SERVICES_BASE"
		_add_if_missing "${prefix}_PASSWORD" "$(generate_password 64)"
	done
	
	chmod 0640 "$MIAB_INTERNAL_CONF_FILE"
	. "$MIAB_INTERNAL_CONF_FILE"
}


create_directory_containers() {
	# create organizationUnit containers
	local basedn
	for basedn in "$LDAP_SERVICES_BASE" "$LDAP_CONFIG_BASE" "$LDAP_DOMAINS_BASE" "$LDAP_PERMITTED_SENDERS_BASE" "$LDAP_USERS_BASE" "$LDAP_ALIASES_BASE"; do
		# add ou container
		get_attribute "$basedn" "objectClass=*" "ou" base
		if [ -z "$ATTR_DN" ]; then
			say_verbose "Adding $basedn"
			ldapadd -H ldap://127.0.0.1/ -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >/dev/null <<EOF
dn: $basedn
objectClass: organizationalUnit
ou: $(awk -F'[=,]' '{print $2}' <<< $basedn)
EOF
		fi
	done
}

create_service_accounts() {
	# create service accounts. service accounts have special access
	# rights, generally read-only to users, aliases, and configuration
	# subtrees (see apply_access_control)
	
	local prefix dn pass
	for prefix in ${SERVICE_ACCOUNTS[*]}
	do
		eval "dn=\$${prefix}_DN"
		eval "pass=\$${prefix}_PASSWORD"
		get_attribute "$dn" "objectClass=*" "cn" base
		say_debug "SERVICE_ACCOUNT $dn"
		if [ -z "$ATTR_DN" ]; then
			local cn=$(awk -F'[=,]' '{print $2}' <<< $dn)
			say_verbose "Adding service account: $dn"
			ldapadd -H ldap://127.0.0.1/ -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD" >/dev/null <<EOF
dn: $dn
objectClass: simpleSecurityObject
objectClass: organizationalRole
cn: $cn
description: ${cn} service account
userPassword: $(slappasswd_hash "$pass")
EOF
		fi
	done
	
}


install_system_packages() {
	# install required deb packages, generate admin credentials
	# and apply them to the installation
	create_miab_conf
	
	# Set installation defaults to avoid interactive dialogs. See
	# /var/lib/dpkg/info/slapd.templates for a list of what can be set
	debconf-set-selections <<EOF
slapd shared/organization string ${ORGANIZATION}
slapd slapd/domain string ${LDAP_DOMAIN}
slapd slapd/password1 password ${LDAP_ADMIN_PASSWORD}
slapd slapd/password2 password ${LDAP_ADMIN_PASSWORD}
EOF
	
	# Install packages
	say "Installing OpenLDAP server..."
	apt_install slapd ldap-utils python3-ldap3 python3-ldif3 ca-certificates xz-utils

	# If slapd was not installed by us, the selections above did
	# nothing.  To check this we see if SLAPD_CONF in
	# /etc/default/slapd is empty and that the olc does not have our
	# database. We could do 2 things in this situation:
	#    1. ask the user for the current admin password and add our domain
	#    2. reconfigure and wipe out the current database
	# we do #2 ....
	local SLAPD_CONF=""
	eval "$(grep ^SLAPD_CONF= /etc/default/slapd)"
	local cursuffix="$(slapcat -s "cn=config" | grep "^olcSuffix: ")"
	if [ -z "$SLAPD_CONF" ] &&
		   ! grep "$LDAP_DOMAIN" <<<"$cursuffix" >/dev/null
	then
		mkdir -p /var/backup
		local tgz="/var/backup/slapd-$(date +%Y%m%d-%H%M%S).tgz"
		(cd /var/lib/ldap; tar czf "$tgz" .)
		chmod 600 "$tgz"
		rm /var/lib/ldap/*
		say "Reininstalling slapd! - existing database saved in $tgz"
		dpkg-reconfigure --frontend=noninteractive slapd
	fi

	# Clear passwords out of debconf
	debconf-set-selections <<EOF
slapd slapd/password1 password
slapd slapd/password2 password
EOF

	# Ensure slapd is running
	systemctl start slapd && wait_slapd_start

	# Change the admin password hash format in the server from slapd's
	# default {SSHA} to SHA-512 {CRYPT} with 16 characters of salt
	get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "olcRootPW"
	if [ ${#ATTR_VALUE[*]} -eq 1 -a $(grep -c "{SSHA}" <<< "$ATTR_VALUE") -eq 1 ]; then
		say_verbose "Updating root hash to SHA512-CRYPT"
		local hash=$(slappasswd_hash "$LDAP_ADMIN_PASSWORD")
		ldapmodify -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
dn: $ATTR_DN
replace: olcRootPW
olcRootPW: $hash
EOF
		say_verbose "Updating admin hash to SHA512-CRYPT"
		ldapmodify -H ldap://127.0.0.1/ -x -D "$LDAP_ADMIN_DN" -w "$LDAP_ADMIN_PASSWORD"  >/dev/null <<EOF
dn: $LDAP_ADMIN_DN
replace: userPassword
userPassword: $hash
EOF
	fi
}

relocate_slapd_data() {
	#
	# Move current ldap databases to user-data (eg. new install). A
	# new slapd installation places the ldap configuration database in
	# /etc/ldap/slapd.d and schema database in /var/lib/ldap. So that
	# backups include the ldap database, move everything to user-data.
	#
	# On entry:
	#	SLAPD_CONF must point to the current slapd.d directory
	#	   (see /etc/default/slapd)
	#	Global variables as defined above must be set
	#	The slapd service must be running
	#
	# On success:
	#	Config and data will be relocated to the new locations
	#
	say_verbose "Relocate ldap databases from current locations to user-data"

	# Get the current database location from olc
	get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "olcDbDirectory"
	local DN="$ATTR_DN"
	local DB_DIR="$ATTR_VALUE"
	if [ -z "$DN" ]; then
		say_verbose ""
		say_verbose "ACK! ${LDAP_BASE} does not exist in the LDAP server!!!"
		say_verbose "Something is amiss!!!!!"
		say_verbose "... to ensure no data is lost, please manually fix the problem"
		say_verbose "	 by running 'sudo dpkg-reconfigure slapd'"
		say_verbose ""
		say_verbose "CAUTION: running dbpg-reconfigure will remove ALL data"
		say_verbose "for the existing domain!"
		say_verbose ""
		die "Unable to continue!"
	fi

	# Exit if destination directories are non-empty
	[ ! -z "$(ls -A $MIAB_SLAPD_CONF)" ] && die "Cannot relocate system LDAP because $MIAB_SLAPD_CONF is not empty!"
	[ ! -z "$(ls -A $MIAB_SLAPD_DB_DIR)" ] && die "Cannot relocate system LDAP because $MIAB_SLAPD_DB_DIR is not empty!"

	# Stop slapd
	say_verbose ""
	say_verbose "Relocating ldap databases:"
	say_verbose "	from: "
	say_verbose "	   CONF='${SLAPD_CONF}'"
	say_verbose "		DB='${DB_DIR}'"
	say_verbose "	to:"
	say_verbose "	   CONF=${MIAB_SLAPD_CONF}"
	say_verbose "		 DB=${MIAB_SLAPD_DB_DIR}"	
	say_verbose ""
	say_verbose "Stopping slapd"
	systemctl stop slapd || die "Could not stop slapd"
	
	# Modify the path to dc=mailinabox's database directory
	say_verbose "Dump config database"
	local TMP="/tmp/miab_relocate_ldap.ldif"
	slapcat -F "${SLAPD_CONF}" -l "$TMP" -n 0 || die "slapcat failed"
	awk -e "/olcDbDirectory:/ {print \$1 \"$MIAB_SLAPD_DB_DIR\"} !/^olcDbDirectory:/ { print \$0}" $TMP > $TMP.2
	rm -f "$TMP"

	# Copy the existing database files
	say_verbose "Copy database files ($DB_DIR => $MIAB_SLAPD_DB_DIR)"
	cp -p "${DB_DIR}"/* "${MIAB_SLAPD_DB_DIR}" || die "Could not copy files '${DB_DIR}/*' to '${MIAB_SLAPD_DB_DIR}'"

	# Re-create the config
	say_verbose "Create new slapd config"
	local xargs=()
	[ ${verbose:-0} -gt 0 ] && xargs+=(-d 10 -v)
	slapadd -F "${MIAB_SLAPD_CONF}" ${xargs[@]} -n 0 -l "$TMP.2" 2>/dev/null || die "slapadd failed!"
	chown -R openldap:openldap "${MIAB_SLAPD_CONF}"
	rm -f "$TMP.2"

	# Remove the old database files
	rm -f "${DB_DIR}/*"
}


schema_to_ldif() {
	# Convert a .schema file to ldif. This function follows the
	# conversion instructions found in /etc/ldap/schema/openldap.ldif
	local schema="$1"  # path or url to schema
	local ldif="$2"	   # destination file - will be overwritten
	local cn="$3"	   # schema common name, eg "postfix"
	local cat='cat'
	if [ ! -e "$schema" ]; then
		if [ -e "conf/schema/$(basename $schema)" ]; then
			schema="conf/schema/$(basename $schema)"
		else
			cat="curl -s"
		fi
	fi
	
	cat >"$ldif" <<EOF
dn: cn=$cn,cn=schema,cn=config
objectClass: olcSchemaConfig
cn: $cn
EOF

	$cat "$schema" \
		| sed s/attributeType/olcAttributeTypes:/ig \
		| sed s/objectClass/olcObjectClasses:/ig \
		| sed s/objectIdentifier/olcObjectIdentifier:/ig \
		| sed 's/\t/  /g' \
		| sed 's/^\s*$/#/g' >> "$ldif"
}

change_core_mail_syntax() {
	# output the ldif to change mail to utf8 from ia5 in the core schema
	get_attribute "cn=schema,cn=config" "(cn={0}core)" "olcAttributeTypes"
	case "${ATTR_VALUE[48]}" in
		*\'mail\'*caseIgnoreIA5Match* )
			newval=$(sed 's/SYNTAX[ \t][^ ]*/SYNTAX 1.3.6.1.4.1.1466.115.121.1.15/g' <<<"${ATTR_VALUE[48]}" |
					 sed 's/caseIgnoreIA5Match/caseIgnoreMatch/g' |
					 sed 's/caseIgnoreIA5SubstringsMatch/caseIgnoreSubstringsMatch/g')
			cat <<EOF
dn: cn={0}core,cn=schema,cn=config
changetype: modify
delete: olcAttributeTypes
olcAttributeTypes: ${ATTR_VALUE[48]}
-
add: olcAttributeTypes
olcAttributeTypes: $newval
EOF
			;;
	esac
}


add_schemas() {
	# Add necessary schema's for MiaB operaion		 
	#
	# Note: the postfix schema originally came from the ldapadmin
	# project (GPL)(*), but has been modified to support the needs of
	# this project.
	#	see: http://ldapadmin.org
	#		 http://ldapadmin.org/docs/postfix.schema
	#		 http://www.postfix.org/LDAP_README.html
	for cn in "postfix" "mfa-totp" "namedProperties"; do
		schema="$cn.schema"
		get_attribute "cn=schema,cn=config" "(&(cn={*}$cn)(objectClass=olcSchemaConfig))" "cn"
		if [ -z "$ATTR_DN" ]; then
			local ldif="/tmp/$cn.$$.ldif"
			schema_to_ldif "$schema" "$ldif" "$cn"
			if [ "$cn" = "postfix" ]; then
				local ldif2="/tmp/$cn.$$-2.ldif"
				change_core_mail_syntax >"$ldif2"
				echo "" >>"$ldif2"
				sed 's/^dn: cn=postfix,\(.*\)$/dn: cn=postfix,\1\nchangetype: add/g' "$ldif" >> "$ldif2"
				rm "$ldif"
				ldif="$ldif2"
			fi
			say_verbose "Adding '$cn' schema"
			[ ${verbose:-0} -gt 1 ] && cat "$ldif"
			ldapadd -Q -Y EXTERNAL -H ldapi:/// -f "$ldif" >/dev/null
			rm -f "$ldif"
		fi
	done	
}


modify_global_config() {
	#
	# Set ldap configuration attributes:
	#  IdleTimeout: seconds to wait before forcibly closing idle connections
	#  LogLevel: logging levels - see OpenLDAP docs
	#  TLS configuration
	#  Disable anonymous binds
	#
	say_verbose "Setting global ldap configuration"

	# TLS requirements:
	#
	# The 'openldap' user must have read access to the TLS private key
	# and certificate (file system permissions and apparmor). If
	# access is not configured properly, slapd retuns error code 80
	# and won't apply the TLS configuration, or won't start.
	#
	# Openldap TLS will not operate with a self-signed server
	# certificate! The server will always log "unable to get TLS
	# client DN, error=49." Ensure the certificate is signed by
	# a certification authority.
	#
	# The list of trusted CA certificates must include the CA that
	# signed the server's certificate!
	#
	# For the olcCiperSuite setting, see:
	# https://www.gnutls.org/manual/gnutls.html#Priority-Strings
	#

	ldapmodify $(ldap_debug_flag) -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
dn: cn=config
##
## timeouts (1800=30 minutes) and logging
##
replace: olcIdleTimeout
olcIdleTimeout: 1800
-
replace: olcLogLevel
olcLogLevel: config stats shell
#olcLogLevel: config stats shell filter ACL
-
##
## TLS
##
replace: olcTLSCACertificateFile
olcTLSCACertificateFile: /etc/ssl/certs/ca-certificates.crt
-
replace: olcTLSCertificateFile
olcTLSCertificateFile: $STORAGE_ROOT/ssl/ssl_certificate.pem
-
replace: olcTLSCertificateKeyFile
olcTLSCertificateKeyFile: $STORAGE_ROOT/ssl/ssl_private_key.pem
-
replace: olcTLSDHParamFile
olcTLSDHParamFile: $STORAGE_ROOT/ssl/dh2048.pem
-
replace: olcTLSCipherSuite
olcTLSCipherSuite: PFS
-
replace: olcTLSVerifyClient
olcTLSVerifyClient: never
-
##
## Password policies - use SHA512 with 16 characters of salt
##
replace: olcPasswordHash
olcPasswordHash: {CRYPT}
-
replace: olcPasswordCryptSaltFormat
olcPasswordCryptSaltFormat: \$6\$%.16s
-
##
## Disable anonymous binds
##
replace: olcDisallows
olcDisallows: bind_anon
-
replace: olcRequires
olcRequires: authc

dn: olcDatabase={-1}frontend,cn=config
replace: olcRequires
olcRequires: authc
EOF
}


add_overlays() {
	# Apply slapd overlays - apply the commonly used member-of overlay
	# now because adding it later is harder.
	
	# Get the config dn for the database
	get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "dn"
	[ -z "$ATTR_DN" ] &&
		die "No config found for olcSuffix=$LDAP_BASE in cn=config!"
	local cdn="$ATTR_DN"

	# Add member-of overlay (man 5 slapo-memberof)
	get_attribute "cn=module{0},cn=config" "(olcModuleLoad=memberof.la)" "dn" base
	if [ -z "$ATTR_DN" ]; then
		say_verbose "Adding memberof overlay module"
		ldapmodify -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
dn: cn=module{0},cn=config
add: olcModuleLoad
olcModuleLoad: memberof.la
EOF
	fi
	
	get_attribute "$cdn" "(olcOverlay=memberof)" "olcOverlay"
	if [ -z "$ATTR_DN" ]; then
		say_verbose "Adding memberof overlay to $LDAP_BASE"
		ldapadd -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
dn: olcOverlay=memberof,$cdn
objectClass: olcOverlayConfig
objectClass: olcMemberOf
olcOverlay: memberof
#olcMemberOfGroupOC: mailGroup
olcMemberOfRefint: TRUE
EOF
	fi
}


add_indexes() {
	# Index mail-related attributes
	
	# Get the config dn for the database
	get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "dn"
	[ -z "$ATTR_DN" ] &&
		die "No config found for olcSuffix=$LDAP_BASE in cn=config!"
	local cdn="$ATTR_DN"

	# Add the indexes
	get_attribute "$cdn" "(objectClass=*)" "olcDbIndex" base
	local attr
	for attr in mail maildrop mailaccess dc dcIntl mailMember; do
		local type="eq" atype="" aindex=""
		[ "$attr" == "mail" ] && type="eq,sub"

		# find the existing index for the attribute
		local item
		for item in "${ATTR_VALUE[@]}"; do
			local split=($item)  # eg "mail eq"
			if [ "${split[0]}" == "$attr" ]; then
				aindex="$item"
				atype="${split[1]}"
				break
			fi
		done

		# if desired index type (eg "eq") is equal to actual type,
		# continue, no change
		[ "$type" == "$atype" ] && continue

		# replace it or add a new index if not present
		if [ ! -z "$atype" ]; then
			say_verbose "Replace index $attr ($atype -> $type)"
			ldapmodify -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
dn: $cdn
delete: olcDbIndex
olcDbIndex: $aindex
-
add: olcDbIndex
olcDbIndex: $attr $type
EOF
		else
			say_verbose "Add index for attribute $attr ($type)"
			ldapmodify -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
dn: $cdn
add: olcDbIndex
olcDbIndex: $attr $type
EOF
		fi
	done
}


apply_access_control() {
	# Apply access control to the mail-in-a-box databse.
	#
	# Permission restrictions:
	#	service accounts (except management):
	#	   can bind but not change passwords, including their own
	#	   can read all attributes of all users but not userPassword,
	#         totpSecret, totpMruToken, totpMruTokenTime, or totpLabel
	#	   can read config subtree (permitted-senders, domains)
	#	   no access to services subtree, except their own dn
	#	management service account:
	#	   can read and change password, shadowLastChange, and totpSecret
	#	   all other service account permissions are the same
	#	users:
	#	   can bind and change their own password
	#	   can read and change their own shadowLastChange
	#      cannot read or modify totpSecret, totpMruToken, totpMruTokenTime, totpLabel
	#	   can read attributess of other users except mailaccess, totpSecret, totpMruToken, totpMruTokenTime, totpLabel
	#	   no access to config subtree
	#	   no access to services subtree
	#

	# Get the config dn for the database
	get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "dn"
	[ -z "$ATTR_DN" ] &&
		die "No config found for olcSuffix=$LDAP_BASE in cn=config!"
	local cdn="$ATTR_DN"

	say_verbose "Setting database permissions"
	ldapmodify -Q -Y EXTERNAL -H ldapi:/// >/dev/null <<EOF
dn: $cdn
replace: olcAccess
# the next line is for nextcloud to be able to change user account
# passwords. remove it when nextcloud server issue #18406 is fixed
olcAccess: to dn.subtree="${LDAP_USERS_BASE}" attrs=userPassword
  by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
  by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
  by dn.exact="cn=nextcloud,${LDAP_SERVICES_BASE}" write
  by self =wx
  by anonymous auth
  by * none
olcAccess: to attrs=userPassword
  by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
  by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
  by dn.subtree="${LDAP_SERVICES_BASE}" none
  by self =wx
  by anonymous auth
  by * none
olcAccess: to attrs=totpSecret,totpMruToken,totpMruTokenTime,totpLabel
  by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
  by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
  by * none
olcAccess: to attrs=shadowLastChange
  by self write
  by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
  by dn.subtree="${LDAP_SERVICES_BASE}" read
  by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
  by * none
olcAccess: to attrs=mailaccess
  by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
  by dn.subtree="${LDAP_SERVICES_BASE}" read
  by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
  by * none
olcAccess: to dn.subtree="${LDAP_CONFIG_BASE}"
  by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
  by dn.subtree="${LDAP_SERVICES_BASE}" read
  by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
  by * none
olcAccess: to dn.subtree="${LDAP_SERVICES_BASE}"
  by self read
  by dn.exact="gidNumber=0+uidNumber=0,cn=peercred,cn=external,cn=auth" read
  by * none
olcAccess: to dn.subtree="${LDAP_USERS_BASE}"
  by dn.exact="cn=management,${LDAP_SERVICES_BASE}" write
  by * read
olcAccess: to *
  by * read
EOF
}



update_apparmor() {
	# Update slapd's access rights under AppArmor so that it has
	# access to database files in the user-data location
	cat > /etc/apparmor.d/local/usr.sbin.slapd <<EOF
	# database directories
	$MIAB_SLAPD_CONF/** rw,
	$MIAB_SLAPD_DB_DIR/ r,
	$MIAB_SLAPD_DB_DIR/** rwk,
	$MIAB_SLAPD_DB_DIR/alock kw,

	# certificates and keys
	$STORAGE_ROOT/ssl/* r,
EOF
	chmod 0644 /etc/apparmor.d/local/usr.sbin.slapd

	# Load settings into the kernel only if AppArmor is enabled
	if aa-status --enabled; then
		apparmor_parser -r /etc/apparmor.d/usr.sbin.slapd
	fi
}


#
# Process command line arguments -- these are here for debugging and
# testing purposes
#
process_cmdline() {
	[ -e "$MIAB_INTERNAL_CONF_FILE" ] && . "$MIAB_INTERNAL_CONF_FILE"
	
	if [ "$1" == "-d" ]; then
		# Start slapd in interactive/debug mode
		echo "!! SERVER DEBUG MODE !!"
		echo "Stopping slapd"
		systemctl stop slapd
		. /etc/default/slapd
		echo "Listening on $SLAPD_SERVICES..."
		/usr/sbin/slapd -h "$SLAPD_SERVICES" -g openldap -u openldap -F $MIAB_SLAPD_CONF -d ${2:-1}
		exit 0
		
	elif [ "$1" == "-config" ]; then
		# Apply a certain configuration
		if [ "$2" == "server" ]; then
			add_schemas
			modify_global_config
			add_overlays
			add_indexes
			apply_access_control
		elif [ "$2" == "apparmor" ]; then
			update_apparmor
		else
			echo "Invalid: '$2'. Only 'server' and 'apparmor' supported"
			exit 1
		fi
		exit 0

	elif [ "$1" == "-search" ]; then
		# search for email addresses, distinguished names and general
		# ldap filters. Args:
		#    search filter [optional]
		#    base dn [optional]
		#    remaining args are attributes to output [optional]
		shift
		if [ $# -eq 0 ]; then
			debug_search "(objectclass=*)"
		else
			debug_search "$@"
		fi
		exit 0

	elif [ "$1" == "-schema-to-ldif" ]; then
		cn="$2"
		output="${3:-/dev/stdout}"
		schema="$cn.schema"
		schema_to_ldif "$schema" "$output" "$cn"
		exit 0

	elif [ "$1" == "-dumpdb" ]; then
		# Dump (to stdout) select ldap data and configuration
		local s=${2:-all}
		local hide_attrs="(structuralObjectClass|entryUUID|creatorsName|createTimestamp|entryCSN|modifiersName|modifyTimestamp)"
		local slapcat_args=(-F "$MIAB_SLAPD_CONF" -o ldif-wrap=no)
		[ ${verbose:-0} -gt 0 ] && hide_attrs="(_____NEVERMATCHES)"
		
		if [ "$s" == "all" ]; then
			echo ""
			echo '--------------------------------'
			slapcat ${slapcat_args[@]} -s "$LDAP_BASE" | grep -Ev "^$hide_attrs:"
		fi
		if [ "$s" == "all" -o "$s" == "config" ]; then
			echo ""
			echo '--------------------------------'
			cat "$MIAB_SLAPD_CONF/cn=config.ldif" | grep -Ev "^$hide_attrs:"
			get_attribute "cn=config" "olcSuffix=${LDAP_BASE}" "dn"
			echo ""
			slapcat ${slapcat_args[@]} -s "$ATTR_DN" | grep -Ev "^$hide_attrs:"
		fi
		if [ "$s" == "all" -o "$s" == "schema" ]; then
			echo ""
			echo '--------------------------------'
			slapcat ${slapcat_args[@]} -s "cn=schema,cn=config" | grep -Ev "^$hide_attrs:"
		fi
		if [ "$s" == "all" -o "$s" == "frontend" ]; then
			echo ""
			echo '--------------------------------'
			cat "$MIAB_SLAPD_CONF/cn=config/olcDatabase={-1}frontend.ldif" | grep -Ev "^$hide_attrs:"
		fi
		if [ "$s" == "all" -o "$s" == "module" ]; then
			echo ""
			cat "$MIAB_SLAPD_CONF/cn=config/cn=module{0}.ldif" | grep -Ev "^$hide_attrs:"
		fi
		if [ "$s" == "users" ]; then
			echo ""
			echo '--------------------------------'
			debug_search "(objectClass=mailUser)" "$LDAP_USERS_BASE"
		fi
		if [ "$s" == "aliases" ]; then
			echo ""
			echo '--------------------------------'
			local attrs=(mail member mailRoutingAddress mailMember namedProperty)
			[ ${verbose:-0} -gt 0 ] && attrs=()
			debug_search "(objectClass=mailGroup)" "$LDAP_ALIASES_BASE" ${attrs[@]}
		fi
		if [ "$s" == "permitted-senders" -o "$s" == "ps" ]; then
			echo ""
			echo '--------------------------------'
			local attrs=(mail member mailRoutingAddress rfc822MailMember)
			[ ${verbose:-0} -gt 0 ] && attrs=()
			debug_search "(objectClass=mailGroup)" "$LDAP_PERMITTED_SENDERS_BASE" ${attrs[@]}
		fi
		if [ "$s" == "domains" ]; then
			echo ""
			echo '--------------------------------'
			debug_search "(objectClass=domain)" "$LDAP_DOMAINS_BASE"
		fi
		exit 0

	elif [ "$1" == "-reset" ]; then
		#
		# Delete and remove OpenLDAP
		#
		echo ""
		echo "!!!!!			   WARNING!				  !!!!!"
		echo "!!!!!		 OPENLDAP WILL BE REMOVED	  !!!!!"
		echo "!!!!!	 ALL LDAP DATA WILL BE DESTROYED  !!!!!"
		echo ""
		echo -n "Type 'YES' to continue: "
		read ans
		if [ "$ans" != "YES" ]; then
			echo "Aborted"
			exit 1
		fi
		if [ -x /usr/sbin/slapd ]; then
			apt-get remove --purge -y slapd
			apt-get -y autoremove
			apt-get autoclean
		fi
		rm -rf "$STORAGE_LDAP_ROOT"
		rm -rf "/etc/ldap/slapd.d"
		rm -rf "/var/lib/ldap"
		rm -f "/etc/default/slapd"
		echo "Done"
		exit 0
		
	elif [ ! -z "$1" ]; then
		echo "Invalid command line argument '$1'"
		exit 1
	fi
}

[ $# -gt 0 ] && process_cmdline $@



####
#### MAIN SCRIPT CODE STARTS HERE...
####

# Run apt installs
install_system_packages

# Update the ldap schema
add_schemas

#
# Create user-data/ldap directory structure:
#	db/			  - holds slapd database for "dc=mailinabox"
#	slapd.d/	  - holds slapd configuration
#	miab_ldap.conf	- holds values for other subsystems like postfix, management, etc
#
for d in "$STORAGE_LDAP_ROOT" "$MIAB_SLAPD_DB_DIR" "$MIAB_SLAPD_CONF"; do
	mkdir -p "$d"
	chown openldap:openldap "$d"
	chmod 755 "$d"
done

# Ensure openldap can access the tls/ssl private key file
usermod -a -G ssl-cert openldap

# Ensure slapd can interact with the mailinabox database and config
update_apparmor

# Load slapd's init script startup options
. /etc/default/slapd
if [ -z "$SLAPD_CONF" ]; then
	# when not defined, slapd uses its compiled-in default directory
	SLAPD_CONF="/etc/ldap/slapd.d"
fi


# Relocate slapd databases to user-data, which is needed after a new
# installation, we're restoring from backup, or STORAGE_ROOT changes
if [ "$SLAPD_CONF" != "$MIAB_SLAPD_CONF" ]; then
	if [ -z "$(ls -A $MIAB_SLAPD_CONF)" ]; then
		# Empty destination - relocate databases
		relocate_slapd_data
	else
		# Non-empty destination - use the backup data as-is
		systemctl stop slapd
	fi
	# Tell the system startup script to use our config database
	tools/editconf.py /etc/default/slapd \
					  "SLAPD_CONF=$MIAB_SLAPD_CONF"
	systemctl start slapd || die "slapd woudn't start! try running $0 -d"
	wait_slapd_start
fi


# Configure syslog
mkdir -p /var/log/ldap
chmod 750 /var/log/ldap
chown syslog:adm /var/log/ldap
cp conf/slapd-logging.conf /etc/rsyslog.d/20-slapd.conf
chmod 644 /etc/rsyslog.d/20-slapd.conf
restart_service syslog

# Add log rotation
cat > /etc/logrotate.d/slapd <<EOF;
/var/log/ldap/slapd.log {
	weekly
	missingok
	rotate 52
	compress
	delaycompress
	notifempty
}
EOF

# Modify olc server config like TLS
# Skip this step if no ca_certificate.pem exists - this indicates
# that the system hasn't yet been migrated from sqlite
if [ -e "$STORAGE_ROOT/ssl/ca_certificate.pem" ]; then
	modify_global_config
else
	say_debug "Not enabling TLS at this time - ca_certificate hasn't been generated yet"
fi

# Add overlays and ensure mail-related attributes are indexed
add_overlays
add_indexes

# Lock down access
apply_access_control

# Create general db structure
create_directory_containers

# Create service accounts for dovecot, postfix, roundcube, etc
create_service_accounts

# Update where slapd listens for incoming requests
tools/editconf.py /etc/default/slapd \
				  "SLAPD_SERVICES=\"ldap://127.0.0.1:389/ ldaps:/// ldapi:///\""

# Restart slapd
restart_service slapd

# Dump the database daily, before backups run at 3
# This is not required, but nice to have just in case.
cat > /etc/cron.d/mailinabox-ldap << EOF
# Mail-in-a-Box
# Dump database to ldif
30 2 * * *	root	/usr/sbin/slapcat -F "$MIAB_SLAPD_CONF" -o ldif-wrap=no -s "$LDAP_BASE" | /usr/bin/xz > "$STORAGE_LDAP_ROOT/db.ldif.xz"; chmod 600 "$STORAGE_LDAP_ROOT/db.ldif.xz"
EOF