#!/bin/bash

# Script to update ppolicy.ldif in the OpenLDAP Online Configuration (OLC) to
# include the pwdMaxRecordedFailure attribute.

# Check if an update is necessary.
conf_dir='/etc/openldap/slapd.d'
if [ ! -d "${conf_dir}" ] || [ ! -f "${conf_dir}/cn=config.ldif" ]; then
    exit 0
fi
ppolicy_ldif=( "${conf_dir}"/cn=config/cn=schema/cn={*}ppolicy.ldif )
if [ ! -f "${ppolicy_ldif}" ]; then
    exit 0
fi
if /usr/bin/grep -q pwdMaxRecordedFailure "${ppolicy_ldif}"; then
    exit 0
fi

# Check that .rpmnew can be used as a temporary file by the script. The file
# should never be present.
if [ -f "${ppolicy_ldif}.rpmnew" ]; then
    exit 0
fi

echo "Updating ppolicy schema ${ppolicy_ldif} ..."

# Make sure slapd.service is not running.
slapd_running=1

# Don't check if no systemd, we could be in a container.
if [ -f "/usr/bin/systemctl" ]; then
    /usr/bin/systemctl is-active --quiet slapd.service
    slapd_running=$?
fi

if [ $slapd_running -eq 0 ]; then
    /usr/bin/systemctl stop slapd.service
fi

# Backup the original file.
if [ ! -f "${ppolicy_ldif}.rpmsave" ]; then
    /bin/cp -p "${ppolicy_ldif}" "${ppolicy_ldif}.rpmsave"
fi

# Update the ppolicy.ldif file with pwdMaxRecordedFailure.
/bin/cp -p "${ppolicy_ldif}" "${ppolicy_ldif}.rpmnew"
/usr/bin/awk '
BEGIN {
    # CRC-32 ZIP polynomial in reversed bit order.
    POLY = 0xedb88320

    # 8-bit character -> ordinal table.
    for (i = 0; i < 256; i++)
        ORD[sprintf("%c", i)] = i

    # Input location of the last olcAttributeTypes entry.
    last_attr_type_index = -1
    last_attr_type_length = -1
    in_attr_type = 0

    # Input location of the olcObjectClasses pwdPolicy entry.
    pwd_policy_index = -1
    pwd_policy_length = -1
    in_pwd_policy = 0
}

{
    # Remember each input line.
    input[NR] = $0

    # Verify the file header.
    if (NR == 1 && $0 != "# AUTO-GENERATED FILE - DO NOT EDIT!! Use ldapmodify.")
        exit 1
    if (NR == 2 && $0 !~ /# CRC32 ......../)
        exit 1

    # Reset any continuation if a new entry is encountered.
    if ($0 !~ /^ /) {
        in_attr_type = 0
        in_pwd_policy = 0
    }

    # Find the last olcAttributeTypes and the olcObjectClasses pwdPolicy
    # entries in the input.
    if ($0 ~ /^olcAttributeTypes: /) {
        last_attr_type_index = NR
        last_attr_type_length = 1
        in_attr_type = 1
    }
    else if ($0 ~ /^olcObjectClasses: .* NAME '\''pwdPolicy'\''/) {
        pwd_policy_index = NR
        pwd_policy_length = 1
        in_pwd_policy = 1
    }
    else if ($0 ~ /^ /) {
        if (in_attr_type)
            last_attr_type_length++
        if (in_pwd_policy)
            pwd_policy_length++
    }
}

# Unwrap a series of lines from the global `input` array and return the
# resulting entry string.
function input_unwrap(input_index, entry_length,    res, i) {
    res = input[input_index]
    for (i = 1; i < entry_length; i++)
        res = res substr(input[input_index + i], 2)
    return res
}

# Wrap an entry string and record it in the global `output` array.
function output_wrap(output_index, entry) {
    output[output_index] = substr(entry, 1, 76)
    entry = substr(entry, 77)
    output_index++
    while (length(entry) > 0) {
        output[output_index++] = " " substr(entry, 1, 75)
        entry = substr(entry, 76)
    }
    return output_index
}

# Calculate CRC-32.
function crc32(crc, string,    i, j, c) {
    crc = and(compl(crc), 0xffffffff)
    for (i = 1; i <= length(string); i++) {
        c = substr(string, i, 1)
        crc = xor(crc, ORD[c])
        for (j = 0; j < 8; j++)
            crc = and(crc, 1) ? xor(rshift(crc, 1), POLY) : rshift(crc, 1)
    }
    crc = and(compl(crc), 0xffffffff)
    return crc
}

END {
    # Check that the input data looks sensible.
    if (last_attr_type_index == -1 || pwd_policy_index == -1 ||
        last_attr_type_index > pwd_policy_index)
        exit 1

    # Get index of the last olcAttributeTypes entry.
    line = input[last_attr_type_index]
    m = match(line, /{[^}]+}/)
    if (!m)
        exit 1
    next_type_entry = strtonum(substr(line, RSTART + 1, RLENGTH - 2))
    if (!next_type_entry)
        exit 1
    next_type_entry++

    # Prepare the new pwdMaxRecordedFailure entry.
    new_type_entry = "olcAttributeTypes: {" next_type_entry "}( 1.3.6.1.4.1.42.2.27.8.1.30 NAME '\''pwdMaxRecordedFailure'\'' EQUALITY integerMatch ORDERING integerOrderingMatch  SYNTAX 1.3.6.1.4.1.1466.115.121.1.27 SINGLE-VALUE )"

    # Update the pwdPolicy entry to include pwdMaxRecordedFailure.
    pwd_policy_entry = input_unwrap(pwd_policy_index, pwd_policy_length)
    r = gsub(/\$ pwdSafeModify/, "$ pwdSafeModify $ pwdMaxRecordedFailure", pwd_policy_entry)
    if (r != 1)
        exit 1

    # Prepare the output.
    output_index = 1
    last_attr_type_end = last_attr_type_index + last_attr_type_length
    for (i = 1; i < last_attr_type_end; i++)
        output[output_index++] = input[i]
    output_index = output_wrap(output_index, new_type_entry)
    for (i = last_attr_type_end; i < pwd_policy_index; i++)
        output[output_index++] = input[i]
    output_index = output_wrap(output_index, pwd_policy_entry)
    for (i = pwd_policy_index + pwd_policy_length; i <= length(input); i++)
        output[output_index++] = input[i]

    # Calculate CRC-32 of the file and update it in the header.
    crc = 0
    for (i = 3; i <= length(output); i++)
        crc = crc32(crc, output[i] "\n")
    output[2] = "# CRC32 " sprintf("%08x", crc)

    # Print the output.
    for (i = 1; i <= length(output); i++)
        print output[i]
}' "${ppolicy_ldif}" > "${ppolicy_ldif}.rpmnew"
rc=$?
if [ $rc -eq 0 ]; then
    /bin/mv "${ppolicy_ldif}.rpmnew" "${ppolicy_ldif}"
else
    /bin/rm "${ppolicy_ldif}.rpmnew"
fi

# Restore slapd.service to the previous state.
if [ $slapd_running -eq 0 ]; then
    /usr/bin/systemctl start slapd.service
fi

exit 0

