mirror of
https://github.com/osixia/container-openldap.git
synced 2026-11-06 12:40:36 -04:00
feat(bootstrap): add scoped directory administrator
Create an optional administrator for user/group CRUD and password resets while protecting service accounts, password hashes/history and cn=config. Document the distinct data root, config root, admin and management roles. Keep conditional history ACLs in their account directories as optional templates, activated by feature scripts after rendering in a temporary copy. Cover read-only sources, admin permissions, readonly history privacy and custom overrides.
This commit is contained in:
@@ -6,6 +6,11 @@ Project-specific changes only. See upstream [OpenLDAP release notes](https://www
|
||||
|
||||
### Added
|
||||
|
||||
- Add optional `OPENLDAP_BOOTSTRAP_DATA_ADMIN` for a real directory administrator
|
||||
entry that can manage users/groups and reset their passwords, while protecting
|
||||
service accounts, its own account, password hashes/history and `cn=config`.
|
||||
Document how this scoped administrator differs from the data/config root DNs
|
||||
and the application management account.
|
||||
- Expose management account hidden attributes through
|
||||
`OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_HIDDEN_ATTRIBUTES` and
|
||||
`OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_HIDDEN_PPOLICY_ATTRIBUTES`.
|
||||
@@ -40,6 +45,11 @@ Project-specific changes only. See upstream [OpenLDAP release notes](https://www
|
||||
|
||||
### Changed
|
||||
|
||||
- Keep password-history ACL fragments in their account feature directories as
|
||||
`.ldif.optional.template` files, activated with `mv` by the account scripts.
|
||||
The helper only renders/copies fragments; final assembly ignores inactive files.
|
||||
Read-only history overrides must use `64-database-readonly-ppolicy-acl.ldif`.
|
||||
|
||||
- Rename bootstrap feature flags `OPENLDAP_BOOTSTRAP_MONITOR_ENABLED` and
|
||||
`OPENLDAP_BOOTSTRAP_AUDITLOG_ENABLED` to `OPENLDAP_BOOTSTRAP_MONITOR` and
|
||||
`OPENLDAP_BOOTSTRAP_AUDITLOG`. Update deployment environments to the new names;
|
||||
|
||||
@@ -385,6 +385,9 @@ docker run --rm --user $UID -v $(pwd)/schema:/data \
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_DN` | Users OU DN | `ou=users,${OPENLDAP_BOOTSTRAP_SUFFIX}` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_TREE_GROUPS_DN` | Groups OU DN | `ou=groups,${OPENLDAP_BOOTSTRAP_SUFFIX}` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN` | Services OU DN | `ou=services,${OPENLDAP_BOOTSTRAP_SUFFIX}` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_ADMIN` | Create an account to administer users and groups | `false` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN` | Scoped administrator DN | `cn=admin,${OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN}` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_ADMIN_PASSWORD_HASHED` | Hashed administrator password; generated if empty when enabled | `` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT` | Enable user/group reads and user password resets | `false` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN` | Management account DN | `cn=management,${OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN}` |
|
||||
| `OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_PASSWORD_HASHED` | Hashed management password; generated if empty when enabled | `` |
|
||||
@@ -455,8 +458,8 @@ When ppolicy and the data read-only account are enabled, bootstrap explicitly
|
||||
denies that account access to `pwdHistory` before its general read grant. Other
|
||||
identities continue through the usual ACLs: administrators and replication retain
|
||||
access, while ordinary users are denied by the final catch-all unless a custom
|
||||
ACL grants access. Replace `64-password-history-acl.ldif.template` through the
|
||||
custom config directory to override the read-only restriction. This ACL is added
|
||||
ACL grants access. Override `64-database-readonly-ppolicy-acl.ldif` through the
|
||||
custom config directory to change the read-only restriction. This ACL is added
|
||||
on new bootstraps only; existing databases need an explicit ACL update.
|
||||
|
||||
**Unique**
|
||||
|
||||
@@ -21,6 +21,10 @@ own native runner, then publishes them together from their OCI archives
|
||||
After the shared version check, `tests/` runs integration scenarios on each built image:
|
||||
|
||||
- default bootstrap;
|
||||
- scoped directory administrator: disabled by default, custom DNs and supplied
|
||||
OUs, generated passwords, readable account identifiers for directory clients,
|
||||
user/group creation, edits, renames, deletion and password resets, hidden
|
||||
hashes/history, and protected service accounts, own account and configuration;
|
||||
- standard users/groups/services tree and management account: independent opt-ins,
|
||||
custom DNs and supplied OUs, generated passwords, user/group
|
||||
reads, password resets including application administrators, ppolicy-safe resets,
|
||||
|
||||
@@ -0,0 +1,109 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"context"
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"dagger.io/dagger"
|
||||
)
|
||||
|
||||
// DataAdmin checks the scoped directory administrator and coexistence with other accounts.
|
||||
func DataAdmin(ctx context.Context, client *dagger.Client, image *dagger.Container) error {
|
||||
passwords, err := rootPasswords(ctx, image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
base := merge(passwords, env{
|
||||
"OPENLDAP_BOOTSTRAP_DATA_TREE": "true",
|
||||
"OPENLDAP_BOOTSTRAP_DATA_ADMIN": "true",
|
||||
"OPENLDAP_BOOTSTRAP_DATA_ADMIN_PASSWORD_HASHED": passwords["OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED"],
|
||||
"OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT": "true",
|
||||
"OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_PASSWORD_HASHED": passwords["OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED"],
|
||||
})
|
||||
for _, mode := range []string{"disabled", "plain", "policy", "custom", "supplied"} {
|
||||
if err := func() error {
|
||||
usersDN := "ou=users," + suffix
|
||||
groupsDN := "ou=groups," + suffix
|
||||
servicesDN := "ou=services," + suffix
|
||||
settings := merge(base)
|
||||
nodeImage := image
|
||||
switch mode {
|
||||
case "disabled":
|
||||
settings["OPENLDAP_BOOTSTRAP_DATA_ADMIN"] = "false"
|
||||
case "policy":
|
||||
settings = merge(settings, env{
|
||||
"OPENLDAP_BOOTSTRAP_PPOLICY": "true",
|
||||
"OPENLDAP_BOOTSTRAP_DATA_READONLY": "true",
|
||||
"OPENLDAP_BOOTSTRAP_DATA_READONLY_PASSWORD_HASHED": passwords["OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED"],
|
||||
})
|
||||
case "custom":
|
||||
usersDN = "ou=people," + suffix
|
||||
groupsDN = "ou=roles," + suffix
|
||||
servicesDN = "ou=applications," + suffix
|
||||
case "supplied":
|
||||
settings["OPENLDAP_BOOTSTRAP_DATA_TREE"] = "false"
|
||||
nodeImage = image.WithNewFile("/container/services/openldap-bootstrap/assets/ldif/data/custom/30-custom-tree.ldif", fmt.Sprintf(`dn: %s
|
||||
objectClass: organizationalUnit
|
||||
ou: users
|
||||
|
||||
dn: %s
|
||||
objectClass: organizationalUnit
|
||||
ou: groups
|
||||
|
||||
dn: %s
|
||||
objectClass: organizationalUnit
|
||||
ou: services
|
||||
`, usersDN, groupsDN, servicesDN))
|
||||
}
|
||||
adminDN := "cn=admin," + servicesDN
|
||||
if mode == "custom" {
|
||||
adminDN = "cn=directory-admin," + servicesDN
|
||||
}
|
||||
managementDN := "cn=management," + servicesDN
|
||||
settings = merge(settings, env{
|
||||
"OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_DN": usersDN,
|
||||
"OPENLDAP_BOOTSTRAP_DATA_TREE_GROUPS_DN": groupsDN,
|
||||
"OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN": servicesDN,
|
||||
"OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN": adminDN,
|
||||
"OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN": managementDN,
|
||||
})
|
||||
node := "directory-admin-" + mode
|
||||
service, err := startNode(ctx, nodeImage, node, settings, nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer stopNodes(ctx, []*dagger.Service{service})
|
||||
return check(ctx, image, nil, env{
|
||||
"TEST_NODES": node,
|
||||
"TEST_ADMIN_MODE": mode,
|
||||
"TEST_USERS_DN": usersDN,
|
||||
"TEST_GROUPS_DN": groupsDN,
|
||||
"TEST_SERVICES_DN": servicesDN,
|
||||
"TEST_DIRECTORY_ADMIN_DN": adminDN,
|
||||
"TEST_MANAGEMENT_DN": managementDN,
|
||||
"TEST_PASSWORD_HASHED": passwords["OPENLDAP_BOOTSTRAP_DATA_ROOT_PASSWORD_HASHED"],
|
||||
}, dataAdminCheck)
|
||||
}(); err != nil {
|
||||
return fmt.Errorf("directory admin %s: %w", mode, err)
|
||||
}
|
||||
}
|
||||
|
||||
generated, err := expect(ctx, withEnv(image, merge(base, env{"OPENLDAP_BOOTSTRAP_DATA_ADMIN_PASSWORD_HASHED": ""})), true,
|
||||
[]string{"Generated database admin user password:", "config file testing succeeded"}, "container", "--skip-process")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
data, err := generated.container.WithExec([]string{"container", "run", "--", "slapcat", "-n", "1", "-a", "(cn=admin)"}).Stdout(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !strings.Contains(data, "dn: cn=admin,ou=services,"+suffix) || !strings.Contains(data, "userPassword:: ") {
|
||||
return fmt.Errorf("generated directory admin account/password missing")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
//go:embed testdata/data-admin.sh
|
||||
var dataAdminCheck string
|
||||
@@ -47,7 +47,7 @@ func PasswordPolicyHistory(ctx context.Context, client *dagger.Client, image *da
|
||||
by * break
|
||||
`)
|
||||
} else if mode == "replacement" {
|
||||
nodeImage = image.WithNewFile("/container/services/openldap-bootstrap/assets/ldif/config/custom/64-password-history-acl.ldif", "")
|
||||
nodeImage = image.WithNewFile("/container/services/openldap-bootstrap/assets/ldif/config/custom/64-database-readonly-ppolicy-acl.ldif", "")
|
||||
}
|
||||
if mode == "replicated" {
|
||||
nodes = append(nodes, "history-ldap2")
|
||||
@@ -64,7 +64,7 @@ func PasswordPolicyHistory(ctx context.Context, client *dagger.Client, image *da
|
||||
services := make([]*dagger.Service, 0, len(nodes))
|
||||
defer func() { stopNodes(ctx, services) }()
|
||||
for _, node := range nodes {
|
||||
urls := env{"OPENLDAP_URLS": "ldap://" + node + ":3890 ldapi:///"}
|
||||
urls := env{"OPENLDAP_URLS": "ldap://" + node + ":3890 ldaps://" + node + ":6360 ldapi:///"}
|
||||
service, err := startNode(ctx, nodeImage, node, merge(settings, urls), nil, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@@ -20,6 +20,7 @@ type scenario struct {
|
||||
var scenarios = []scenario{
|
||||
{"default bootstrap", Default},
|
||||
{"standard data tree and management account", DataManagement},
|
||||
{"scoped directory administrator", DataAdmin},
|
||||
{"global request limits", GlobalLimits},
|
||||
{"read-only bootstrap templates", ReadOnlyTemplates},
|
||||
{"alternative schemas", AlternativeSchemas},
|
||||
|
||||
Vendored
+171
@@ -0,0 +1,171 @@
|
||||
#!/bin/bash
|
||||
# shellcheck disable=SC2153
|
||||
|
||||
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
|
||||
|
||||
URI="ldap://${TEST_NODES}:3890"
|
||||
DIRECTORY_ADMIN_DN="${TEST_DIRECTORY_ADMIN_DN}"
|
||||
USER_DN="uid=alice,${TEST_USERS_DN}"
|
||||
RENAMED_DN="uid=alice-renamed,${TEST_USERS_DN}"
|
||||
GROUP_DN="cn=team,${TEST_GROUPS_DN}"
|
||||
RENAMED_GROUP_DN="cn=renamed-team,${TEST_GROUPS_DN}"
|
||||
SERVICE_DN="cn=other-service,${TEST_SERVICES_DN}"
|
||||
OUTSIDE_DN="cn=outside,${TEST_SUFFIX}"
|
||||
NEW_PASSWORD="Changed-passw0rd-2026"
|
||||
|
||||
eventually has_entry "${URI}" "${TEST_SUFFIX}"
|
||||
|
||||
if [ "${TEST_ADMIN_MODE}" = "disabled" ]; then
|
||||
expect_rc 32 bind_search "${URI}" "${ADMIN_DN}" -b "${DIRECTORY_ADMIN_DN}" -s base dn
|
||||
expect_rc 0 bind_search "${URI}" "${CONFIG_ADMIN_DN}" -b 'olcDatabase={1}mdb,cn=config' -s base olcAccess
|
||||
expect_no_output "${DIRECTORY_ADMIN_DN}"
|
||||
ok 'directory administrator and its ACLs are disabled by default'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${DIRECTORY_ADMIN_DN}" -w "${TEST_PASSWORD}"
|
||||
expect_rc 0 bind_search "${URI}" "${DIRECTORY_ADMIN_DN}" -b "${DIRECTORY_ADMIN_DN}" -s base objectClass cn entryUUID userPassword
|
||||
expect_output '^objectClass: organizationalRole$'
|
||||
expect_output '^objectClass: simpleSecurityObject$'
|
||||
expect_output '^entryUUID:'
|
||||
expect_no_output '^userPassword:'
|
||||
ok 'directory administrator can bind and read its real entry and identifier'
|
||||
|
||||
expect_rc 0 bind_modify "${URI}" "${DIRECTORY_ADMIN_DN}" -a <<LDIF
|
||||
dn: ${USER_DN}
|
||||
objectClass: inetOrgPerson
|
||||
uid: alice
|
||||
cn: Alice
|
||||
sn: User
|
||||
displayName: Alice User
|
||||
mail: alice@example.org
|
||||
userPassword: ${TEST_PASSWORD_HASHED}
|
||||
|
||||
dn: ${GROUP_DN}
|
||||
objectClass: groupOfNames
|
||||
cn: team
|
||||
member: ${USER_DN}
|
||||
LDIF
|
||||
expect_rc 0 bind_modify "${URI}" "${DIRECTORY_ADMIN_DN}" <<LDIF
|
||||
dn: ${USER_DN}
|
||||
changetype: modify
|
||||
replace: mail
|
||||
mail: updated@example.org
|
||||
|
||||
dn: ${GROUP_DN}
|
||||
changetype: modify
|
||||
replace: description
|
||||
description: Updated by directory administrator
|
||||
LDIF
|
||||
expect_rc 0 bind_search "${URI}" "${DIRECTORY_ADMIN_DN}" -b "${USER_DN}" -s base mail userPassword
|
||||
expect_output '^mail: updated@example.org$'
|
||||
expect_no_output '^userPassword:'
|
||||
expect_rc 0 bind_search "${URI}" "${DIRECTORY_ADMIN_DN}" -b "${GROUP_DN}" -s base description member
|
||||
expect_output '^description: Updated by directory administrator$'
|
||||
expect_output "^member: ${USER_DN}$"
|
||||
expect_rc 50 ldapcompare -x -H "${URI}" -D "${DIRECTORY_ADMIN_DN}" -w "${TEST_PASSWORD}" "${USER_DN}" "userPassword:${TEST_PASSWORD_HASHED}"
|
||||
ok 'directory administrator creates and edits users/groups without hash read/compare'
|
||||
|
||||
expect_rc 0 ldappasswd -x -H "${URI}" -D "${DIRECTORY_ADMIN_DN}" -w "${TEST_PASSWORD}" -s "${NEW_PASSWORD}" "${USER_DN}"
|
||||
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${USER_DN}" -w "${NEW_PASSWORD}"
|
||||
expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b "${USER_DN}" -s base userPassword
|
||||
expect_output '^userPassword:: '
|
||||
|
||||
if [ "${TEST_ADMIN_MODE}" = "policy" ]; then
|
||||
expect_rc 0 bind_modify "${URI}" "${DIRECTORY_ADMIN_DN}" <<LDIF
|
||||
dn: ${USER_DN}
|
||||
changetype: modify
|
||||
replace: userPassword
|
||||
userPassword: short
|
||||
LDIF
|
||||
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${USER_DN}" -w short
|
||||
expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b "${USER_DN}" -s base pwdHistory
|
||||
expect_output '^pwdHistory:'
|
||||
expect_rc 0 bind_search "${URI}" "${DIRECTORY_ADMIN_DN}" -b "${USER_DN}" -s base userPassword pwdHistory
|
||||
expect_no_output '^(userPassword|pwdHistory):'
|
||||
expect_fail bind_modify "${URI}" "${DIRECTORY_ADMIN_DN}" <<LDIF
|
||||
dn: ${USER_DN}
|
||||
changetype: modify
|
||||
delete: pwdHistory
|
||||
LDIF
|
||||
expect_rc 0 bind_search "${URI}" "cn=readonly,${TEST_SUFFIX}" -b "${USER_DN}" -s base cn pwdHistory
|
||||
expect_output '^cn: Alice$'
|
||||
expect_no_output '^pwdHistory:'
|
||||
ok 'policy resets bypass quality checks while password history stays protected'
|
||||
fi
|
||||
|
||||
expect_rc 0 ldappasswd -x -H "${URI}" -D "${TEST_MANAGEMENT_DN}" -w "${TEST_PASSWORD}" -s "${NEW_PASSWORD}" "${USER_DN}"
|
||||
expect_rc 0 bind_search "${URI}" "${TEST_MANAGEMENT_DN}" -b "${USER_DN}" -s base mail
|
||||
expect_output '^mail: updated@example.org$'
|
||||
expect_rc 50 bind_modify "${URI}" "${TEST_MANAGEMENT_DN}" <<LDIF
|
||||
dn: ${USER_DN}
|
||||
changetype: modify
|
||||
replace: mail
|
||||
mail: forbidden@example.org
|
||||
LDIF
|
||||
expect_rc 0 ldappasswd -x -H "${URI}" -D "${USER_DN}" -w "${NEW_PASSWORD}" -a "${NEW_PASSWORD}" -s "Another-passw0rd-2026"
|
||||
ok 'management permissions and normal user password changes remain available'
|
||||
|
||||
for dn in "${SERVICE_DN}" "${OUTSIDE_DN}"; do
|
||||
cn="${dn%%,*}"
|
||||
expect_rc 0 bind_modify "${URI}" "${ADMIN_DN}" -a <<LDIF
|
||||
dn: ${dn}
|
||||
objectClass: organizationalRole
|
||||
objectClass: simpleSecurityObject
|
||||
cn: ${cn#cn=}
|
||||
userPassword: ${TEST_PASSWORD_HASHED}
|
||||
LDIF
|
||||
done
|
||||
for dn in "${SERVICE_DN}" "${OUTSIDE_DN}" "${TEST_MANAGEMENT_DN}"; do
|
||||
expect_rc 32 bind_search "${URI}" "${DIRECTORY_ADMIN_DN}" -b "${dn}" -s base '*' '+'
|
||||
done
|
||||
for dn in "${SERVICE_DN}" "${OUTSIDE_DN}" "${TEST_MANAGEMENT_DN}" "${DIRECTORY_ADMIN_DN}"; do
|
||||
expect_fail ldappasswd -x -H "${URI}" -D "${DIRECTORY_ADMIN_DN}" -w "${TEST_PASSWORD}" -s "${NEW_PASSWORD}" "${dn}"
|
||||
expect_fail bind_modify "${URI}" "${DIRECTORY_ADMIN_DN}" <<LDIF
|
||||
dn: ${dn}
|
||||
changetype: modify
|
||||
replace: description
|
||||
description: Forbidden change
|
||||
LDIF
|
||||
expect_fail ldapdelete -x -H "${URI}" -D "${DIRECTORY_ADMIN_DN}" -w "${TEST_PASSWORD}" "${dn}"
|
||||
expect_rc 0 ldapwhoami -x -H "${URI}" -D "${dn}" -w "${TEST_PASSWORD}"
|
||||
done
|
||||
expect_fail bind_modify "${URI}" "${DIRECTORY_ADMIN_DN}" -a <<LDIF
|
||||
dn: cn=forbidden,${TEST_SERVICES_DN}
|
||||
objectClass: organizationalRole
|
||||
cn: forbidden
|
||||
LDIF
|
||||
expect_fail ldapmodrdn -x -H "${URI}" -D "${DIRECTORY_ADMIN_DN}" -w "${TEST_PASSWORD}" -r -s "${TEST_SERVICES_DN}" "${USER_DN}" uid=escaped
|
||||
expect_fail ldapmodrdn -x -H "${URI}" -D "${DIRECTORY_ADMIN_DN}" -w "${TEST_PASSWORD}" "${DIRECTORY_ADMIN_DN}" cn=renamed-admin
|
||||
expect_rc 32 bind_search "${URI}" "${DIRECTORY_ADMIN_DN}" -b cn=config -s base '*'
|
||||
expect_rc 50 bind_modify "${URI}" "${DIRECTORY_ADMIN_DN}" <<LDIF
|
||||
dn: olcDatabase={1}mdb,cn=config
|
||||
changetype: modify
|
||||
replace: olcRootPW
|
||||
olcRootPW: forbidden
|
||||
LDIF
|
||||
ok 'administrator cannot change services, its own account, outside entries or configuration'
|
||||
|
||||
expect_rc 0 ldapmodrdn -x -H "${URI}" -D "${DIRECTORY_ADMIN_DN}" -w "${TEST_PASSWORD}" -r "${USER_DN}" uid=alice-renamed
|
||||
expect_rc 0 ldapmodrdn -x -H "${URI}" -D "${DIRECTORY_ADMIN_DN}" -w "${TEST_PASSWORD}" -r "${GROUP_DN}" cn=renamed-team
|
||||
expect_rc 0 bind_modify "${URI}" "${DIRECTORY_ADMIN_DN}" <<LDIF
|
||||
dn: ${RENAMED_GROUP_DN}
|
||||
changetype: modify
|
||||
replace: member
|
||||
member: ${RENAMED_DN}
|
||||
LDIF
|
||||
expect_rc 0 ldapdelete -x -H "${URI}" -D "${DIRECTORY_ADMIN_DN}" -w "${TEST_PASSWORD}" "${RENAMED_GROUP_DN}"
|
||||
expect_rc 0 ldapdelete -x -H "${URI}" -D "${DIRECTORY_ADMIN_DN}" -w "${TEST_PASSWORD}" "${RENAMED_DN}"
|
||||
expect_rc 32 bind_search "${URI}" "${ADMIN_DN}" -b "${RENAMED_DN}" -s base dn
|
||||
expect_rc 32 bind_search "${URI}" "${ADMIN_DN}" -b "${RENAMED_GROUP_DN}" -s base dn
|
||||
expect_rc 0 bind_search "${URI}" "${CONFIG_ADMIN_DN}" -b cn=config -s base dn
|
||||
expect_rc 0 bind_search "${URI}" "${ADMIN_DN}" -b "${SERVICE_DN}" -s base userPassword
|
||||
expect_output '^userPassword:'
|
||||
ok 'administrator renames/deletes users and groups; data/config root access stays intact'
|
||||
Vendored
+7
@@ -15,15 +15,22 @@ trap 'chmod -R u+rwX "${test_dir:?}"; rm -rf "${test_dir:?}"' EXIT
|
||||
mkdir -p "${test_dir}/source/..revision" "${test_dir}/output" "${test_dir}/custom"
|
||||
printf 'dn: %s\n' "\${OPENLDAP_TEST_DN}" > "${test_dir}/source/..revision/10-entry.ldif.template"
|
||||
printf 'description: original\n' > "${test_dir}/source/..revision/20-plain.ldif"
|
||||
printf 'description: %s\n' "\${OPENLDAP_TEST_DN}" > "${test_dir}/source/..revision/15-optional.ldif.optional.template"
|
||||
ln -s ..revision "${test_dir}/source/..data"
|
||||
ln -s ..data/10-entry.ldif.template "${test_dir}/source/10-entry.ldif.template"
|
||||
ln -s ..data/20-plain.ldif "${test_dir}/source/20-plain.ldif"
|
||||
ln -s ..data/15-optional.ldif.optional.template "${test_dir}/source/15-optional.ldif.optional.template"
|
||||
chmod -R a-w "${test_dir}/source"
|
||||
export OPENLDAP_TEST_DN='dc=example,dc=org'
|
||||
|
||||
prepare_ldif_files "${test_dir}/source" "${test_dir}/output"
|
||||
test "$(cat "${test_dir}/output/10-entry.ldif")" = "dn: ${OPENLDAP_TEST_DN}"
|
||||
test "$(cat "${test_dir}/output/20-plain.ldif")" = 'description: original'
|
||||
test "$(cat "${test_dir}/output/15-optional.ldif.optional")" = "description: ${OPENLDAP_TEST_DN}"
|
||||
test ! -e "${test_dir}/output/15-optional.ldif"
|
||||
test -L "${test_dir}/source/15-optional.ldif.optional.template"
|
||||
test "$(cat "${test_dir}/source/15-optional.ldif.optional.template")" = "description: \${OPENLDAP_TEST_DN}"
|
||||
test "$(find "${test_dir}/output" -type f -name '*.ldif' | wc -l)" -eq 2
|
||||
test ! -e "${test_dir}/source/10-entry.ldif"
|
||||
test "$(cat "${test_dir}/source/10-entry.ldif.template")" = "dn: \${OPENLDAP_TEST_DN}"
|
||||
test -L "${test_dir}/source/10-entry.ldif.template"
|
||||
|
||||
@@ -31,6 +31,10 @@ OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_DN="ou=users,${OPENLDAP_BOOTSTRAP_SUFFIX}"
|
||||
OPENLDAP_BOOTSTRAP_DATA_TREE_GROUPS_DN="ou=groups,${OPENLDAP_BOOTSTRAP_SUFFIX}"
|
||||
OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN="ou=services,${OPENLDAP_BOOTSTRAP_SUFFIX}"
|
||||
|
||||
OPENLDAP_BOOTSTRAP_DATA_ADMIN=false
|
||||
OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN="cn=admin,${OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN}"
|
||||
OPENLDAP_BOOTSTRAP_DATA_ADMIN_PASSWORD_HASHED=""
|
||||
|
||||
OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT=false
|
||||
OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_DN="cn=management,${OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN}"
|
||||
OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT_PASSWORD_HASHED=""
|
||||
|
||||
@@ -6,6 +6,11 @@ Files with a `.template` extension use `${VARIABLE}` placeholders that are subst
|
||||
|
||||
Rendered LDIF files are assembled in version-sort order (`sort -V`); numerical prefixes control ordering.
|
||||
|
||||
Conditional templates use `.ldif.optional.template`. They render to `.ldif.optional`
|
||||
in the temporary output directory. The owning feature script activates them with
|
||||
`mv` to `.ldif` when needed; final assembly ignores files still ending in `.optional`.
|
||||
Sources stay unchanged, including on read-only volumes.
|
||||
|
||||
## Structure
|
||||
|
||||
```
|
||||
@@ -15,11 +20,11 @@ ldif/
|
||||
│ ├── tls/ # TLS settings (OPENLDAP_BOOTSTRAP_TLS=true)
|
||||
│ ├── tls-required/ # Enforce TLS (OPENLDAP_BOOTSTRAP_TLS_REQUIRED=true)
|
||||
│ ├── database-readonly/ # Read-only data DB user (OPENLDAP_BOOTSTRAP_DATA_READONLY=true)
|
||||
│ ├── database-admin/ # Scoped users/groups administrator (OPENLDAP_BOOTSTRAP_DATA_ADMIN=true)
|
||||
│ ├── database-management/ # Scoped application account ACLs (OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT=true)
|
||||
│ ├── monitor-readonly/ # Read-only monitor user (OPENLDAP_BOOTSTRAP_MONITOR_READONLY=true)
|
||||
│ ├── auditlog/ # Audit file (OPENLDAP_BOOTSTRAP_AUDITLOG=true)
|
||||
│ ├── ppolicy/ # Password policy overlay (OPENLDAP_BOOTSTRAP_PPOLICY=true)
|
||||
│ ├── ppolicy-readonly/ # Hide history from the data read-only account (PPOLICY and DATA_READONLY enabled)
|
||||
│ ├── refint/ # Referential integrity overlay (OPENLDAP_BOOTSTRAP_REFINT=true)
|
||||
│ ├── unique/ # Unique attributes overlay (OPENLDAP_BOOTSTRAP_UNIQUE=true)
|
||||
│ ├── memberof/ # Memberof overlay (OPENLDAP_BOOTSTRAP_MEMBEROF=true)
|
||||
@@ -28,6 +33,7 @@ ldif/
|
||||
└── data/ # LDAP directory data (populated on first bootstrap)
|
||||
├── base/ # Root organization entry (always applied)
|
||||
├── data-tree/ # Users, groups and services OUs (OPENLDAP_BOOTSTRAP_DATA_TREE=true)
|
||||
├── database-admin/ # Directory administrator (OPENLDAP_BOOTSTRAP_DATA_ADMIN=true)
|
||||
├── database-management/ # Application account (OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT=true)
|
||||
├── database-readonly/ # Read-only account entry (OPENLDAP_BOOTSTRAP_DATA_READONLY=true)
|
||||
├── monitor-readonly/ # Read-only monitor account (OPENLDAP_BOOTSTRAP_MONITOR_READONLY=true)
|
||||
@@ -79,6 +85,66 @@ Core OpenLDAP configuration applied on every bootstrap. Files are numbered to co
|
||||
|
||||
ACL files ending in `-close.ldif` append a final `access to * by * none` rule to deny any unmatched access, ensuring a safe default.
|
||||
|
||||
## Root, admin and management accounts
|
||||
|
||||
A DN identifies an LDAP account or entry. These accounts serve different purposes:
|
||||
|
||||
| Account | Default DN | Purpose and permissions |
|
||||
|---|---|---|
|
||||
| Data root | `cn=admin,<suffix>` | Maintenance superuser for the data database; bypasses that database's ACLs and administrative limits |
|
||||
| Config root | `cn=admin,cn=config` | Server configuration superuser; can change schemas, ACLs, root passwords and other settings in `cn=config` |
|
||||
| Directory admin | `cn=admin,ou=services,<suffix>` | Day-to-day user/group administration, for example through phpLDAPadmin; obeys the scoped ACLs |
|
||||
| Management | `cn=management,ou=services,<suffix>` | Application account, for example Authelia; reads users/groups and resets user passwords |
|
||||
|
||||
The two root DNs are configured through `OPENLDAP_BOOTSTRAP_DATA_ROOT_*` and
|
||||
`OPENLDAP_BOOTSTRAP_CONFIG_ROOT_*`. Each bypasses ACLs only in its own database:
|
||||
the data root is not automatically the configuration root. Config root can change
|
||||
the data database's ACLs and root password, so reserve it for server maintenance.
|
||||
Here, "root" means an LDAP superuser, not the Linux user running the container.
|
||||
|
||||
Root DNs can authenticate without a directory entry. The optional directory admin
|
||||
has a real entry so clients such as phpLDAPadmin 2 can retrieve it after login.
|
||||
Its DN and password are separate from the root credentials. Use it for routine
|
||||
administration and keep root credentials for maintenance/recovery.
|
||||
|
||||
## data/database-admin and config/database-admin
|
||||
|
||||
Enable the standard branches and scoped administrator on a new installation:
|
||||
|
||||
```bash
|
||||
OPENLDAP_BOOTSTRAP_DATA_TREE=true
|
||||
OPENLDAP_BOOTSTRAP_DATA_ADMIN=true
|
||||
OPENLDAP_BOOTSTRAP_DATA_ADMIN_PASSWORD_HASHED="{ARGON2}..."
|
||||
```
|
||||
|
||||
An empty hash generates a password and prints it in the bootstrap logs, as for
|
||||
the other accounts. `OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN` defaults to
|
||||
`cn=admin,${OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN}`; use a DN starting with `cn=`.
|
||||
Keep administrator and application accounts outside the writable users/groups
|
||||
branches, normally in the separate services branch.
|
||||
The parent OUs must exist: enable tree creation or supply custom LDIFs before
|
||||
`70-database-admin-account.ldif`. Enabling this option does not modify an existing
|
||||
database.
|
||||
|
||||
`62-database-admin-acl.ldif.template` lets the administrator create, read, edit,
|
||||
rename and delete entries in the configured users/groups branches, and reset user
|
||||
passwords. It denies password hash read/compare and access to `shadowLastChange`;
|
||||
with ppolicy, `61-database-admin-ppolicy-acl.ldif.optional.template` also denies access to
|
||||
`pwdHistory`. The administrator can read its own non-secret attributes, including
|
||||
its identifier, but cannot modify, rename, delete or reset its own account.
|
||||
Other service accounts, entries outside users/groups and `cn=config` stay protected.
|
||||
Use data root to rotate the directory administrator's password.
|
||||
|
||||
Password resets use the same `=wm` privileges as management resets. With ppolicy,
|
||||
these administrative resets bypass quality, history-reuse and minimum-age checks;
|
||||
the application performing them must enforce the required password rules.
|
||||
This remains a powerful identity-management account: it can reset users' passwords
|
||||
and change group memberships, but it cannot grant itself configuration access.
|
||||
|
||||
For phpLDAPadmin 2, configure DN login (`LDAP_LOGIN_ATTR=dn`) and accept this
|
||||
entry's object class (`LDAP_LOGIN_OBJECTCLASS=organizationalRole`). Configure its
|
||||
LDAP connection/search credentials and TLS separately.
|
||||
|
||||
## data/data-tree and data/database-management
|
||||
|
||||
`OPENLDAP_BOOTSTRAP_DATA_TREE=true` creates `ou=users`, `ou=groups` and `ou=services`
|
||||
@@ -106,14 +172,16 @@ minimum-age checks: enforce password requirements in the application.
|
||||
Service accounts and configuration remain protected; other identities keep their
|
||||
usual ACLs.
|
||||
|
||||
## config/ppolicy-readonly
|
||||
## config/database-readonly
|
||||
|
||||
`64-password-history-acl.ldif.template` denies `pwdHistory` access to the data
|
||||
`64-database-readonly-ppolicy-acl.ldif.optional.template` denies `pwdHistory` access to the data
|
||||
read-only account before its general read grant. It is loaded only when ppolicy
|
||||
and the data read-only account are both enabled. Its final `by * break` lets other
|
||||
and the data read-only account are both enabled, by `20-database-readonly.sh`.
|
||||
Its final `by * break` lets other
|
||||
identities follow the usual database ACLs, including administrator and replication
|
||||
access and any custom grants before the final deny-all. Replace this fragment
|
||||
through the custom config directory to change the read-only restriction.
|
||||
access and any custom grants before the final deny-all. Provide
|
||||
`64-database-readonly-ppolicy-acl.ldif` or its `.ldif.template` variant through
|
||||
the custom config directory to override the read-only restriction.
|
||||
|
||||
## data/ppolicy
|
||||
|
||||
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
olcAccess: to attrs=pwdHistory
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}" none
|
||||
by * break
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Let directory clients retrieve the administrator's entry after binding.
|
||||
olcAccess: to dn.base="${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}" attrs=userPassword,shadowLastChange
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}" none
|
||||
by * break
|
||||
olcAccess: to dn.base="${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}"
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}" read
|
||||
by * break
|
||||
olcAccess: to dn.subtree="${OPENLDAP_BOOTSTRAP_DATA_TREE_SERVICES_DN}"
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}" none
|
||||
by * break
|
||||
olcAccess: to dn.subtree="${OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_DN}" attrs=userPassword
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}" =wm
|
||||
by * break
|
||||
olcAccess: to attrs=userPassword,shadowLastChange
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}" none
|
||||
by * break
|
||||
olcAccess: to dn.subtree="${OPENLDAP_BOOTSTRAP_DATA_TREE_USERS_DN}"
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}" write
|
||||
by * break
|
||||
olcAccess: to dn.subtree="${OPENLDAP_BOOTSTRAP_DATA_TREE_GROUPS_DN}"
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}" write
|
||||
by * break
|
||||
olcAccess: to dn.base="${OPENLDAP_BOOTSTRAP_SUFFIX}" attrs=entry,objectClass,o,dc
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}" read
|
||||
by * break
|
||||
olcAccess: to *
|
||||
by dn.exact="${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}" none
|
||||
by * break
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
dn: ${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}
|
||||
objectClass: top
|
||||
objectClass: organizationalRole
|
||||
objectClass: simpleSecurityObject
|
||||
cn: ${OPENLDAP_BOOTSTRAP_DATA_ADMIN_CN}
|
||||
userPassword: ${OPENLDAP_BOOTSTRAP_DATA_ADMIN_PASSWORD_HASHED}
|
||||
@@ -33,6 +33,9 @@ if [ "${OPENLDAP_BOOTSTRAP_DATA_READONLY}" = "true" ]; then
|
||||
OUTPUT_DIR="$3"
|
||||
|
||||
prepare_ldif_files "${SOURCE_DIR}/database-readonly" "${OUTPUT_DIR}"
|
||||
if [ "${OPENLDAP_BOOTSTRAP_PPOLICY}" = "true" ] && [ -f "${OUTPUT_DIR}/64-database-readonly-ppolicy-acl.ldif.optional" ]; then
|
||||
mv "${OUTPUT_DIR}/64-database-readonly-ppolicy-acl.ldif.optional" "${OUTPUT_DIR}/64-database-readonly-ppolicy-acl.ldif"
|
||||
fi
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
#!/bin/bash
|
||||
# shellcheck disable=SC2034
|
||||
# shellcheck disable=SC1091
|
||||
# shellcheck disable=SC2153
|
||||
|
||||
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
|
||||
|
||||
if [ "${OPENLDAP_BOOTSTRAP_DATA_ADMIN}" = "true" ]; then
|
||||
|
||||
source "/container/services/openldap-bootstrap/assets/scripts/helpers/common.sh"
|
||||
|
||||
check_args "$@"
|
||||
|
||||
TYPE="$1"
|
||||
|
||||
if [ "${TYPE}" = "env" ]; then
|
||||
ENV_FILE="$2"
|
||||
|
||||
source "/container/services/openldap-bootstrap/assets/scripts/helpers/password.sh"
|
||||
|
||||
OPENLDAP_BOOTSTRAP_DATA_ADMIN_CN=$(get_dn_attr cn "${OPENLDAP_BOOTSTRAP_DATA_ADMIN_DN}")
|
||||
|
||||
ensure_hashed_password OPENLDAP_BOOTSTRAP_DATA_ADMIN_PASSWORD_HASHED "database admin user"
|
||||
|
||||
prepare_env_file "${ENV_FILE}" OPENLDAP_BOOTSTRAP_DATA_ADMIN_CN OPENLDAP_BOOTSTRAP_DATA_ADMIN_PASSWORD_HASHED
|
||||
|
||||
elif [ "${TYPE}" = "ldif" ]; then
|
||||
SOURCE_DIR="$2"
|
||||
OUTPUT_DIR="$3"
|
||||
|
||||
prepare_ldif_files "${SOURCE_DIR}/database-admin" "${OUTPUT_DIR}"
|
||||
if [ "${OPENLDAP_BOOTSTRAP_PPOLICY}" = "true" ] && [ -f "${OUTPUT_DIR}/61-database-admin-ppolicy-acl.ldif.optional" ]; then
|
||||
mv "${OUTPUT_DIR}/61-database-admin-ppolicy-acl.ldif.optional" "${OUTPUT_DIR}/61-database-admin-ppolicy-acl.ldif"
|
||||
fi
|
||||
fi
|
||||
|
||||
fi
|
||||
@@ -62,9 +62,6 @@ if [ "${OPENLDAP_BOOTSTRAP_PPOLICY}" = "true" ]; then
|
||||
OUTPUT_DIR="$3"
|
||||
|
||||
prepare_ldif_files "${SOURCE_DIR}/ppolicy" "${OUTPUT_DIR}"
|
||||
if [ "${OPENLDAP_BOOTSTRAP_DATA_READONLY}" = "true" ]; then
|
||||
prepare_ldif_files "${SOURCE_DIR}/ppolicy-readonly" "${OUTPUT_DIR}"
|
||||
fi
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
@@ -15,13 +15,14 @@ Scripts are executed in numeric order. Most are feature-gated by an environment
|
||||
| `05-data-tree.sh` | `OPENLDAP_BOOTSTRAP_DATA_TREE=true` | Creates the users, groups and services OUs |
|
||||
| `10-tls.sh` | `OPENLDAP_BOOTSTRAP_TLS=true` | Applies TLS certificate and protocol configuration |
|
||||
| `15-tls-required.sh` | `OPENLDAP_BOOTSTRAP_TLS_REQUIRED=true` | Enforces minimum 128-bit SSF (warns if TLS is not also enabled) |
|
||||
| `20-database-readonly.sh` | `OPENLDAP_BOOTSTRAP_DATA_READONLY=true` | Creates a read-only service account for the data database |
|
||||
| `20-database-readonly.sh` | `OPENLDAP_BOOTSTRAP_DATA_READONLY=true` | Creates a read-only service account for the data database and hides password history when ppolicy is enabled |
|
||||
| `23-database-admin.sh` | `OPENLDAP_BOOTSTRAP_DATA_ADMIN=true` | Creates a scoped users/groups administrator, protecting services, password hashes/history and configuration |
|
||||
| `25-database-management.sh` | `OPENLDAP_BOOTSTRAP_DATA_MANAGEMENT=true` | Creates one account below the services branch, reading users/groups and resetting user passwords with configurable hidden attributes and no other write permissions |
|
||||
| `30-monitor-readonly.sh` | `OPENLDAP_BOOTSTRAP_MONITOR_READONLY=true` | Creates a read-only service account for the monitor database |
|
||||
| `35-auditlog.sh` | `OPENLDAP_BOOTSTRAP_AUDITLOG=true` | Configures file-based auditing on the data database |
|
||||
| `40-refint.sh` | `OPENLDAP_BOOTSTRAP_REFINT=true` | Configures the referential integrity overlay and optional replacement DN |
|
||||
| `45-refint-nothing.sh` | `OPENLDAP_BOOTSTRAP_REFINT=true` and `OPENLDAP_BOOTSTRAP_REFINT_NOTHING_DN` non-empty | Creates the passwordless replacement role |
|
||||
| `50-ppolicy.sh` | `OPENLDAP_BOOTSTRAP_PPOLICY=true` | Configures password policy, hides history from the data read-only account while preserving normal ACL evaluation, and validates inactivity and progressive lockout prerequisites |
|
||||
| `50-ppolicy.sh` | `OPENLDAP_BOOTSTRAP_PPOLICY=true` | Configures password policy and validates inactivity and progressive lockout prerequisites |
|
||||
| `60-unique.sh` | `OPENLDAP_BOOTSTRAP_UNIQUE=true` | Configures the unique attributes overlay |
|
||||
| `70-memberof.sh` | `OPENLDAP_BOOTSTRAP_MEMBEROF=true` | Configures the memberof overlay |
|
||||
| `80-replication.sh` | `OPENLDAP_BOOTSTRAP_REPLICATION=true` | Sets up multi-provider syncrepl replication (requires 2 to 9 hosts) |
|
||||
@@ -37,7 +38,7 @@ Utility functions shared across the bootstrap scripts.
|
||||
|----------|-------------|
|
||||
| `check_args` | Validates that a script was called with the expected operation (`env`/`ldif`) and argument count |
|
||||
| `prepare_env_file` | Appends exported variable assignments (safely shell-quoted) to an env file |
|
||||
| `prepare_ldif_files` | Copies sources to a temporary writable directory (dereferencing symlinks), renders `.ldif.template` files via `container envsubst templates`, then copies all resulting `.ldif` files (sorted) to the output directory. Sources can be mounted read-only. The function removes its working copy on success; the bootstrap's global EXIT trap cleans up on failure. |
|
||||
| `prepare_ldif_files` | Copies sources to a temporary writable directory (dereferencing symlinks), renders templates via `container envsubst templates`, then copies the resulting `.ldif` and `.ldif.optional` files (sorted) to the output directory. Feature scripts activate optional fragments with `mv`; the helper does not select them. Sources can be mounted read-only. The function removes its working copy on success; the bootstrap's global EXIT trap cleans up on failure. |
|
||||
| `get_dn_attr` | Extracts the value of a named attribute from a DN string (e.g. `cn` from `cn=admin,dc=example,dc=com`) |
|
||||
|
||||
### `helpers/password.sh`
|
||||
@@ -52,5 +53,5 @@ Utility functions shared across the bootstrap scripts.
|
||||
|
||||
1. Create a numbered script (e.g. `55-myfeature.sh`) that accepts `env` and `ldif` arguments.
|
||||
2. Guard execution with the appropriate environment variable check.
|
||||
3. Add the corresponding LDIF templates under `../ldif/config/` and/or `../ldif/data/`.
|
||||
3. Add LDIF templates under matching feature directories in `../ldif/config/` and/or `../ldif/data/`: `55-myfeature.sh` loads `myfeature/`, with filenames such as `NN-myfeature-description.ldif.template`. Name conditional templates `.ldif.optional.template` and activate their rendered `.ldif.optional` files with `mv` in the feature script when needed. Unactivated fragments are ignored during final assembly.
|
||||
4. Source `helpers/common.sh` and `helpers/password.sh` as needed.
|
||||
|
||||
@@ -56,7 +56,7 @@ prepare_ldif_files() {
|
||||
|
||||
while IFS= read -r file; do
|
||||
cp -frav "${file}" "${output_dir}" 2>&1 | container log debug
|
||||
done < <(find "${work_dir}" -type f -name "*.ldif" | sort -V)
|
||||
done < <(find "${work_dir}" -type f \( -name "*.ldif" -o -name "*.ldif.optional" \) | sort -V)
|
||||
|
||||
rm -rf "${work_dir:?}"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user