Files
container-images/database/ldap2mariadb/sync-roles.sh
T
akritikos 5744f4b7db ✨ Commits existing images to git
- Implemented sync-roles.sh to synchronize Active Directory group memberships as MariaDB roles and user accounts.
- Created Containerfile for building a custom ldap2pg image with patched YAML decoding.
- Added provision-user-db.sh for provisioning user databases and roles based on LDAP synchronization.
- Introduced a new Containerfile for MariaDB with PAM authentication via pam_ldap.
- Configured PAM service for MariaDB logins and added necessary plugins for LDAP authentication.
- Implemented health checks and entrypoint scripts for MariaDB and pgpool containers.
- Enhanced PostgreSQL image with additional extensions and custom pg_hba.conf for improved security and functionality.
2026-08-15 19:57:25 +03:00

118 lines
5.2 KiB
Bash

#!/usr/bin/env bash
set -euo pipefail
# ─────────────────────────────────────────────────────────────────────────────
# sync-roles.sh — the ldap2pg analogue for MariaDB.
#
# There is no ldap2pg for MariaDB, so this reproduces the parts of ldap2pg.yaml
# we rely on: read AD group membership and materialise it as MariaDB roles plus
# per-user login accounts. Human accounts authenticate via PAM -> AD (no
# password is stored in MariaDB); the anchor roles carry the actual privileges,
# which provision-user-db.sh grants per database.
#
# Model (mirrors ldap2pg.yaml):
# Global groups under OU=Global,OU=Database,OU=Groups:
# owners | readers | writers | ddl -> members get the matching role
# Per-DB groups under OU=Database,OU=Groups:
# <db>_readers | <db>_writers | <db>_ddl -> scoped roles
#
# Those groups are engine-agnostic: they say WHAT privileges a member gets, not
# on which server. ENGINE_GROUP_DN is the orthogonal gate — a member is only
# provisioned here if they are also in that group, which is what keeps the
# shared role groups from creating MariaDB accounts for Postgres-only users.
#
# Connection: MYSQL_HOST/PORT/USER + MYSQL_PWD (read from the environment by the
# mariadb client, so the password never appears on the command line). Point this
# at the backend directly, NOT MaxScale — same as ldap2pg talking to the PG
# primary rather than through pgpool.
# ─────────────────────────────────────────────────────────────────────────────
: "${LDAPURI:?}"; : "${LDAPBINDDN:?}"; : "${LDAPPASSWORD:?}"
: "${GLOBAL_BASE:?}"
: "${PERDB_BASE:?}"
# Set but allowed to be empty: empty disables the engine gate entirely.
: "${ENGINE_GROUP_DN?}"
# LDAP_MATCHING_RULE_IN_CHAIN: matches through nested groups, so the engine gate
# can hold org groups (Daemons, DBAs) instead of only individual users.
ENGINE_FILTER=""
if [ -n "$ENGINE_GROUP_DN" ]; then
ENGINE_FILTER="(memberOf:1.2.840.113556.1.4.1941:=${ENGINE_GROUP_DN})"
fi
: "${MYSQL_HOST:?}"; : "${MYSQL_PORT:?}"; : "${MYSQL_USER:?}"
MARIADB=(mariadb --host "${MYSQL_HOST}" --port "${MYSQL_PORT}"
--user "${MYSQL_USER}" --batch --skip-column-names)
sql() { "${MARIADB[@]}" -e "$1"; }
# Escaping is context-dependent: in a '...' literal a backslash escapes the next
# character (default sql_mode), while in a `...` identifier it is literal.
esc_str() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e "s/'/''/g"; }
esc_id() { printf '%s' "$1" | sed 's/`/``/g'; }
# Emit the sAMAccountName of every direct user member of the named AD group.
group_members() {
local base="$1" group="$2" dns
dns="$(ldapsearch -LLL -o ldif-wrap=no -H "$LDAPURI" \
-D "$LDAPBINDDN" -w "$LDAPPASSWORD" \
-b "$base" -s one "(&(objectClass=group)(cn=${group}))" member \
| sed -n 's/^member: //p')"
[ -z "$dns" ] && return 0
while IFS= read -r dn; do
[ -z "$dn" ] && continue
ldapsearch -LLL -o ldif-wrap=no -H "$LDAPURI" -D "$LDAPBINDDN" -w "$LDAPPASSWORD" \
-b "$dn" -s base \
"(&(objectCategory=person)(objectClass=user)(!(objectClass=computer))${ENGINE_FILTER})" \
sAMAccountName 2>/dev/null | sed -n 's/^sAMAccountName: //p'
done <<< "$dns"
}
ensure_role() {
local role; role="$(esc_id "$1")"
sql "CREATE ROLE IF NOT EXISTS \`${role}\`;"
}
ensure_user_in_role() {
local user role
user="$(esc_str "$1")"; role="$(esc_id "$2")"
# PAM login account: the password is verified against AD, nothing is stored.
sql "CREATE USER IF NOT EXISTS '${user}'@'%' IDENTIFIED VIA pam USING 'mariadb';"
sql "GRANT \`${role}\` TO '${user}'@'%';"
# MariaDB roles are inactive until selected, and it allows only ONE default
# role per user (no ALL, unlike MySQL) — make this role active at login.
sql "SET DEFAULT ROLE \`${role}\` FOR '${user}'@'%';"
}
echo "[*] Ensuring anchor roles"
for r in owners readers writers ddl; do ensure_role "$r"; done
echo "[*] Syncing global AD groups -> roles"
for grp in owners readers writers ddl; do
while IFS= read -r sam; do
[ -z "$sam" ] && continue
sam="$(printf '%s' "$sam" | tr '[:upper:]' '[:lower:]')"
echo " - ${sam} -> ${grp}"
ensure_user_in_role "$sam" "$grp"
done < <(group_members "$GLOBAL_BASE" "$grp")
done
echo "[*] Syncing per-database AD groups -> scoped roles"
perdb_groups="$(ldapsearch -LLL -o ldif-wrap=no -H "$LDAPURI" \
-D "$LDAPBINDDN" -w "$LDAPPASSWORD" \
-b "$PERDB_BASE" -s one \
'(&(objectClass=group)(|(cn=*_readers)(cn=*_writers)(cn=*_ddl)))' cn \
| sed -n 's/^cn: //p')"
while IFS= read -r grp; do
[ -z "$grp" ] && continue
role="$(printf '%s' "$grp" | tr '[:upper:]' '[:lower:]')"
ensure_role "$role"
while IFS= read -r sam; do
[ -z "$sam" ] && continue
sam="$(printf '%s' "$sam" | tr '[:upper:]' '[:lower:]')"
echo " - ${sam} -> ${role}"
ensure_user_in_role "$sam" "$role"
done < <(group_members "$PERDB_BASE" "$grp")
done <<< "$perdb_groups"
echo "[*] Role sync complete."