blob: 35c6c7d7ec0b43873a1ad643e33340066eaaec0e [file] [log] [blame]
Václav Kubernátbabbab92021-01-27 09:25:05 +01001/*
2 * Copyright (C) 2021 CESNET, https://photonics.cesnet.cz/
3 *
4 * Written by Václav Kubernát <kubernat@cesnet.cz>
5 *
6*/
7
8#include <fmt/core.h>
9#include <fstream>
10#include <pwd.h>
11#include <shadow.h>
12#include <spdlog/spdlog.h>
13#include <sstream>
14#include "Authentication.h"
15#include "system_vars.h"
16#include "utils/exec.h"
17#include "utils/io.h"
18#include "utils/libyang.h"
19#include "utils/sysrepo.h"
20#include "utils/time.h"
21
22using namespace std::string_literals;
23namespace {
24const auto czechlight_system_module = "czechlight-system"s;
25const auto authentication_container = "/" + czechlight_system_module + ":authentication";
26const auto change_password_action = "/" + czechlight_system_module + ":authentication/users/change-password";
27const auto add_key_action = "/" + czechlight_system_module + ":authentication/users/add-authorized-key";
28const auto remove_key_action = "/" + czechlight_system_module + ":authentication/users/authorized-keys/remove";
29}
30
31namespace velia::system {
32namespace {
33
34void writeKeys(const std::string& filename, const std::vector<std::string>& keys)
35{
36 std::ostringstream ss;
37
38 for (const auto& key : keys) {
39 ss << key << "\n";
40 }
41 utils::safeWriteFile(filename, ss.str());
42}
43}
44
45namespace impl {
Tomáš Peckad9e741f2021-02-10 15:51:17 +010046void changePassword(const std::string& name, const std::string& password, const std::string& etc_shadow)
Václav Kubernátbabbab92021-01-27 09:25:05 +010047{
48 utils::execAndWait(spdlog::get("system"), CHPASSWD_EXECUTABLE, {}, name + ":" + password);
Tomáš Peckad9e741f2021-02-10 15:51:17 +010049 auto shadow = velia::utils::readFileToString(etc_shadow);
Václav Kubernátbabbab92021-01-27 09:25:05 +010050 utils::safeWriteFile(BACKUP_ETC_SHADOW_FILE, shadow);
51}
Jan Kundrátb25ef242021-02-18 14:56:54 +010052
53auto file_open(const char* filename, const char* mode)
54{
55 auto res = std::unique_ptr<std::FILE, decltype(&std::fclose)>(std::fopen(filename, mode), std::fclose);
56 if (!res.get()) {
57 throw std::system_error{errno, std::system_category(), "fopen("s + filename + ") failed"};
58 }
59 return res;
60}
Václav Kubernátbabbab92021-01-27 09:25:05 +010061}
62
63std::string Authentication::homeDirectory(const std::string& username)
64{
Jan Kundrátb25ef242021-02-18 14:56:54 +010065 auto passwdFile = impl::file_open(m_etc_passwd.c_str(), "r");
Václav Kubernátbabbab92021-01-27 09:25:05 +010066 passwd entryBuf;
67 size_t bufLen = 10;
68 auto buffer = std::make_unique<char[]>(bufLen);
69 passwd* entry;
70
71 while (true) {
Václav Kubernát515cef02021-03-25 05:48:57 +010072 auto pos = ftell(passwdFile.get());
Jan Kundrátb25ef242021-02-18 14:56:54 +010073 auto ret = fgetpwent_r(passwdFile.get(), &entryBuf, buffer.get(), bufLen, &entry);
Václav Kubernátbabbab92021-01-27 09:25:05 +010074 if (ret == ERANGE) {
75 bufLen += 100;
76 buffer = std::make_unique<char[]>(bufLen);
Václav Kubernát515cef02021-03-25 05:48:57 +010077 fseek(passwdFile.get(), pos, SEEK_SET);
Václav Kubernátbabbab92021-01-27 09:25:05 +010078 continue;
79 }
Jan Kundrátb25ef242021-02-18 14:56:54 +010080
81 if (ret == ENOENT) {
82 break;
Václav Kubernátbabbab92021-01-27 09:25:05 +010083 }
84
Jan Kundrátb25ef242021-02-18 14:56:54 +010085 if (ret != 0) {
86 throw std::system_error{ret, std::system_category(), "fgetpwent_r() failed"};
87 }
88
89 assert(entry);
90
91 if (username != entry->pw_name) {
92 continue;
93 }
94
95 return entry->pw_dir;
Václav Kubernátbabbab92021-01-27 09:25:05 +010096 }
97
98 throw std::runtime_error("User " + username + " doesn't exist");
99}
100
Václav Kubernát8ea630e2021-02-18 16:55:25 +0100101std::map<std::string, std::optional<std::string>> Authentication::lastPasswordChanges()
Václav Kubernátbabbab92021-01-27 09:25:05 +0100102{
Jan Kundrátb25ef242021-02-18 14:56:54 +0100103 auto shadowFile = impl::file_open(m_etc_shadow.c_str(), "r");
Václav Kubernátbabbab92021-01-27 09:25:05 +0100104 spwd entryBuf;
105 size_t bufLen = 10;
106 auto buffer = std::make_unique<char[]>(bufLen);
107 spwd* entry;
108
Václav Kubernát8ea630e2021-02-18 16:55:25 +0100109 std::map<std::string, std::optional<std::string>> res;
Václav Kubernátbabbab92021-01-27 09:25:05 +0100110 while (true) {
Václav Kubernát515cef02021-03-25 05:48:57 +0100111 auto pos = ftell(shadowFile.get());
Jan Kundrátb25ef242021-02-18 14:56:54 +0100112 auto ret = fgetspent_r(shadowFile.get(), &entryBuf, buffer.get(), bufLen, &entry);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100113 if (ret == ERANGE) {
114 bufLen += 100;
115 buffer = std::make_unique<char[]>(bufLen);
Václav Kubernát515cef02021-03-25 05:48:57 +0100116 fseek(shadowFile.get(), pos, SEEK_SET);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100117 continue;
118 }
119
Jan Kundrátb25ef242021-02-18 14:56:54 +0100120 if (ret == ENOENT) {
121 break;
Václav Kubernátbabbab92021-01-27 09:25:05 +0100122 }
123
Jan Kundrátb25ef242021-02-18 14:56:54 +0100124 if (ret != 0) {
125 throw std::system_error{ret, std::system_category(), "fgetspent_r() failed"};
126 }
127
128 assert(entry);
129
Jan Kundrátb25ef242021-02-18 14:56:54 +0100130 using namespace std::chrono_literals;
131 using TimeType = std::chrono::time_point<std::chrono::system_clock>;
Václav Kubernát8ea630e2021-02-18 16:55:25 +0100132 res.emplace(entry->sp_namp, velia::utils::yangTimeFormat(TimeType(24h * entry->sp_lstchg)));
Václav Kubernátbabbab92021-01-27 09:25:05 +0100133 }
134
Václav Kubernát8ea630e2021-02-18 16:55:25 +0100135 return res;
Václav Kubernátbabbab92021-01-27 09:25:05 +0100136}
137
138std::string Authentication::authorizedKeysPath(const std::string& username)
139{
140 using namespace fmt::literals;
Tomáš Pecka08c1aa42021-08-02 14:36:40 +0200141
142#if FMT_VERSION >= 80000 // fmt >= 8.0.0
143 auto str = fmt::runtime(m_authorized_keys_format);
144#else
145 auto str = m_authorized_keys_format;
146#endif
147
148 return fmt::format(str, "USER"_a=username, "HOME"_a=homeDirectory(username));
Václav Kubernátbabbab92021-01-27 09:25:05 +0100149}
150
151std::vector<std::string> Authentication::listKeys(const std::string& username)
152{
153 std::vector<std::string> res;
154 std::ifstream ifs(authorizedKeysPath(username));
155 if (!ifs.is_open()) {
156 return res;
157 }
158 std::string line;
159 while (std::getline(ifs, line)) {
160 if (line.find_first_not_of(" \r\t") == std::string::npos) {
161 continue;
162 }
163
164 res.emplace_back(line);
165 }
166
167 return res;
168}
169
170std::vector<User> Authentication::listUsers()
171{
172 std::vector<User> res;
Jan Kundrátb25ef242021-02-18 14:56:54 +0100173 auto passwdFile = impl::file_open(m_etc_passwd.c_str(), "r");
Václav Kubernátbabbab92021-01-27 09:25:05 +0100174 passwd entryBuf;
175 size_t bufLen = 10;
176 auto buffer = std::make_unique<char[]>(bufLen);
177 passwd* entry;
178
Václav Kubernát8ea630e2021-02-18 16:55:25 +0100179 auto pwChanges = lastPasswordChanges();
Václav Kubernátbabbab92021-01-27 09:25:05 +0100180 while (true) {
Václav Kubernát515cef02021-03-25 05:48:57 +0100181 auto pos = ftell(passwdFile.get());
Jan Kundrátb25ef242021-02-18 14:56:54 +0100182 auto ret = fgetpwent_r(passwdFile.get(), &entryBuf, buffer.get(), bufLen, &entry);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100183 if (ret == ERANGE) {
184 bufLen += 100;
185 buffer = std::make_unique<char[]>(bufLen);
Václav Kubernát515cef02021-03-25 05:48:57 +0100186 fseek(passwdFile.get(), pos, SEEK_SET);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100187 continue;
188 }
189
190 if (ret == ENOENT) {
191 break;
192 }
Jan Kundrátb25ef242021-02-18 14:56:54 +0100193
194 if (ret != 0) {
195 throw std::system_error{ret, std::system_category(), "fgetpwent_r() failed"};
196 }
197
198 assert(entry);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100199 User user;
200 user.name = entry->pw_name;
201 user.authorizedKeys = listKeys(user.name);
Václav Kubernát8ea630e2021-02-18 16:55:25 +0100202 if (auto it = pwChanges.find(user.name); it != pwChanges.end()) {
203 user.lastPasswordChange = it->second;
204 }
Václav Kubernátbabbab92021-01-27 09:25:05 +0100205 res.emplace_back(user);
206 }
207
Václav Kubernátbabbab92021-01-27 09:25:05 +0100208 return res;
209}
210
211void Authentication::addKey(const std::string& username, const std::string& key)
212{
213 try {
214 utils::execAndWait(spdlog::get("system"), SSH_KEYGEN_EXECUTABLE, {"-l", "-f", "-"}, key, {utils::ExecOptions::DropRoot});
215 } catch (std::runtime_error& ex) {
216 using namespace fmt::literals;
217 throw AuthException(fmt::format("Key is not a valid SSH public key: {stderr}\n{key}", "stderr"_a=ex.what(), "key"_a=key));
218 }
219 auto currentKeys = listKeys(username);
220 currentKeys.emplace_back(key);
221 writeKeys(authorizedKeysPath(username), currentKeys);
222}
223
224void Authentication::removeKey(const std::string& username, const int index)
225{
226 auto currentKeys = listKeys(username);
227 if (currentKeys.size() == 1) {
228 // FIXME: maybe add an option to bypass this check?
229 throw AuthException("Can't remove last key.");
230 }
231 currentKeys.erase(currentKeys.begin() + index);
232 writeKeys(authorizedKeysPath(username), currentKeys);
233}
234}
235
236void usersToTree(libyang::S_Context ctx, const std::vector<velia::system::User> users, libyang::S_Data_Node& out)
237{
238 out = std::make_shared<libyang::Data_Node>(
239 ctx,
240 authentication_container.c_str(),
241 nullptr,
242 LYD_ANYDATA_CONSTSTRING,
243 0);
244 for (const auto& user : users) {
245 auto userNode = out->new_path(ctx, ("users[name='" + user.name + "']").c_str(), nullptr, LYD_ANYDATA_CONSTSTRING, 0);
246
247 decltype(user.authorizedKeys)::size_type entries = 0;
248 for (const auto& authorizedKey : user.authorizedKeys) {
249 auto entry = userNode->new_path(ctx, ("authorized-keys[index='" + std::to_string(entries) + "']").c_str(), nullptr, LYD_ANYDATA_CONSTSTRING, 0);
250 entry->new_path(ctx, "public-key", authorizedKey.c_str(), LYD_ANYDATA_CONSTSTRING, 0);
251 entries++;
252 }
253
254 if (user.lastPasswordChange) {
255 userNode->new_path(ctx, "password-last-change", user.lastPasswordChange->c_str(), LYD_ANYDATA_CONSTSTRING, 0);
256 }
257 }
258}
259
260velia::system::Authentication::Authentication(
261 sysrepo::S_Session srSess,
262 const std::string& etc_passwd,
263 const std::string& etc_shadow,
264 const std::string& authorized_keys_format,
265 ChangePassword changePassword
266 )
267 : m_log(spdlog::get("system"))
268 , m_etc_passwd(etc_passwd)
269 , m_etc_shadow(etc_shadow)
270 , m_authorized_keys_format(authorized_keys_format)
271 , m_session(srSess)
272 , m_sub(std::make_shared<sysrepo::Subscribe>(srSess))
273{
274 m_log->debug("Initializing authentication");
275 m_log->debug("Using {} as passwd file", m_etc_passwd);
276 m_log->debug("Using {} as shadow file", m_etc_shadow);
277 m_log->debug("Using {} authorized_keys format", m_authorized_keys_format);
278 utils::ensureModuleImplemented(srSess, "czechlight-system", "2021-01-13");
279
280 sysrepo::OperGetItemsCb listUsersCb = [this] (
Jan Kundrátef2b3802021-02-18 09:57:05 +0100281 auto session,
282 auto,
283 auto,
284 auto,
285 auto,
286 auto& out) {
Václav Kubernátbabbab92021-01-27 09:25:05 +0100287 m_log->debug("Listing users");
288
289 auto users = listUsers();
290 m_log->trace("got {} users", users.size());
291 usersToTree(session->get_context(), users, out);
292
293 return SR_ERR_OK;
294 };
295
296 sysrepo::RpcTreeCb changePasswordCb = [this, changePassword] (
Jan Kundrátef2b3802021-02-18 09:57:05 +0100297 auto session,
298 auto,
299 auto input,
300 auto,
301 auto,
302 auto output) {
Václav Kubernátbabbab92021-01-27 09:25:05 +0100303
Tomáš Peckafd90efb2021-10-07 10:40:44 +0200304 auto userNode = utils::getUniqueSubtree(input, (authentication_container + "/users" ).c_str()).value();
305 auto name = utils::getValueAsString(utils::getUniqueSubtree(userNode, "name").value());
306 auto password = utils::getValueAsString(utils::getUniqueSubtree(userNode, "change-password/password-cleartext").value());
Václav Kubernátbabbab92021-01-27 09:25:05 +0100307 m_log->debug("Changing password for {}", name);
308 try {
Tomáš Peckad9e741f2021-02-10 15:51:17 +0100309 changePassword(name, password, m_etc_shadow);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100310 output->new_path(session->get_context(), "result", "success", LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT);
311 m_log->info("Changed password for {}", name);
312 } catch (std::runtime_error& ex) {
313 output->new_path(session->get_context(), "result", "failure", LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT);
314 output->new_path(session->get_context(), "message", ex.what(), LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT);
315 m_log->info("Failed to change password for {}: {}", name, ex.what());
316 }
317
318 return SR_ERR_OK;
319 };
320
321 sysrepo::RpcTreeCb addKeyCb = [this] (
Jan Kundrátef2b3802021-02-18 09:57:05 +0100322 auto session,
323 auto,
324 auto input,
325 auto,
326 auto,
327 auto output) {
Václav Kubernátbabbab92021-01-27 09:25:05 +0100328
Tomáš Peckafd90efb2021-10-07 10:40:44 +0200329 auto userNode = utils::getUniqueSubtree(input, (authentication_container + "/users").c_str()).value();
330 auto name = utils::getValueAsString(utils::getUniqueSubtree(userNode, "name").value());
331 auto key = utils::getValueAsString(utils::getUniqueSubtree(userNode, "add-authorized-key/key").value());
Václav Kubernátbabbab92021-01-27 09:25:05 +0100332 m_log->debug("Adding key for {}", name);
333 try {
334 addKey(name, key);
335 output->new_path(session->get_context(), "result", "success", LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT);
336 m_log->info("Added a key for {}", name);
337 } catch (AuthException& ex) {
338 output->new_path(session->get_context(), "result", "failure", LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT);
339 output->new_path(session->get_context(), "message", ex.what(), LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT);
340 m_log->warn("Failed to add a key for {}: {}", name, ex.what());
341 }
342
343 return SR_ERR_OK;
344 };
345
346 sysrepo::RpcTreeCb removeKeyCb = [this] (
Jan Kundrátef2b3802021-02-18 09:57:05 +0100347 auto session,
348 auto,
349 auto input,
350 auto,
351 auto,
352 auto output) {
Václav Kubernátbabbab92021-01-27 09:25:05 +0100353
Tomáš Peckafd90efb2021-10-07 10:40:44 +0200354 auto userNode = utils::getUniqueSubtree(input, (authentication_container + "/users").c_str()).value();
355 auto name = utils::getValueAsString(utils::getUniqueSubtree(userNode, "name").value());
356 auto key = std::stol(utils::getValueAsString(utils::getUniqueSubtree(userNode, "authorized-keys/index").value()));
Václav Kubernátbabbab92021-01-27 09:25:05 +0100357 m_log->debug("Removing key for {}", name);
358 try {
359 removeKey(name, key);
360 output->new_path(session->get_context(), "result", "success", LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT);
361 m_log->info("Removed key for {}", name);
362 } catch (AuthException& ex) {
363 output->new_path(session->get_context(), "result", "failure", LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT);
364 output->new_path(session->get_context(), "message", ex.what(), LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT);
365 m_log->warn("Failed to remove a key for {}: {}", name, ex.what());
366 }
367
368 return SR_ERR_OK;
369 };
370
371 m_sub->oper_get_items_subscribe(czechlight_system_module.c_str(), listUsersCb, authentication_container.c_str());
372 m_sub->rpc_subscribe_tree(change_password_action.c_str(), changePasswordCb);
373 m_sub->rpc_subscribe_tree(add_key_action.c_str(), addKeyCb);
374 m_sub->rpc_subscribe_tree(remove_key_action.c_str(), removeKeyCb);
375}