Browse Source

Send MySQL passwords via anonymous pipe instead of environment variable (#1009).

Dan Helfman 3 months ago
parent
commit
36d0073375
2 changed files with 25 additions and 37 deletions
  1. 1 1
      borgmatic/hooks/data_source/mariadb.py
  2. 24 36
      borgmatic/hooks/data_source/mysql.py

+ 1 - 1
borgmatic/hooks/data_source/mariadb.py

@@ -34,7 +34,7 @@ def make_defaults_file_pipe(username=None, password=None):
 
 
     If no username or password are given, then return None.
     If no username or password are given, then return None.
 
 
-    Do not and use this value for multiple different command invocations. That will not work because
+    Do not use this value for multiple different command invocations. That will not work because
     each pipe is "used up" once read.
     each pipe is "used up" once read.
     '''
     '''
     values = '\n'.join(
     values = '\n'.join(

+ 24 - 36
borgmatic/hooks/data_source/mysql.py

@@ -6,6 +6,7 @@ import shlex
 import borgmatic.borg.pattern
 import borgmatic.borg.pattern
 import borgmatic.config.paths
 import borgmatic.config.paths
 import borgmatic.hooks.credential.parse
 import borgmatic.hooks.credential.parse
+import borgmatic.hooks.data_source.mariadb
 from borgmatic.execute import (
 from borgmatic.execute import (
     execute_command,
     execute_command,
     execute_command_and_capture_output,
     execute_command_and_capture_output,
@@ -26,11 +27,12 @@ def make_dump_path(base_directory):  # pragma: no cover
 SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
 SYSTEM_DATABASE_NAMES = ('information_schema', 'mysql', 'performance_schema', 'sys')
 
 
 
 
-def database_names_to_dump(database, config, environment, dry_run):
+def database_names_to_dump(database, config, username, password, environment, dry_run):
     '''
     '''
-    Given a requested database config and a configuration dict, return the corresponding sequence of
-    database names to dump. In the case of "all", query for the names of databases on the configured
-    host and return them, excluding any system databases that will cause problems during restore.
+    Given a requested database config, a configuration dict, a database username and password, an
+    environment dict, and whether this is a dry run, return the corresponding sequence of database
+    names to dump. In the case of "all", query for the names of databases on the configured host and
+    return them, excluding any system databases that will cause problems during restore.
     '''
     '''
     if database['name'] != 'all':
     if database['name'] != 'all':
         return (database['name'],)
         return (database['name'],)
@@ -40,24 +42,20 @@ def database_names_to_dump(database, config, environment, dry_run):
     mysql_show_command = tuple(
     mysql_show_command = tuple(
         shlex.quote(part) for part in shlex.split(database.get('mysql_command') or 'mysql')
         shlex.quote(part) for part in shlex.split(database.get('mysql_command') or 'mysql')
     )
     )
+    defaults_file_descriptor = borgmatic.hooks.data_source.mariadb.make_defaults_file_pipe(username, password)
     show_command = (
     show_command = (
         mysql_show_command
         mysql_show_command
+        + ((f'--defaults-extra-file=/dev/fd/{defaults_file_descriptor}',) if defaults_file_descriptor else ())
         + (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
         + (tuple(database['list_options'].split(' ')) if 'list_options' in database else ())
         + (('--host', database['hostname']) if 'hostname' in database else ())
         + (('--host', database['hostname']) if 'hostname' in database else ())
         + (('--port', str(database['port'])) if 'port' in database else ())
         + (('--port', str(database['port'])) if 'port' in database else ())
         + (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
         + (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
-        + (
-            (
-                '--user',
-                borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
-            )
-            if 'username' in database
-            else ()
-        )
         + ('--skip-column-names', '--batch')
         + ('--skip-column-names', '--batch')
         + ('--execute', 'show schemas')
         + ('--execute', 'show schemas')
     )
     )
+
     logger.debug('Querying for "all" MySQL databases to dump')
     logger.debug('Querying for "all" MySQL databases to dump')
+
     show_output = execute_command_and_capture_output(show_command, environment=environment)
     show_output = execute_command_and_capture_output(show_command, environment=environment)
 
 
     return tuple(
     return tuple(
@@ -68,7 +66,7 @@ def database_names_to_dump(database, config, environment, dry_run):
 
 
 
 
 def execute_dump_command(
 def execute_dump_command(
-    database, config, dump_path, database_names, environment, dry_run, dry_run_label
+    database, config, username, password, dump_path, database_names, environment, dry_run, dry_run_label
 ):
 ):
     '''
     '''
     Kick off a dump for the given MySQL/MariaDB database (provided as a configuration dict) to a
     Kick off a dump for the given MySQL/MariaDB database (provided as a configuration dict) to a
@@ -94,21 +92,15 @@ def execute_dump_command(
     mysql_dump_command = tuple(
     mysql_dump_command = tuple(
         shlex.quote(part) for part in shlex.split(database.get('mysql_dump_command') or 'mysqldump')
         shlex.quote(part) for part in shlex.split(database.get('mysql_dump_command') or 'mysqldump')
     )
     )
+    defaults_file_descriptor = borgmatic.hooks.data_source.mariadb.make_defaults_file_pipe(username, password)
     dump_command = (
     dump_command = (
         mysql_dump_command
         mysql_dump_command
+        + ((f'--defaults-extra-file=/dev/fd/{defaults_file_descriptor}',) if defaults_file_descriptor else ())
         + (tuple(database['options'].split(' ')) if 'options' in database else ())
         + (tuple(database['options'].split(' ')) if 'options' in database else ())
         + (('--add-drop-database',) if database.get('add_drop_database', True) else ())
         + (('--add-drop-database',) if database.get('add_drop_database', True) else ())
         + (('--host', database['hostname']) if 'hostname' in database else ())
         + (('--host', database['hostname']) if 'hostname' in database else ())
         + (('--port', str(database['port'])) if 'port' in database else ())
         + (('--port', str(database['port'])) if 'port' in database else ())
         + (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
         + (('--protocol', 'tcp') if 'hostname' in database or 'port' in database else ())
-        + (
-            (
-                '--user',
-                borgmatic.hooks.credential.parse.resolve_credential(database['username'], config),
-            )
-            if 'username' in database
-            else ()
-        )
         + ('--databases',)
         + ('--databases',)
         + database_names
         + database_names
         + ('--result-file', dump_filename)
         + ('--result-file', dump_filename)
@@ -164,19 +156,10 @@ def dump_data_sources(
 
 
     for database in databases:
     for database in databases:
         dump_path = make_dump_path(borgmatic_runtime_directory)
         dump_path = make_dump_path(borgmatic_runtime_directory)
-        environment = dict(
-            os.environ,
-            **(
-                {
-                    'MYSQL_PWD': borgmatic.hooks.credential.parse.resolve_credential(
-                        database['password'], config
-                    )
-                }
-                if 'password' in database
-                else {}
-            ),
-        )
-        dump_database_names = database_names_to_dump(database, config, environment, dry_run)
+        username = borgmatic.hooks.credential.parse.resolve_credential(database.get('username'), config)
+        password = borgmatic.hooks.credential.parse.resolve_credential(database.get('password'), config)
+        environment = dict(os.environ)
+        dump_database_names = database_names_to_dump(database, config, username, password, environment, dry_run)
 
 
         if not dump_database_names:
         if not dump_database_names:
             if dry_run:
             if dry_run:
@@ -192,6 +175,8 @@ def dump_data_sources(
                     execute_dump_command(
                     execute_dump_command(
                         renamed_database,
                         renamed_database,
                         config,
                         config,
+                        username,
+                        password,
                         dump_path,
                         dump_path,
                         (dump_name,),
                         (dump_name,),
                         environment,
                         environment,
@@ -204,6 +189,8 @@ def dump_data_sources(
                 execute_dump_command(
                 execute_dump_command(
                     database,
                     database,
                     config,
                     config,
+                    username,
+                    password,
                     dump_path,
                     dump_path,
                     dump_database_names,
                     dump_database_names,
                     environment,
                     environment,
@@ -295,8 +282,10 @@ def restore_data_source_dump(
     mysql_restore_command = tuple(
     mysql_restore_command = tuple(
         shlex.quote(part) for part in shlex.split(data_source.get('mysql_command') or 'mysql')
         shlex.quote(part) for part in shlex.split(data_source.get('mysql_command') or 'mysql')
     )
     )
+    defaults_file_descriptor = borgmatic.hooks.data_source.mariadb.make_defaults_file_pipe(username, password)
     restore_command = (
     restore_command = (
         mysql_restore_command
         mysql_restore_command
+        + ((f'--defaults-extra-file=/dev/fd/{defaults_file_descriptor}',) if defaults_file_descriptor else ())
         + ('--batch',)
         + ('--batch',)
         + (
         + (
             tuple(data_source['restore_options'].split(' '))
             tuple(data_source['restore_options'].split(' '))
@@ -306,9 +295,8 @@ def restore_data_source_dump(
         + (('--host', hostname) if hostname else ())
         + (('--host', hostname) if hostname else ())
         + (('--port', str(port)) if port else ())
         + (('--port', str(port)) if port else ())
         + (('--protocol', 'tcp') if hostname or port else ())
         + (('--protocol', 'tcp') if hostname or port else ())
-        + (('--user', username) if username else ())
     )
     )
-    environment = dict(os.environ, **({'MYSQL_PWD': password} if password else {}))
+    environment = dict(os.environ)
 
 
     logger.debug(f"Restoring MySQL database {data_source['name']}{dry_run_label}")
     logger.debug(f"Restoring MySQL database {data_source['name']}{dry_run_label}")
     if dry_run:
     if dry_run: