Import postgresql plugin

This commit is contained in:
Marek Miklewicz
2026-07-04 16:00:04 +02:00
commit ddc971567f
74 changed files with 18735 additions and 0 deletions
+636
View File
@@ -0,0 +1,636 @@
#!/bin/bash
set -euo pipefail
CREDS_FILE="/usr/local/directadmin/conf/postgresql.conf"
PLUGIN_ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
PLUGIN_SETTINGS_FILE="$PLUGIN_ROOT/plugin-settings.conf"
PGHOST_VAL=""
PGPORT_VAL="5432"
PGUSER_VAL="postgres"
PGPASSWORD_VAL=""
PG_VERSION_NUM=""
ALLOW_REMOTE_HOSTS="1"
RESTART_POSTGRESQL="0"
HBA_MANAGED_BLOCK_BEGIN="# DA_POSTGRESQL_PLUGIN_HBA_BEGIN"
HBA_MANAGED_BLOCK_END="# DA_POSTGRESQL_PLUGIN_HBA_END"
POSTGRESQL_CONF=""
HBA_FILE=""
CSF_ALLOW_FILE="/etc/csf/csf.allow"
CSF_INCLUDE_FILE="/etc/csf.allow.postgres"
PG_CONF_MARKER_BEGIN="# DA_POSTGRESQL_PLUGIN_BEGIN"
PG_CONF_MARKER_END="# DA_POSTGRESQL_PLUGIN_END"
declare -A REMOTE_IP_NOTES=()
parse_args() {
while [ "$#" -gt 0 ]; do
case "$1" in
--restart-postgresql)
RESTART_POSTGRESQL="1"
;;
*)
echo "Nieznany argument: $1" >&2
exit 2
;;
esac
shift
done
}
find_psql() {
if command -v psql >/dev/null 2>&1; then
command -v psql
return
fi
local candidate
candidate="$(ls -1d /usr/pgsql-*/bin/psql 2>/dev/null | sort -V | tail -n1 || true)"
if [ -n "$candidate" ] && [ -x "$candidate" ]; then
echo "$candidate"
return
fi
echo ""
}
trim() {
local value="$1"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
printf '%s' "$value"
}
strip_inline_comment() {
local value="$1"
value="${value%%#*}"
trim "$value"
}
unquote() {
local value
value="$(trim "$1")"
if [ "${#value}" -ge 2 ] && [ "${value:0:1}" = "'" ] && [ "${value: -1}" = "'" ]; then
value="${value:1:${#value}-2}"
elif [ "${#value}" -ge 2 ] && [ "${value:0:1}" = '"' ] && [ "${value: -1}" = '"' ]; then
value="${value:1:${#value}-2}"
fi
printf '%s' "$value"
}
bool_to_01() {
local raw
raw="$(echo "$1" | tr '[:upper:]' '[:lower:]')"
raw="$(trim "$raw")"
case "$raw" in
1|true|yes|y|on|checked)
echo "1"
;;
*)
echo "0"
;;
esac
}
is_ipv4_strict() {
local ip="$1"
if [[ ! "$ip" =~ ^([0-9]{1,3}\.){3}[0-9]{1,3}$ ]]; then
return 1
fi
local IFS='.'
local o1 o2 o3 o4
read -r o1 o2 o3 o4 <<< "$ip"
for octet in "$o1" "$o2" "$o3" "$o4"; do
if [ "$octet" -gt 255 ]; then
return 1
fi
if [[ "$octet" =~ ^0[0-9]+$ ]]; then
return 1
fi
done
return 0
}
is_ipv4_cidr_strict() {
local cidr="$1"
if [[ ! "$cidr" =~ ^([^/]+)/([0-9]{1,2})$ ]]; then
return 1
fi
local ip="${BASH_REMATCH[1]}"
local prefix="${BASH_REMATCH[2]}"
is_ipv4_strict "$ip" || return 1
[ "$prefix" -ge 0 ] && [ "$prefix" -le 32 ]
}
skip_csf_remote_pattern() {
[ "$1" = "0.0.0.0/0" ]
}
sanitize_note() {
local note="$1"
note="${note//$'\r'/ }"
note="${note//$'\n'/ }"
note="${note//$'\t'/ }"
note="${note//#/ }"
note="$(echo "$note" | sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//')"
note="${note:0:180}"
printf '%s' "$note"
}
load_plugin_settings() {
local plain_allow=""
local legacy_allow=""
if [ ! -r "$PLUGIN_SETTINGS_FILE" ]; then
ALLOW_REMOTE_HOSTS="1"
return
fi
while IFS= read -r raw_line || [ -n "$raw_line" ]; do
raw_line="$(trim "$raw_line")"
[ -n "$raw_line" ] || continue
[[ "$raw_line" =~ ^# ]] && continue
[[ "$raw_line" == *"="* ]] || continue
local key="${raw_line%%=*}"
local value="${raw_line#*=}"
key="$(trim "$key")"
value="$(unquote "$(strip_inline_comment "$value")")"
case "$key" in
allow_remote_hosts)
plain_allow="$value"
;;
PG_PLUGIN_ALLOW_REMOTE_HOSTS)
legacy_allow="$value"
;;
esac
done < "$PLUGIN_SETTINGS_FILE"
if [ -n "$plain_allow" ]; then
ALLOW_REMOTE_HOSTS="$(bool_to_01 "$plain_allow")"
elif [ -n "$legacy_allow" ]; then
ALLOW_REMOTE_HOSTS="$(bool_to_01 "$legacy_allow")"
else
ALLOW_REMOTE_HOSTS="1"
fi
}
load_credentials() {
[ -r "$CREDS_FILE" ] || {
echo "Brak pliku poświadczeń: $CREDS_FILE" >&2
exit 1
}
local line
line="$(grep -Ev '^[[:space:]]*($|#)' "$CREDS_FILE" | head -n1 || true)"
[ -n "$line" ] || {
echo "Pusty plik poświadczeń: $CREDS_FILE" >&2
exit 1
}
if [[ "$line" == *=* ]]; then
while IFS='=' read -r key value; do
key="$(echo "$key" | tr -d '[:space:]' | tr '[:lower:]' '[:upper:]')"
value="$(trim "$value")"
value="$(unquote "$value")"
case "$key" in
PG_HOST) PGHOST_VAL="$value" ;;
PG_PORT) PGPORT_VAL="$value" ;;
PG_USER) PGUSER_VAL="$value" ;;
PG_PASSWORD) PGPASSWORD_VAL="$value" ;;
esac
done < <(grep -Ev '^[[:space:]]*($|#)' "$CREDS_FILE")
else
local rest dbname
PGHOST_VAL="${line%%:*}"
rest="${line#*:}"
PGPORT_VAL="${rest%%:*}"
rest="${rest#*:}"
dbname="${rest%%:*}"
rest="${rest#*:}"
PGUSER_VAL="${rest%%:*}"
PGPASSWORD_VAL="${rest#*:}"
[ -n "$dbname" ] || dbname="postgres"
fi
[ -n "$PGHOST_VAL" ] || PGHOST_VAL="localhost"
[ -n "$PGPORT_VAL" ] || PGPORT_VAL="5432"
[ -n "$PGUSER_VAL" ] || PGUSER_VAL="postgres"
[ -n "$PGPASSWORD_VAL" ] || {
echo "Brak hasła PostgreSQL w $CREDS_FILE" >&2
exit 1
}
}
psql_exec() {
PGPASSWORD="$PGPASSWORD_VAL" "$PSQL_BIN" -h "$PGHOST_VAL" -p "$PGPORT_VAL" -U "$PGUSER_VAL" -d postgres -v ON_ERROR_STOP=1 -At -F $'\t' "$@"
}
load_server_version_num() {
PG_VERSION_NUM="$(psql_exec -c 'SHOW server_version_num;' | tr -d '[:space:]')"
[[ "$PG_VERSION_NUM" =~ ^[0-9]+$ ]] || {
echo "Nieprawidłowy server_version_num: ${PG_VERSION_NUM}" >&2
exit 1
}
}
cleanup_hba_managed_entries() {
local hba_file="$1"
local tmp_file
tmp_file="$(mktemp)"
awk -v begin="$HBA_MANAGED_BLOCK_BEGIN" -v end="$HBA_MANAGED_BLOCK_END" '
$0 == begin {skip=1; next}
$0 == end {skip=0; next}
skip == 1 {next}
{
lower = tolower($0)
if (lower ~ /^[[:space:]]*include([[:space:]]|_if_exists)/ && lower ~ /pg_hba_da_plugin\.conf/) {
next
}
print
}
' "$hba_file" > "$tmp_file"
install -m 600 "$tmp_file" "$hba_file"
chown postgres:postgres "$hba_file" 2>/dev/null || true
rm -f "$tmp_file"
}
write_hba_inline_block() {
local hba_file="$1"
local include_file="$2"
local tmp_file
tmp_file="$(mktemp)"
cp -p "$hba_file" "${hba_file}.bak.$(date +%s)"
cat "$hba_file" > "$tmp_file"
{
echo ""
echo "$HBA_MANAGED_BLOCK_BEGIN"
echo "# Managed by DirectAdmin PostgreSQL plugin (inline mode)."
cat "$include_file"
echo "$HBA_MANAGED_BLOCK_END"
} >> "$tmp_file"
install -m 600 "$tmp_file" "$hba_file"
chown postgres:postgres "$hba_file" 2>/dev/null || true
rm -f "$tmp_file"
}
append_remote_ip_note() {
local ip="$1"
local note="$2"
if [ -z "${REMOTE_IP_NOTES[$ip]+x}" ]; then
REMOTE_IP_NOTES["$ip"]="$note"
return
fi
if [ -z "$note" ]; then
return
fi
local existing="${REMOTE_IP_NOTES[$ip]}"
if [ -z "$existing" ]; then
REMOTE_IP_NOTES["$ip"]="$note"
return
fi
if [[ "$existing" != *"$note"* ]]; then
REMOTE_IP_NOTES["$ip"]="$existing | $note"
fi
}
expand_hba_addresses() {
local host="$1"
case "$host" in
localhost)
echo "127.0.0.1/32"
echo "::1/128"
return
;;
127.0.0.1)
echo "127.0.0.1/32"
return
;;
::1)
echo "::1/128"
return
;;
esac
if is_ipv4_strict "$host"; then
echo "$host/32"
return
fi
if is_ipv4_cidr_strict "$host"; then
echo "$host"
fi
}
build_hba_include() {
local include_file="$1"
local tmp_file
tmp_file="$(mktemp)"
{
echo "# Auto-generated by DirectAdmin PostgreSQL plugin"
echo "# Generated at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
echo "# Method: scram-sha-256"
} > "$tmp_file"
local sql_query
sql_query="SELECT d.datname, h.role_name, h.host_pattern, COALESCE(h.note, '')
FROM da_plugin.access_hosts h
JOIN pg_roles r ON r.rolname = h.role_name
JOIN pg_database d
ON d.datistemplate = false
AND (
(COALESCE(h.db_name, '') <> '' AND d.datname = h.db_name)
OR
(COALESCE(h.db_name, '') = '' AND left(d.datname, length(h.da_user) + 1) = h.da_user || '_')
)
WHERE COALESCE(h.db_name, '') <> ''
OR has_database_privilege(h.role_name, d.datname, 'CONNECT')
ORDER BY d.datname, h.role_name, h.host_pattern;"
local query_output
query_output="$(psql_exec -c "$sql_query")" || {
local status=$?
rm -f "$tmp_file"
echo "Nie udało się odczytać rekordów hostów z da_plugin.access_hosts." >&2
return "$status"
}
local access_host_rows="0"
local generated_rules="0"
while IFS=$'\t' read -r db role host note; do
[ -n "$db" ] || continue
[ -n "$role" ] || continue
[ -n "$host" ] || continue
access_host_rows=$((access_host_rows + 1))
local host_note
host_note="$(sanitize_note "$note")"
local include_host="0"
local is_remote="0"
case "$host" in
localhost|127.0.0.1|::1)
include_host="1"
;;
*)
if [ "$ALLOW_REMOTE_HOSTS" = "1" ] && { is_ipv4_strict "$host" || is_ipv4_cidr_strict "$host"; }; then
include_host="1"
is_remote="1"
fi
;;
esac
[ "$include_host" = "1" ] || continue
if [ "$is_remote" = "1" ]; then
append_remote_ip_note "$host" "$host_note"
fi
local note_label="$host_note"
[ -n "$note_label" ] || note_label="(brak komentarza)"
printf '# role=%s db=%s host=%s comment=%s\n' "$role" "$db" "$host" "$note_label" >> "$tmp_file"
while IFS= read -r addr; do
[ -n "$addr" ] || continue
printf 'host\t%s\t%s\t%s\tscram-sha-256\n' "$db" "$role" "$addr" >> "$tmp_file"
generated_rules=$((generated_rules + 1))
done < <(expand_hba_addresses "$host")
done <<< "$query_output"
install -m 600 "$tmp_file" "$include_file"
chown postgres:postgres "$include_file" 2>/dev/null || true
rm -f "$tmp_file"
echo "Read access host rows: $access_host_rows"
echo "Generated pg_hba host rules: $generated_rules"
}
sorted_remote_ips() {
if [ "${#REMOTE_IP_NOTES[@]}" -eq 0 ]; then
return 0
fi
printf '%s\n' "${!REMOTE_IP_NOTES[@]}" | sort -V
}
write_postgresql_conf_block() {
local conf_file="$1"
local listen_value="localhost"
local has_remote_ips="0"
if [ "$ALLOW_REMOTE_HOSTS" = "1" ]; then
listen_value="*"
fi
if [ "$ALLOW_REMOTE_HOSTS" = "1" ] && [ "${#REMOTE_IP_NOTES[@]}" -gt 0 ]; then
has_remote_ips="1"
fi
local tmp_file
tmp_file="$(mktemp)"
awk -v begin="$PG_CONF_MARKER_BEGIN" -v end="$PG_CONF_MARKER_END" '
$0 == begin {skip=1; next}
$0 == end {skip=0; next}
skip != 1 {print}
' "$conf_file" > "$tmp_file"
{
echo ""
echo "$PG_CONF_MARKER_BEGIN"
echo "# Managed by DirectAdmin PostgreSQL plugin."
echo "listen_addresses = '$listen_value'"
echo "# Zdefiniowane hosty IPv4 z pluginu:"
if [ "$has_remote_ips" = "1" ]; then
while IFS= read -r ip; do
[ -n "$ip" ] || continue
local note="${REMOTE_IP_NOTES[$ip]}"
if [ -n "$note" ]; then
printf '# %s ; %s\n' "$ip" "$note"
else
printf '# %s\n' "$ip"
fi
done < <(sorted_remote_ips)
else
echo "# brak zdalnych hostów"
fi
echo "$PG_CONF_MARKER_END"
} >> "$tmp_file"
install -m 600 "$tmp_file" "$conf_file"
chown postgres:postgres "$conf_file" 2>/dev/null || true
rm -f "$tmp_file"
}
ensure_csf_include_directive() {
local allow_file="$1"
local include_file="$2"
[ -f "$allow_file" ] || return 1
local include_line="Include $include_file"
if ! grep -Fxq "$include_line" "$allow_file"; then
echo "$include_line" >> "$allow_file"
fi
return 0
}
write_csf_include_file() {
local include_file="$1"
local tmp_file
tmp_file="$(mktemp)"
{
echo "# Auto-generated by DirectAdmin PostgreSQL plugin"
echo "# Generated at: $(date -u +'%Y-%m-%d %H:%M:%S UTC')"
} > "$tmp_file"
if [ "$ALLOW_REMOTE_HOSTS" = "1" ] && [ "${#REMOTE_IP_NOTES[@]}" -gt 0 ]; then
while IFS= read -r ip; do
[ -n "$ip" ] || continue
if skip_csf_remote_pattern "$ip"; then
continue
fi
local note="${REMOTE_IP_NOTES[$ip]}"
if [ -n "$note" ]; then
printf '%s # DA PostgreSQL: %s\n' "$ip" "$note" >> "$tmp_file"
else
printf '%s # DA PostgreSQL\n' "$ip" >> "$tmp_file"
fi
done < <(sorted_remote_ips)
fi
install -m 600 "$tmp_file" "$include_file"
chown root:root "$include_file" 2>/dev/null || true
rm -f "$tmp_file"
}
reload_postgres_conf() {
psql_exec -c 'SELECT pg_reload_conf();' >/dev/null
}
restart_postgresql_service() {
if ! command -v systemctl >/dev/null 2>&1; then
echo "Nie znaleziono systemctl, nie można zrestartować PostgreSQL." >&2
return 1
fi
local major=""
if [[ "$PG_VERSION_NUM" =~ ^[0-9]+$ ]] && [ "$PG_VERSION_NUM" -ge 100000 ]; then
major="$((PG_VERSION_NUM / 10000))"
fi
local candidates=()
if [ -n "$major" ]; then
candidates+=("postgresql-${major}" "postgresql@${major}-main")
fi
candidates+=("postgresql")
local service
for service in "${candidates[@]}"; do
if systemctl list-unit-files --no-legend "${service}.service" 2>/dev/null | grep -q . || systemctl status "$service" >/dev/null 2>&1; then
if systemctl restart "$service"; then
echo "Restarted PostgreSQL service: $service"
return 0
fi
fi
done
echo "Nie znaleziono usługi PostgreSQL do restartu. Próbowano: ${candidates[*]}" >&2
return 1
}
reload_csf_if_available() {
if command -v csf >/dev/null 2>&1; then
csf -r >/dev/null 2>&1 || true
fi
}
parse_args "$@"
load_plugin_settings
load_credentials
PSQL_BIN="$(find_psql)"
[ -n "$PSQL_BIN" ] || {
echo "Nie znaleziono binarki psql" >&2
exit 1
}
HBA_FILE="$(psql_exec -c 'SHOW hba_file;')"
[ -n "$HBA_FILE" ] || {
echo "Nie udało się odczytać ścieżki do pg_hba.conf" >&2
exit 1
}
POSTGRESQL_CONF="$(psql_exec -c 'SHOW config_file;')"
[ -n "$POSTGRESQL_CONF" ] || {
echo "Nie udało się odczytać ścieżki do postgresql.conf" >&2
exit 1
}
load_server_version_num
HBA_DIR="$(dirname "$HBA_FILE")"
HBA_INCLUDE_FILE="$HBA_DIR/pg_hba_da_plugin.conf"
build_hba_include "$HBA_INCLUDE_FILE"
cleanup_hba_managed_entries "$HBA_FILE"
write_hba_inline_block "$HBA_FILE" "$HBA_INCLUDE_FILE"
CSF_SYNCED="0"
CSF_SKIP_REASON=""
if [ "$ALLOW_REMOTE_HOSTS" = "1" ]; then
write_postgresql_conf_block "$POSTGRESQL_CONF"
if ensure_csf_include_directive "$CSF_ALLOW_FILE" "$CSF_INCLUDE_FILE"; then
write_csf_include_file "$CSF_INCLUDE_FILE"
reload_csf_if_available
CSF_SYNCED="1"
else
CSF_SKIP_REASON="CSF allow file not found: $CSF_ALLOW_FILE"
echo "Ostrzeżenie: nie znaleziono $CSF_ALLOW_FILE, pomijam aktualizację CSF." >&2
fi
fi
reload_postgres_conf
if [ "$RESTART_POSTGRESQL" = "1" ]; then
restart_postgresql_service
fi
echo "Synchronized pg_hba inline block in: $HBA_FILE"
echo "Generated pg_hba include file (diagnostic copy): $HBA_INCLUDE_FILE"
if [ "$ALLOW_REMOTE_HOSTS" = "1" ]; then
echo "Synchronized postgresql.conf: $POSTGRESQL_CONF"
echo "PostgreSQL restart required for listen_addresses changes"
if [ "$CSF_SYNCED" = "1" ]; then
echo "Synchronized CSF include: $CSF_INCLUDE_FILE"
else
echo "Skipped CSF synchronization: ${CSF_SKIP_REASON:-CSF allow file unavailable}"
fi
else
echo "Remote hosts disabled: skipped postgresql.conf and CSF synchronization"
fi