mirror of
https://github.com/osixia/container-openldap.git
synced 2026-11-06 20:48:18 -04:00
feat(ctl): validate LDIF files and directories without importing
This commit is contained in:
@@ -6,6 +6,8 @@ Project-specific changes only. See upstream [OpenLDAP release notes](https://www
|
||||
|
||||
### Added
|
||||
|
||||
- Add `openldap-ctl validate-ldif` for files and directories to check data entry LDIF
|
||||
syntax, values and schemas without importing entries or stopping slapd.
|
||||
- Expose bootstrap settings for pending request limits, incoming LDAP message sizes
|
||||
and blocked network write timeouts, preserving OpenLDAP defaults.
|
||||
- Add `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MIN_DELAY` and `OPENLDAP_BOOTSTRAP_PPOLICY_DEFAULT_MAX_DELAY`
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
- [🔁 Replication](#-replication)
|
||||
- [🛠️ LDAP Toolkit](#️-ldap-toolkit)
|
||||
- [OpenLDAP Utilities](#openldap-utilities)
|
||||
- [Validate LDIF](#validate-ldif)
|
||||
- [Password Helpers](#password-helpers)
|
||||
- [Convert Schema to LDIF](#convert-schema-to-ldif)
|
||||
- [🔤 Environment Variables](#-environment-variables)
|
||||
@@ -293,6 +294,21 @@ docker run --rm osixia/openldap:2.6.15-beta run -- \
|
||||
-s newpassword "uid=jdoe,ou=people,dc=example,dc=org"
|
||||
```
|
||||
|
||||
### Validate LDIF
|
||||
|
||||
Check data entries against the initialized local configuration without importing them or stopping slapd:
|
||||
|
||||
```bash
|
||||
docker exec openldap container run -- openldap-ctl validate-ldif /data/users.ldif
|
||||
docker exec openldap container run -- openldap-ctl validate-ldif /data/users.ldif /data/groups.ldif
|
||||
docker exec openldap container run -- openldap-ctl validate-ldif /data/ldif/
|
||||
```
|
||||
|
||||
Directories are searched recursively for `*.ldif`, sorted by version and excluding hidden paths.
|
||||
Each file must contain complete entries. All files are checked; any validation failure returns a non-zero status.
|
||||
Checks LDIF syntax, attribute values and schemas. Does not check existing entries,
|
||||
ACLs or overlay rules, and does not accept `ldapmodify` change records.
|
||||
|
||||
### Password Helpers
|
||||
|
||||
Generate a random password:
|
||||
|
||||
@@ -44,6 +44,9 @@ After the shared version check, `tests/` runs integration scenarios on each buil
|
||||
- `openldap-ctl` backup and restore;
|
||||
- password hashing: arguments, secret files and stdin, exact contents, custom hash
|
||||
commands and trace output without file/stdin secrets;
|
||||
- LDIF validation: syntax, required attributes, value syntax and suffix checks,
|
||||
multiple files, recursive directories and projected-volume symlinks, with unchanged
|
||||
configuration and data while slapd is stopped or running;
|
||||
- upgrade: pre-upgrade backup on a simulated N-1 image, restore on N, downgrade refused;
|
||||
- upgrade from previous published releases (`previousReleases` in `tests/upgrade.go`).
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ var scenarios = []scenario{
|
||||
{"password policy inactivity", PasswordPolicyIdle},
|
||||
{"progressive password policy lockouts", PasswordPolicyDelay},
|
||||
{"password helpers", PasswordHelpers},
|
||||
{"LDIF validation", ValidateLDIF},
|
||||
{"refint empty-group placeholders", RefintNothing},
|
||||
{"all features with 3 replicated nodes", AllFeatures},
|
||||
{"backup and restore", BackupRestore},
|
||||
|
||||
+128
@@ -0,0 +1,128 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# if container log level is trace:
|
||||
# print commands and their arguments as they are executed
|
||||
container log level eq trace && set -x
|
||||
|
||||
# shellcheck disable=SC1091
|
||||
source /tmp/openldap-test-lib.sh
|
||||
|
||||
if [ "${TEST_VALIDATE_LDIF_MODE}" = online ] && [ "${1:-}" != --online ]; then
|
||||
printf 'bash %q --online\n' "$0" | container --bash
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${TEST_VALIDATE_LDIF_MODE}" = offline ]; then
|
||||
expect_rc 0 container --skip-process
|
||||
else
|
||||
eventually has_entry ldap://127.0.0.1:3890 "${TEST_SUFFIX}"
|
||||
PID_BEFORE=$(cat /run/container/processes/openldap.pid)
|
||||
fi
|
||||
|
||||
TEST_DIR=$(mktemp -d)
|
||||
trap 'rm -rf "${TEST_DIR:?}"' EXIT
|
||||
|
||||
snapshot() {
|
||||
find /etc/openldap/slapd.d -type f -exec sha256sum {} \; | sort
|
||||
sha256sum /var/lib/openldap/openldap-data/data.mdb
|
||||
}
|
||||
snapshot > "${TEST_DIR}/before"
|
||||
|
||||
USER_DN="uid=validate-only,${TEST_SUFFIX}"
|
||||
cat > "${TEST_DIR}/valid entries.ldif" <<LDIF
|
||||
dn: ${USER_DN}
|
||||
objectClass: inetOrgPerson
|
||||
uid: validate-only
|
||||
cn: Validate Only
|
||||
sn: User
|
||||
LDIF
|
||||
chmod 400 "${TEST_DIR}/valid entries.ldif"
|
||||
ln -s 'valid entries.ldif' "${TEST_DIR}/projected.ldif"
|
||||
expect_rc 0 container run -- openldap-ctl validate-ldif "${TEST_DIR}/projected.ldif"
|
||||
expect_output 'LDIF validation succeeded'
|
||||
|
||||
sed 's/validate-only/validate-second/g' "${TEST_DIR}/valid entries.ldif" > "${TEST_DIR}/second.ldif"
|
||||
expect_rc 0 container run -- openldap-ctl validate-ldif "${TEST_DIR}/projected.ldif" "${TEST_DIR}/second.ldif"
|
||||
expect_output 'LDIF validation succeeded: .*projected.ldif$'
|
||||
expect_output 'LDIF validation succeeded: .*second.ldif$'
|
||||
|
||||
mkdir -p "${TEST_DIR}/directory/..revision" "${TEST_DIR}/directory/nested"
|
||||
cat "${TEST_DIR}/second.ldif" > "${TEST_DIR}/directory/..revision/2-second.ldif"
|
||||
cat "${TEST_DIR}/valid entries.ldif" > "${TEST_DIR}/directory/..revision/10-last.ldif"
|
||||
cat "${TEST_DIR}/valid entries.ldif" > "${TEST_DIR}/directory/nested/entry with spaces.ldif"
|
||||
ln -s ..revision "${TEST_DIR}/directory/..data"
|
||||
ln -s ..data/2-second.ldif "${TEST_DIR}/directory/2-second.ldif"
|
||||
ln -s ..data/10-last.ldif "${TEST_DIR}/directory/10-last.ldif"
|
||||
printf 'invalid LDIF\n' > "${TEST_DIR}/directory/.hidden.ldif"
|
||||
printf 'invalid LDIF\n' > "${TEST_DIR}/directory/ignored.ldif.template"
|
||||
expect_rc 0 container run -- openldap-ctl validate-ldif "${TEST_DIR}/directory"
|
||||
mapfile -t VALIDATED_FILES < <(sed -n 's/.*LDIF validation succeeded: //p' "${OUTPUT_FILE}")
|
||||
[ "${#VALIDATED_FILES[@]}" -eq 3 ] || fail 'directory entries were skipped or validated twice'
|
||||
[ "${VALIDATED_FILES[0]}" = "${TEST_DIR}/directory/2-second.ldif" ] || fail 'directory ordering is not numeric'
|
||||
[ "${VALIDATED_FILES[1]}" = "${TEST_DIR}/directory/10-last.ldif" ] || fail 'directory ordering is not numeric'
|
||||
[ "${VALIDATED_FILES[2]}" = "${TEST_DIR}/directory/nested/entry with spaces.ldif" ] || fail 'nested LDIF was not validated'
|
||||
ok 'multiple files, recursive directories and projected-volume symlinks are supported'
|
||||
|
||||
sed '/^sn:/d' "${TEST_DIR}/valid entries.ldif" > "${TEST_DIR}/missing-sn.ldif"
|
||||
expect_fail container run -- openldap-ctl validate-ldif "${TEST_DIR}/missing-sn.ldif"
|
||||
expect_output 'requires attribute.*sn'
|
||||
|
||||
expect_fail container run -- openldap-ctl validate-ldif "${TEST_DIR}/projected.ldif" "${TEST_DIR}/missing-sn.ldif" "${TEST_DIR}/second.ldif"
|
||||
expect_output 'LDIF validation succeeded: .*second.ldif$'
|
||||
expect_output 'One or more LDIF files failed validation'
|
||||
cat "${TEST_DIR}/missing-sn.ldif" > "${TEST_DIR}/directory/nested/invalid.ldif"
|
||||
expect_fail container run -- openldap-ctl validate-ldif "${TEST_DIR}/directory"
|
||||
expect_output 'requires attribute.*sn'
|
||||
|
||||
cat "${TEST_DIR}/valid entries.ldif" > "${TEST_DIR}/unknown-attribute.ldif"
|
||||
printf 'unknownAttribute: value\n' >> "${TEST_DIR}/unknown-attribute.ldif"
|
||||
expect_fail container run -- openldap-ctl validate-ldif "${TEST_DIR}/unknown-attribute.ldif"
|
||||
expect_output 'attribute type undefined'
|
||||
|
||||
cat > "${TEST_DIR}/invalid-value.ldif" <<LDIF
|
||||
dn: cn=role,${TEST_SUFFIX}
|
||||
objectClass: organizationalRole
|
||||
cn: role
|
||||
roleOccupant: invalid-dn
|
||||
LDIF
|
||||
expect_fail container run -- openldap-ctl validate-ldif "${TEST_DIR}/invalid-value.ldif"
|
||||
expect_output 'smr_normalize roleOccupant|invalid.*syntax|value.*invalid'
|
||||
|
||||
sed 's/^dn:.*/dn: uid=validate-only,dc=outside/' "${TEST_DIR}/valid entries.ldif" > "${TEST_DIR}/wrong-suffix.ldif"
|
||||
expect_fail container run -- openldap-ctl validate-ldif "${TEST_DIR}/wrong-suffix.ldif"
|
||||
expect_output 'not configured to hold'
|
||||
|
||||
printf 'dn: %s\nchangetype: modify\nreplace: cn\ncn: Changed\n' "${USER_DN}" > "${TEST_DIR}/modify.ldif"
|
||||
expect_fail container run -- openldap-ctl validate-ldif "${TEST_DIR}/modify.ldif"
|
||||
printf 'dn: invalid-dn\nobjectClass: inetOrgPerson\n' > "${TEST_DIR}/invalid-dn.ldif"
|
||||
expect_fail container run -- openldap-ctl validate-ldif "${TEST_DIR}/invalid-dn.ldif"
|
||||
ok "LDIF validation (${TEST_VALIDATE_LDIF_MODE}) rejects invalid entries and change records"
|
||||
|
||||
expect_fail container run -- openldap-ctl validate-ldif
|
||||
expect_output 'Usage: openldap-ctl validate-ldif'
|
||||
expect_fail container run -- openldap-ctl validate-ldif "${TEST_DIR}/projected.ldif" extra
|
||||
expect_fail container run -- openldap-ctl validate-ldif "${TEST_DIR}/missing.ldif"
|
||||
mkdir "${TEST_DIR}/empty"
|
||||
expect_fail container run -- openldap-ctl validate-ldif "${TEST_DIR}/empty"
|
||||
expect_output 'No LDIF files found'
|
||||
mkdir "${TEST_DIR}/loop"
|
||||
ln -s . "${TEST_DIR}/loop/cycle"
|
||||
expect_fail container run -- openldap-ctl validate-ldif "${TEST_DIR}/loop"
|
||||
expect_fail env OPENLDAP_CONF_DIR="${TEST_DIR}" container run -- openldap-ctl validate-ldif "${TEST_DIR}/projected.ldif"
|
||||
expect_output 'configuration is not initialized'
|
||||
expect_rc 0 container run -- openldap-ctl validate-ldif --help
|
||||
|
||||
snapshot > "${TEST_DIR}/after"
|
||||
cmp "${TEST_DIR}/before" "${TEST_DIR}/after" || fail 'validation changed config or data files'
|
||||
[ -z "$(find /run/container/tmp -maxdepth 1 -name 'openldap-validate.*' -print)" ] || fail 'validation left a temporary file'
|
||||
|
||||
if [ "${TEST_VALIDATE_LDIF_MODE}" = online ]; then
|
||||
[ "$(cat /run/container/processes/openldap.pid)" = "${PID_BEFORE}" ] || fail 'validation restarted slapd'
|
||||
expect_rc 32 bind_search ldap://127.0.0.1:3890 "${ADMIN_DN}" -b "${USER_DN}" -s base dn
|
||||
expect_rc 0 bind_search ldap://127.0.0.1:3890 "${ADMIN_DN}" -b "${TEST_SUFFIX}" -s base dn
|
||||
else
|
||||
[ ! -e /run/container/processes/openldap.pid ] || fail 'validation started slapd'
|
||||
fi
|
||||
ok "LDIF validation (${TEST_VALIDATE_LDIF_MODE}) leaves configuration, data and process state unchanged"
|
||||
@@ -0,0 +1,27 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
|
||||
"dagger.io/dagger"
|
||||
)
|
||||
|
||||
// ValidateLDIF checks entry validation without changing local config or data.
|
||||
func ValidateLDIF(ctx context.Context, client *dagger.Client, image *dagger.Container) error {
|
||||
passwords, err := rootPasswords(ctx, image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, mode := range []string{"offline", "online"} {
|
||||
if err := check(ctx, withEnv(image, passwords), nil,
|
||||
env{"TEST_VALIDATE_LDIF_MODE": mode}, validateLDIFCheck); err != nil {
|
||||
return fmt.Errorf("LDIF validation %s: %w", mode, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed testdata/validate-ldif.sh
|
||||
var validateLDIFCheck string
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# if container log level is trace:
|
||||
# print commands and their arguments as they are executed
|
||||
container log level eq trace && set -x
|
||||
|
||||
usage() {
|
||||
echo "Usage: openldap-ctl validate-ldif file-or-directory [file-or-directory ...]"
|
||||
}
|
||||
|
||||
if [ "$#" -eq 1 ] && { [ "$1" = --help ] || [ "$1" = -h ]; }; then
|
||||
usage
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "$#" -eq 0 ]; then
|
||||
usage
|
||||
container log fatal "At least one LDIF file or directory is required"
|
||||
fi
|
||||
|
||||
if [ ! -f "${OPENLDAP_CONF_DIR}/cn=config.ldif" ]; then
|
||||
container log fatal "OpenLDAP configuration is not initialized in ${OPENLDAP_CONF_DIR}"
|
||||
fi
|
||||
|
||||
LDIF_FILES=()
|
||||
LDIF_LIST_FILE=$(mktemp /run/container/tmp/openldap-validate.XXXXXX)
|
||||
trap 'rm -f "${LDIF_LIST_FILE}"' EXIT
|
||||
|
||||
for path in "$@"; do
|
||||
if [ -d "${path}" ]; then
|
||||
# Prefix relative paths so find never interprets them as expressions.
|
||||
if [[ "${path}" != /* ]]; then
|
||||
path="./${path}"
|
||||
fi
|
||||
# Follow projected-volume symlinks, but skip hidden revision directories
|
||||
# to avoid validating their entries twice. Keep traversal errors fatal.
|
||||
find -L "${path}" -mindepth 1 -name '.*' -prune -o \
|
||||
-type f -name '*.ldif' -print0 | sort -zV > "${LDIF_LIST_FILE}"
|
||||
if [ ! -s "${LDIF_LIST_FILE}" ]; then
|
||||
container log fatal "No LDIF files found in directory: ${path}"
|
||||
fi
|
||||
while IFS= read -r -d '' file; do
|
||||
LDIF_FILES+=("${file}")
|
||||
done < "${LDIF_LIST_FILE}"
|
||||
elif [ -f "${path}" ] && [ -r "${path}" ]; then
|
||||
LDIF_FILES+=("${path}")
|
||||
else
|
||||
container log fatal "Path must be a readable LDIF file or directory: ${path}"
|
||||
fi
|
||||
done
|
||||
|
||||
FAILED=false
|
||||
for file in "${LDIF_FILES[@]}"; do
|
||||
# Dry-run mode skips database writes; value-check also validates attribute syntax.
|
||||
if slapadd -u -o value-check=yes -F "${OPENLDAP_CONF_DIR}" -n 1 -l "${file}"; then
|
||||
container log info "LDIF validation succeeded: ${file}"
|
||||
else
|
||||
container log error "LDIF validation failed: ${file}"
|
||||
FAILED=true
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${FAILED}" = true ]; then
|
||||
container log fatal "One or more LDIF files failed validation"
|
||||
fi
|
||||
Reference in New Issue
Block a user