|
| 1 | +"""gNSI console module used to manage console credentials""" |
| 2 | + |
| 3 | +import json |
| 4 | +import os |
| 5 | +import shutil |
| 6 | +import logging |
| 7 | + |
| 8 | +from host_modules import host_service |
| 9 | +from utils.run_cmd import _run_command |
| 10 | + |
| 11 | +MOD_NAME = 'gnsi_console' |
| 12 | + |
| 13 | +# File path which consists of console password |
| 14 | +PASSWD_FILE = "/etc/shadow" |
| 15 | +PASSWD_FILE_CHECKPOINT_FILE = PASSWD_FILE + "_checkpoint" |
| 16 | +PASSWD_FILE_TEMP = PASSWD_FILE + "_temp" |
| 17 | + |
| 18 | +# Openssl command to generate hashed password using SHA512-based algorithm |
| 19 | +OPENSSL_COMMAND = "openssl passwd -6 " |
| 20 | + |
| 21 | +# Constant trailing info regarding each password in the password file |
| 22 | +TRAILING_PASSWORD_INFO = ":12215:0:99999:7:::\n" |
| 23 | + |
| 24 | +logger = logging.getLogger(__name__) |
| 25 | + |
| 26 | +class GnsiConsole(host_service.HostModule): |
| 27 | + """DBus endpoint used to update console credentials for an existing user |
| 28 | + """ |
| 29 | + |
| 30 | + @host_service.method(host_service.bus_name(MOD_NAME), in_signature='as', out_signature='is') |
| 31 | + def create_checkpoint(self, options): |
| 32 | + """Creates checkpoint for console password file so that the current |
| 33 | + state can be restored later using restore_checkpoint(). create_checkpoint() will be |
| 34 | + invoked when gNSI client starts the password change process.""" |
| 35 | + try: |
| 36 | + shutil.copy(PASSWD_FILE, PASSWD_FILE_CHECKPOINT_FILE) |
| 37 | + except Exception as error: |
| 38 | + return 1, "Failed to create checkpoint with error: " + str(error) |
| 39 | + return 0, "Successfully created checkpoint" |
| 40 | + |
| 41 | + @host_service.method(host_service.bus_name(MOD_NAME), in_signature='as', out_signature='is') |
| 42 | + def restore_checkpoint(self, options): |
| 43 | + """Restore the state of the console password file to the state when |
| 44 | + create_checkpoint() is called, i.e., to the state when the password change process has started. |
| 45 | + Here, a move operation is performed as move is an atomic operation.""" |
| 46 | + if not os.path.isfile(PASSWD_FILE_CHECKPOINT_FILE): |
| 47 | + return 1, "Checkpoint file is not present" |
| 48 | + |
| 49 | + # Update the /etc/shadow with the checkpoint file |
| 50 | + result = self.update_password_file(PASSWD_FILE_CHECKPOINT_FILE) |
| 51 | + return result[0], "restore_checkpoint: " + result[1] |
| 52 | + |
| 53 | + @host_service.method(host_service.bus_name(MOD_NAME), in_signature='as', out_signature='is') |
| 54 | + def delete_checkpoint(self, options): |
| 55 | + """Deletes the checkpoint file created in create_checkpoint(). |
| 56 | + delete_checkpoint() is invoked at the end of the successful password |
| 57 | + change process.""" |
| 58 | + try: |
| 59 | + os.remove(PASSWD_FILE_CHECKPOINT_FILE) |
| 60 | + except Exception as error: |
| 61 | + return 1, "Failed to delete checkpoint with error: " + str(error) |
| 62 | + return 0, "Successfully deleted checkpoint" |
| 63 | + |
| 64 | + def get_hashed_password(self, text_password): |
| 65 | + """Generates and returns hashed password for given text password using |
| 66 | + SHA-512-based password algorithm. Returns empty string on failure.""" |
| 67 | + rc, stdout, stderr = _run_command(OPENSSL_COMMAND + text_password) |
| 68 | + if rc: |
| 69 | + logger.error("%s: Failed to get hash for given text password " |
| 70 | + "with stdout: %s, stderr: %s" |
| 71 | + % (MOD_NAME, stdout, stderr)) |
| 72 | + return "" |
| 73 | + return stdout[0] |
| 74 | + |
| 75 | + def read_password_file(self): |
| 76 | + """Read contents of /etc/shadow password file and return its contents |
| 77 | + in the form of a list where each line is an element in the list""" |
| 78 | + try: |
| 79 | + with open(PASSWD_FILE, 'r') as f: |
| 80 | + password_file_content_list = f.readlines() |
| 81 | + except IOError as error: |
| 82 | + return [], "Failed to read password file with error: " + str(error) |
| 83 | + return password_file_content_list, "" |
| 84 | + |
| 85 | + def update_password_if_user_found(self, user_name, user_password, |
| 86 | + password_file_content_list): |
| 87 | + """If user with user_name is found in password_file_content_list, then |
| 88 | + this function will update password with user_password in |
| 89 | + password_file_content_list. Logs an error if user_name is not found""" |
| 90 | + found_user = False |
| 91 | + for index,each_line in enumerate(password_file_content_list): |
| 92 | + if each_line.startswith(user_name): |
| 93 | + found_user = True |
| 94 | + password_file_content_list[index] = (user_name + ":" + |
| 95 | + user_password + |
| 96 | + TRAILING_PASSWORD_INFO) |
| 97 | + if not found_user: |
| 98 | + logger.error("%s: The given user name: %s does not exist in the " |
| 99 | + "password file" % (MOD_NAME, user_name)) |
| 100 | + |
| 101 | + def create_temp_passwd_file(self, password_file_content_list): |
| 102 | + """Writes the contents of password_file_content_list into a temporary |
| 103 | + file""" |
| 104 | + rc = 0 |
| 105 | + output = "" |
| 106 | + try: |
| 107 | + with open(PASSWD_FILE_TEMP, 'w') as f: |
| 108 | + f.writelines(password_file_content_list) |
| 109 | + except IOError as error: |
| 110 | + rc = 1 |
| 111 | + output = ("Failed to create temporary password file with error: " |
| 112 | + + str(error)) |
| 113 | + |
| 114 | + # Remove temporary password file if it exists after failing to create |
| 115 | + # this file with password_file_content_list |
| 116 | + if rc and os.path.isfile(PASSWD_FILE_TEMP): |
| 117 | + try: |
| 118 | + os.remove(PASSWD_FILE_TEMP) |
| 119 | + except Exception as error: |
| 120 | + output += (" and also failed to remove temporary file " |
| 121 | + "created with error: " + str(error)) |
| 122 | + return rc, output |
| 123 | + |
| 124 | + def update_password_file(self, given_password_file): |
| 125 | + """Overwrites /etc/shadow with given_password_file through a move operation """ |
| 126 | + rc = 0 |
| 127 | + output = "Successfully updated console passwords" |
| 128 | + try: |
| 129 | + shutil.move(given_password_file, PASSWD_FILE) |
| 130 | + except Exception as error: |
| 131 | + rc = 1 |
| 132 | + output = ("Failed to replace original password file with " |
| 133 | + "given password file with error: " |
| 134 | + + str(error)) |
| 135 | + |
| 136 | + # Remove given_password_file if it exists after failing to overwrite |
| 137 | + # /etc/shadow with given_password_file |
| 138 | + if rc and os.path.isfile(given_password_file): |
| 139 | + try: |
| 140 | + os.remove(given_password_file) |
| 141 | + except Exception as error: |
| 142 | + output += (" and also failed to remove given password file " |
| 143 | + "with error: " + str(error)) |
| 144 | + |
| 145 | + return rc, output |
| 146 | + |
| 147 | + @host_service.method(host_service.bus_name(MOD_NAME), in_signature='as', out_signature='is') |
| 148 | + def set(self, options): |
| 149 | + """Updates console passwords for exisitng users based on input request. |
| 150 | + This API does not support creation or deletion of new user accounts.""" |
| 151 | + if not os.path.isfile(PASSWD_FILE_CHECKPOINT_FILE): |
| 152 | + return 1, "Trying to update console password without creating checkpoint" |
| 153 | + |
| 154 | + """Convert input json formatted password set request into python dict. |
| 155 | + console_password_info_dict is a python dict with the following format: |
| 156 | + { |
| 157 | + "ConsolePasswords": [ |
| 158 | + { "name": "alice", "password" : "password-alice" }, |
| 159 | + { "name": "bob", "password" : "password-bob" } |
| 160 | + ] |
| 161 | + } |
| 162 | + """ |
| 163 | + try: |
| 164 | + console_password_info_dict = json.loads(options[0]) |
| 165 | + except json.JSONDecodeError: |
| 166 | + return 1, ("Failed to parse json formatted password change request: " |
| 167 | + + options[0]) |
| 168 | + |
| 169 | + if "ConsolePasswords" not in console_password_info_dict: |
| 170 | + return 1, "Received invalid password request: %s" % str(console_password_info_dict) |
| 171 | + |
| 172 | + # Return on failed to read contents of /etc/shadow file |
| 173 | + password_file_content_list, errstr = self.read_password_file() |
| 174 | + if not password_file_content_list: |
| 175 | + return 1, errstr |
| 176 | + |
| 177 | + # Iterate over each line in password file and update the passwords for |
| 178 | + # the corresponding users in the input request |
| 179 | + for index, each_request in enumerate(console_password_info_dict["ConsolePasswords"]): |
| 180 | + # Skip processing the current element in input request if |
| 181 | + # either "name" or "password" key is missing |
| 182 | + if "name" not in each_request or "password" not in each_request: |
| 183 | + logger.error("%s: Either name or password is not present at " |
| 184 | + "index %d in password change request: %s" |
| 185 | + % (MOD_NAME, index, str(console_password_info_dict))) |
| 186 | + continue |
| 187 | + |
| 188 | + hashed_password = self.get_hashed_password(each_request["password"]) |
| 189 | + if not hashed_password: |
| 190 | + continue |
| 191 | + |
| 192 | + self.update_password_if_user_found(each_request["name"], hashed_password, |
| 193 | + password_file_content_list) |
| 194 | + |
| 195 | + # Create a temporary password file with new changes |
| 196 | + err, errstr = self.create_temp_passwd_file(password_file_content_list) |
| 197 | + if err: |
| 198 | + return err, errstr |
| 199 | + |
| 200 | + # Update the contents in /etc/shadow password file |
| 201 | + result = self.update_password_file(PASSWD_FILE_TEMP) |
| 202 | + return result[0], "set: " + result[1] |
| 203 | + |
| 204 | + |
| 205 | +def register(): |
| 206 | + """Return the class name""" |
| 207 | + return GnsiConsole, MOD_NAME |
0 commit comments