Check database dump privileges before stopping applications
This commit is contained in:
1 parent
898df2e926
commit
5c8a7a30be
6 files changed
+241
-7
No files matched your search
@@ -170,6 +170,56 @@ def crypt(name):
|
||||
print("Crypt configuration accepted (contents, filenames, directories)")
|
||||
|
||||
|
||||
def has_reload(grants):
|
||||
"""Inspect privilege names only; SHOW GRANTS can contain authentication hashes."""
|
||||
for line in grants.splitlines():
|
||||
match = re.match(r'^GRANT ([A-Z_ ,]+) ON \*\.\* TO ', line)
|
||||
if match and {'RELOAD', 'ALL PRIVILEGES'} & set(match[1].split(', ')):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def db_privileges(client, credentials, database):
|
||||
"""Read privileges without executing FLUSH or acquiring a production DB lock.
|
||||
|
||||
Check lock-all-tables and MariaDB's routine metadata prerequisite, not every
|
||||
object-specific permission. Grants (including auth hashes) stay in memory.
|
||||
"""
|
||||
def query(sql, purpose='privilege metadata'):
|
||||
result = subprocess.run([client, '--defaults-extra-file=' + credentials,
|
||||
'--database=' + database, '--batch', '--skip-column-names',
|
||||
'--execute=' + sql], capture_output=True, text=True)
|
||||
if result.returncode:
|
||||
# Neither query results nor server errors may disclose auth material.
|
||||
raise ValueError(f'DB {database}: {purpose} query failed; inspect account permissions')
|
||||
return result.stdout
|
||||
|
||||
grants = query('SHOW GRANTS FOR CURRENT_USER')
|
||||
allowed = has_reload(grants)
|
||||
via_role = False
|
||||
mariadb = 'MariaDB' in query('SELECT VERSION()')
|
||||
if not allowed and mariadb:
|
||||
# Only session-enabled roles count; a merely granted but inactive role
|
||||
# does not help mariadb-dump. ENABLED_ROLES also includes nested roles.
|
||||
roles = query('SELECT ROLE_NAME FROM information_schema.ENABLED_ROLES WHERE ROLE_NAME IS NOT NULL').splitlines()
|
||||
for role in roles:
|
||||
if not re.fullmatch(r'[A-Za-z0-9_.:-]+', role):
|
||||
raise ValueError(f'DB {database}: unsupported role name; configure direct RELOAD grant for backup account')
|
||||
if has_reload(query('SHOW GRANTS FOR `' + role + '`')):
|
||||
allowed = via_role = True
|
||||
break
|
||||
if not allowed:
|
||||
raise ValueError(f'DB {database}: global RELOAD privilege missing or not verifiable; '
|
||||
'required for --lock-all-tables. Grant RELOAD to the actual backup account; '
|
||||
'see UPDATE_ANLEITUNG.md. No FLUSH or database lock performed by this check')
|
||||
print(f'DB {database}: global RELOAD privilege confirmed' + (' through an enabled role' if via_role else ''))
|
||||
if mariadb:
|
||||
# --routines needs mysql.proc access. LIMIT 0 checks permissions without
|
||||
# returning routine bodies or reading the stored routine contents.
|
||||
query('SELECT * FROM mysql.proc LIMIT 0', 'routine metadata (SELECT on mysql.proc required for --routines)')
|
||||
print(f'DB {database}: MariaDB routine metadata access confirmed')
|
||||
|
||||
|
||||
def candidates(prefix, days, keep):
|
||||
# Only v2 names supplied AFTER completion-marker validation are eligible.
|
||||
names = set(sys.stdin.read().splitlines())
|
||||
@@ -233,6 +283,8 @@ def main():
|
||||
op, *args = sys.argv[1:]
|
||||
if op == "wait-nextcloud":
|
||||
wait_nextcloud(*args)
|
||||
elif op == "db-privileges":
|
||||
db_privileges(*args)
|
||||
elif op == "status":
|
||||
write_status(*args)
|
||||
elif op == "crypt":
|
||||
|
||||
+15
-2
@@ -197,6 +197,11 @@ preflight() {
|
||||
need "$DB_DUMP_BIN"; need "$DB_CLIENT_BIN"
|
||||
for i in "${!DB_LABELS[@]}"; do
|
||||
"$DB_CLIENT_BIN" --defaults-extra-file="${DB_CNFS[i]}" --database="${DB_NAMES[i]}" --execute='SELECT 1' >/dev/null
|
||||
# Restore uses different privileges; this prerequisite belongs to dumps.
|
||||
# Metadata only: never briefly lock the live database during --check.
|
||||
if [[ $OPERATION == backup ]]; then
|
||||
helper db-privileges "$DB_CLIENT_BIN" "${DB_CNFS[i]}" "${DB_NAMES[i]}"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
if [[ $ENABLE_NEXTCLOUD == true ]]; then need sudo; need php; [[ -f $NC_DIR/occ ]] || die "Nextcloud occ missing"; fi
|
||||
@@ -262,7 +267,9 @@ stop_service() {
|
||||
# Record intent before stop: a failed/partial stop still needs recovery.
|
||||
STOPPED_SERVICES+=("$1")
|
||||
save_recovery
|
||||
echo "-- Stopping service: $1 (waiting for systemd)"
|
||||
systemctl stop "$1"
|
||||
echo "-- Service stopped: $1"
|
||||
;;
|
||||
inactive|failed) ;;
|
||||
*) die "Unknown/transitioning service state for $1: $state" ;;
|
||||
@@ -278,7 +285,7 @@ quiesce() {
|
||||
if [[ $NC_WAS_ON == false ]]; then NC_CHANGED=true; save_recovery; occ maintenance:mode --on; fi
|
||||
else echo "WARNING: Nextcloud maintenance disabled; consistency depends on external writer shutdown"; fi
|
||||
fi
|
||||
# Include application cron/worker services in this array; crond itself is not stopped.
|
||||
# Cron daemons are included only if explicitly configured by the operator.
|
||||
for p in "${QUIESCE_SERVICES[@]}"; do stop_service "$p"; done
|
||||
if [[ $ENABLE_WORDPRESS == true || $ENABLE_NEXTCLOUD == true ]]; then
|
||||
if [[ $ENABLE_HTTPD_STOP == true ]]; then stop_service "$HTTPD_SERVICE_NAME"; fi
|
||||
@@ -294,7 +301,9 @@ quiesce() {
|
||||
if [[ $ENABLE_NEXTCLOUD == true ]]; then
|
||||
# Cron daemons may leave an already launched PHP child running after stop.
|
||||
# Wait for direct cron.php / occ processes to finish; never kill them blindly.
|
||||
echo '-- Waiting for remaining Nextcloud writers'
|
||||
helper wait-nextcloud "$NC_DIR" "${WRITER_DRAIN_TIMEOUT:=300}"
|
||||
echo '-- Nextcloud writers finished'
|
||||
fi
|
||||
}
|
||||
# Survives SIGKILL/power loss, which Bash traps cannot handle. This is an
|
||||
@@ -317,7 +326,11 @@ resume_services() {
|
||||
fi
|
||||
for ((i=${#STOPPED_SERVICES[@]}-1; i>=0; i--)); do
|
||||
[[ -n ${STOPPED_SERVICES[i]} ]] || continue
|
||||
if systemctl start "${STOPPED_SERVICES[i]}"; then STOPPED_SERVICES[i]=''; else failed=1; fi
|
||||
echo "-- Starting service: ${STOPPED_SERVICES[i]} (waiting for systemd)"
|
||||
if systemctl start "${STOPPED_SERVICES[i]}"; then
|
||||
echo "-- Service started: ${STOPPED_SERVICES[i]}"
|
||||
STOPPED_SERVICES[i]=''
|
||||
else failed=1; fi
|
||||
done
|
||||
if (( failed == 0 )); then
|
||||
rm -f -- "$WORKDIR/recovery-$RUN_ID.json"
|
||||
|
||||
Reference in new issue
Block a user