Skip to content

Commit e1e21d8

Browse files
ndas7Pattela JAYARAGINI
authored andcommitted
gNSI: Add backend support for Credentialz
Signed-off-by: Pattela JAYARAGINI <[email protected]>
1 parent 84b2892 commit e1e21d8

File tree

5 files changed

+1775
-2
lines changed

5 files changed

+1775
-2
lines changed

host_modules/gnsi_console.py

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,207 @@
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

Comments
 (0)