diff --git a/cdist/conf/type/__postgres_role/explorer/state b/cdist/conf/type/__postgres_role/explorer/state
index c8e1fa9d..34069de9 100755
--- a/cdist/conf/type/__postgres_role/explorer/state
+++ b/cdist/conf/type/__postgres_role/explorer/state
@@ -1,6 +1,7 @@
-#!/bin/sh
+#!/bin/sh -e
#
# 2011 Steven Armstrong (steven-cdist at armstrong.cc)
+# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
#
# This file is part of cdist.
#
@@ -11,32 +12,140 @@
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see .
#
-case "$("${__explorer}/os")"
+case $("${__explorer:?}/os")
in
- netbsd)
- postgres_user='pgsql'
- ;;
- openbsd)
- postgres_user='_postgresql'
- ;;
- *)
- postgres_user='postgres'
- ;;
+ (netbsd)
+ postgres_user='pgsql'
+ ;;
+ (openbsd)
+ postgres_user='_postgresql'
+ ;;
+ (*)
+ postgres_user='postgres'
+ ;;
esac
+rolename=${__object_id:?}
-name="$__object_id"
-if test -n "$(su - "$postgres_user" -c "psql postgres -twAc \"SELECT 1 FROM pg_roles WHERE rolname='$name'\"")"
+psql_query() {
+ su -l "${postgres_user}" -c "$(
+ printf "psql -q -F '\034' -R '\036' -wAc '%s'" \
+ "$(printf %s "$*" | sed "s/'/'\\\\''/g")"
+ )"
+}
+
+password_check_login() (
+ PGPASSWORD=$(cat "${__object:?}/parameter/password"; printf .)
+ PGPASSWORD=${PGPASSWORD%?.}
+ export PGPASSWORD
+ psql -q -w -h localhost -U "${rolename}" template1 -c '\q' >/dev/null 2>&1
+)
+
+role_properties=$(
+ psql_query "SELECT * FROM pg_roles WHERE rolname = '${rolename}'" \
+ | awk '
+ BEGIN { RS = "\036"; FS = "\034" }
+ /^\([0-9]+ rows?\)/ { exit }
+ NR == 1 { for (i = 1; i <= NF; i++) cols[i] = $i; next }
+ NR == 2 { for (i = 1; i <= NF; i++) printf "%s=%s\n", cols[i], $i }
+ '
+)
+
+if test -n "${role_properties}"
then
- echo 'present'
+ # Check if the user's properties match the parameters
+ for prop in login createdb createrole superuser
+ do
+ bool_should=$(test -f "${__object:?}/parameter/${prop}" && echo 't' || echo 'f')
+ bool_is=$(
+ printf '%s\n' "${role_properties}" |
+ awk -F '=' -v key="${prop}" '
+ BEGIN {
+ if (key == "login")
+ key = "canlogin"
+ else if (key == "superuser")
+ key = "super"
+ key = "rol" key
+ }
+ $1 == key {
+ sub(/^[^=]*=/, "")
+ print
+ }
+ '
+ )
+
+ test "${bool_is}" = "${bool_should}" || {
+ state='different properties'
+ }
+ done
+
+ # Check password
+ passwd_stored=$(
+ psql_query "SELECT rolpassword FROM pg_authid WHERE rolname = '${rolename}'" \
+ | awk 'BEGIN { RS = "\036" } NR == 2'
+ printf .
+ )
+ passwd_stored=${passwd_stored%?.}
+
+ if test -f "${__object:?}/parameter/password"
+ then
+ passwd_should=$(cat "${__object:?}/parameter/password"; printf .)
+ fi
+ passwd_should=${passwd_should%?.}
+
+ if test -z "${passwd_stored}"
+ then
+ test -z "${passwd_should}" || state="${state:-different} password"
+ elif expr "${passwd_stored}" : 'SCRAM-SHA-256\$.*$' >/dev/null
+ then
+ # SCRAM-SHA-256 "encrypted" password
+ # NOTE: There is currently no easy way to check SCRAM passwords without
+ # logging in
+ password_check_login || state="${state:-different} password"
+ elif expr "${passwd_stored}" : 'md5[0-9a-f]\{32\}$' >/dev/null
+ then
+ # MD5 "encrypted" password
+ if command -v md5sum >/dev/null 2>&1
+ then
+ should_md5=$(
+ printf '%s%s' "${passwd_should}" "${rolename}" \
+ | md5sum - | sed -e 's/[^0-9a-f]*$//')
+ elif command -v gmd5sum >/dev/null 2>&1
+ then
+ should_md5=$(
+ printf '%s%s' "${passwd_should}" "${rolename}" \
+ | gmd5sum - | sed -e 's/[^0-9a-f]*$//')
+ elif command -v openssl >/dev/null 2>&1
+ then
+ should_md5=$(
+ printf '%s%s' "${passwd_should}" "${rolename}" \
+ | openssl dgst -md5 | sed 's/^.* //')
+ fi
+
+ if test -n "${should_md5}"
+ then
+ test "${passwd_stored}" = "md5${should_md5}" \
+ || state="${state:-different} password"
+ else
+ password_check_login || state="${state:-different} password"
+ fi
+ else
+ # unencrypted password (unsupported since PostgreSQL 10)
+ test "${passwd_stored}" = "${passwd_should}" \
+ || state="${state:-different} password"
+ fi
+
+ test -n "${state}" || state='present'
else
- echo 'absent'
+ state='absent'
fi
+
+echo "${state}"
diff --git a/cdist/conf/type/__postgres_role/gencode-remote b/cdist/conf/type/__postgres_role/gencode-remote
index 282294c9..d7631fbd 100755
--- a/cdist/conf/type/__postgres_role/gencode-remote
+++ b/cdist/conf/type/__postgres_role/gencode-remote
@@ -1,6 +1,7 @@
#!/bin/sh -e
#
# 2011 Steven Armstrong (steven-cdist at armstrong.cc)
+# 2020 Dennis Camera (dennis.camera at ssrq-sds-fds.ch)
#
# This file is part of cdist.
#
@@ -11,55 +12,117 @@
#
# cdist is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with cdist. If not, see .
#
-case "$(cat "${__global}/explorer/os")"
+quote() {
+ if test $# -gt 0
+ then
+ printf '%s' "$*"
+ else
+ cat -
+ fi | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/'/"
+}
+
+case $(cat "${__global:?}/explorer/os")
in
- netbsd)
- postgres_user='pgsql'
- ;;
- openbsd)
- postgres_user='_postgresql'
- ;;
- *)
- postgres_user='postgres'
- ;;
+ (netbsd)
+ postgres_user='pgsql'
+ ;;
+ (openbsd)
+ postgres_user='_postgresql'
+ ;;
+ (*)
+ postgres_user='postgres'
+ ;;
esac
-name="$__object_id"
-state_is="$(cat "$__object/explorer/state")"
-state_should="$(cat "$__object/parameter/state")"
+rolename=${__object_id:?}
+state_is=$(cat "${__object:?}/explorer/state")
+state_should=$(cat "${__object:?}/parameter/state")
-[ "$state_is" = "$state_should" ] && exit 0
+if test "${state_is}" = "${state_should}"
+then
+ exit 0
+fi
-case "$state_should" in
- present)
- if [ -f "$__object/parameter/password" ]; then
- password="$(cat "$__object/parameter/password")"
- fi
- booleans=""
- for boolean in login createdb createrole superuser; do
- if [ ! -f "$__object/parameter/$boolean" ]; then
- boolean="no${boolean}"
- fi
- upper=$(echo $boolean | tr '[:lower:]' '[:upper:]')
- booleans="$booleans $upper"
- done
+psql_query() {
+ printf 'su -l %s -c %s\n' \
+ "$(quote "${postgres_user}")" \
+ "$(quote "psql postgres -q -w -c $(quote "$1")")"
+}
- [ -n "$password" ] && password="PASSWORD '$password'"
- cat << EOF
-su - '$postgres_user' -c "psql postgres -wc \"CREATE ROLE \\\\\"$name\\\\\" WITH $password $booleans;\""
-EOF
- ;;
- absent)
- cat << EOF
-su - '$postgres_user' -c "dropuser \"$name\""
-EOF
- ;;
+psql_set_password() {
+ # NOTE: Always make sure that the password does not end up in psql_history!
+ # NOTE: Never set an empty string as the password, because they can be
+ # interpreted differently by different tooling.
+ if test -s "${__object:?}/parameter/password"
+ then
+ cat <<-EOF
+ exec 3< "\${__object:?}/parameter/password"
+ su -l '${postgres_user}' -c 'psql -q -w postgres' <<'SQL'
+ \set HISTFILE /dev/null
+ \set pw \`cat <&3\`
+ ALTER ROLE "${rolename}" WITH PASSWORD :'pw';
+ SQL
+ exec 3<&-
+ EOF
+ else
+ psql_query "ALTER ROLE \"${rolename}\" WITH PASSWORD NULL;"
+ fi
+}
+
+role_properties_should() {
+ _props=
+ for _prop in login createdb createrole superuser
+ do
+ _props="${_props}${_props:+ }$(
+ if test -f "${__object:?}/parameter/${_prop}"
+ then
+ echo "${_prop}"
+ else
+ echo "no${_prop}"
+ fi \
+ | tr '[:lower:]' '[:upper:]')"
+ done
+ printf '%s\n' "${_props}"
+ unset _prop _props
+}
+
+case ${state_should}
+in
+ (present)
+ case ${state_is}
+ in
+ (absent)
+ psql_query "CREATE ROLE \"${rolename}\" WITH $(role_properties_should);"
+ psql_set_password
+ ;;
+ (different*)
+ if expr "${state_is}" : 'different.*properties' >/dev/null
+ then
+ psql_query "ALTER ROLE \"${rolename}\" WITH $(role_properties_should);"
+ fi
+
+ if expr "${state_is}" : 'different.*password' >/dev/null
+ then
+ psql_set_password
+ fi
+ ;;
+ (*)
+ printf 'Invalid state reported by state explorer: %s\n' "${state_is}" >&2
+ exit 1
+ ;;
+ esac
+ ;;
+ (absent)
+ printf 'su -l %s -c %s\n' \
+ "$(quote "${postgres_user}")" \
+ "$(quote "dropuser $(quote "${rolename}")")"
+ ;;
esac