From 546639ff25eddd6f3d2230dbabfa37270d109487 Mon Sep 17 00:00:00 2001
From: dobli <dobler.alex@gmail.com>
Date: Thu, 17 Jan 2019 17:40:52 +0100
Subject: [PATCH] added functions to generate user-password files for mosquitto
 and sftp

---
 README.md           |   3 ++
 building_manager.py | 105 ++++++++++++++++++++++++++++++++++++++++----
 2 files changed, 99 insertions(+), 9 deletions(-)

diff --git a/README.md b/README.md
index cf24c48..737d397 100644
--- a/README.md
+++ b/README.md
@@ -105,6 +105,9 @@ The openhab-pb stack consists of multiple configuration files that need to be av
   - copy from template folder
 - *sftp_users.conf*: file containing users for sftp container
   - generated, grants access to configuration files
+  - uses `makepasswd` to generate MD5 hashed passwords
+    - script uses pythons `crypt` to generate them
+    - as it relies on the Linux password system we can even use stronger hashes like SHA512
 - *known_hosts*: make backup (volumerize) hosts know internal ssh servers
   - generated using ssh-keygen
 - *id_rsa/id_rsa.pub*: key pair for passwordless ssh between containers
diff --git a/building_manager.py b/building_manager.py
index df001d9..57cc5fc 100755
--- a/building_manager.py
+++ b/building_manager.py
@@ -18,6 +18,10 @@ TEMPLATE_FILES = [
     'mosquitto/mosquitto.conf', 'nodered/nodered_package.json',
     'nodered/nodered_settings.js', 'ssh/sshd_config', 'traefik/traefik.toml'
 ]
+EDIT_FILES = {
+    "mosquitto_passwords": "mosquitto/mosquitto_passwords",
+    "sftp_users": "ssh/sftp_users.conf"
+}
 
 # Default Swarm port
 SWARM_PORT = 2377
@@ -61,8 +65,8 @@ def generate_mosquitto_user_line(username, password):
 
     :username: username to use
     :password: password that will be hashed (SHA512)
-    :returns: a line as expected by mosquitto
 
+    :returns: a line as expected by mosquitto
     """
     import crypt
     password_hash = crypt.crypt(password, crypt.mksalt(crypt.METHOD_SHA512))
@@ -70,6 +74,77 @@ def generate_mosquitto_user_line(username, password):
     return line
 
 
+def generate_sftp_user_line(username, password, directories=None):
+    """Generates a line for a sftp user with a hashed password
+
+    :username: username to use
+    :password: password that will be hashed (MD5)
+    :directories: list of directories which the user should have
+
+    :returns: a line as expected by mosquitto
+    """
+    import crypt
+    # generate user line with hashed password
+    password_hash = crypt.crypt(password, crypt.mksalt(crypt.METHOD_SHA512))
+    line = f"{username}:{password_hash}:e"
+    # add directory entries when available
+    if directories:
+        # create comma separated string from list
+        dir_line = ','.join(d for d in directories)
+        line = f"{line}:{dir_line}"
+    return line
+
+
+def generate_mosquitto_file(base_dir, username, password):
+    """Generates a mosquitto password file using mosquitto_passwd system tool
+
+    :base_dir: path that contains custom config folder
+    :username: username to use
+    :password: password that will be used
+
+    """
+    passwd_path = base_dir + '/' + CUSTOM_DIR + "/" + EDIT_FILES[
+        'mosquitto_passwords']
+
+    # ensure file exists
+    if not os.path.exists(passwd_path):
+        open(passwd_path, 'a').close()
+
+    # execute mosquitto passwd
+    mos_result = run(
+        ['mosquitto_passwd', '-b', passwd_path, username, password],
+        text=True,
+        capture_output=True)
+    return mos_result.returncode == 0
+
+
+def generate_sftp_file(base_dir, username, password, direcories=None):
+    """Generates a mosquitto password file using mosquitto_passwd system tool
+
+    :base_dir: path that contains custom config folder
+    :username: username to use
+    :password: password that will be used
+    :directories: list of directories which the user should have
+
+    """
+    # generate line and save it into a file
+    file_content = generate_sftp_user_line(username, password, direcories)
+    create_or_replace_config_file(base_dir, EDIT_FILES['sftp_users'],
+                                  file_content)
+
+
+def create_or_replace_config_file(base_dir, config_path, content):
+    """Creates or replaces a config file with new content
+
+    :base_dir: path that contains custom config folder
+    :config_path: relative path of config
+    :content: content of the file as a string
+    """
+    custom_path = base_dir + '/' + CUSTOM_DIR + "/" + config_path
+    with open(custom_path, 'w+') as file:
+        file.write(content)
+
+
 # }}}
 
 
@@ -296,7 +371,7 @@ def interactive_command(args):
 
     :args: command line arguments
     """
-    print(main_menu())
+    print(main_menu(args))
 
 
 # }}}
@@ -305,7 +380,7 @@ def interactive_command(args):
 # ******************************
 # Interactive menu entries {{{
 # ******************************
-def main_menu():
+def main_menu(args):
     """ Display main menu
     """
     questions = [{
@@ -320,14 +395,21 @@ def main_menu():
     answers = prompt(questions)
 
     if 'Create' in answers['main']:
-        init_menu()
+        init_menu(args)
 
     return answers
 
 
-def init_menu():
+def init_menu(args):
     """Menu entry for initial setup and file generation
     """
+    # Base directory for configs
+    base_dir = args.base_dir
+
+    if base_dir is None:
+        base_dir = os.getcwd()
+
+    # Prompts
     questions = [{
         'type': 'input',
         'name': 'stack_name',
@@ -365,9 +447,10 @@ def init_menu():
             if (join_swarm_machine(machine, leader)):
                 print('Joining swarm successful\n')
 
-    user_line = generate_mosquitto_user_line(answers['username'],
-                                             answers['password'])
-    print(user_line)
+    # Generate config files based on input
+    generate_sftp_file(base_dir, answers['username'], answers['password'])
+    generate_mosquitto_file(base_dir, answers['username'], answers['password'])
+
     print(answers)
 
 
@@ -382,7 +465,7 @@ def generate_checkbox_choices(list):
 # }}}
 
 # ******************************
-# Script main (entry) {{{
+# Script main ( entry) {{{
 # ******************************
 if __name__ == '__main__':
     import argparse
@@ -396,6 +479,10 @@ if __name__ == '__main__':
     parser_interactive = subparsers.add_parser(
         'interactive',
         help='Starts the interactive mode of the building manager')
+    parser_interactive.add_argument(
+        '--base_dir',
+        '-d',
+        help='Directory to creat config folders in, default is current dir')
     parser_interactive.set_defaults(func=interactive_command)
 
     # Restore command
-- 
GitLab