blob: 460e53ca1eafffcafa90d7f31e4166736f939079 [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>
Tomáš Pecka72f540e2024-07-08 13:45:22 +020010#include <libyang-cpp/Time.hpp>
Václav Kubernátbabbab92021-01-27 09:25:05 +010011#include <pwd.h>
12#include <shadow.h>
13#include <spdlog/spdlog.h>
14#include <sstream>
15#include "Authentication.h"
16#include "system_vars.h"
17#include "utils/exec.h"
18#include "utils/io.h"
19#include "utils/libyang.h"
20#include "utils/sysrepo.h"
Václav Kubernátbabbab92021-01-27 09:25:05 +010021
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 }
Václav Kubernátb6adbde2021-10-11 22:06:06 +020041
42 std::filesystem::create_directories(std::filesystem::path(filename).parent_path());
43
Václav Kubernátbabbab92021-01-27 09:25:05 +010044 utils::safeWriteFile(filename, ss.str());
45}
46}
47
48namespace impl {
Tomáš Peckad9e741f2021-02-10 15:51:17 +010049void changePassword(const std::string& name, const std::string& password, const std::string& etc_shadow)
Václav Kubernátbabbab92021-01-27 09:25:05 +010050{
51 utils::execAndWait(spdlog::get("system"), CHPASSWD_EXECUTABLE, {}, name + ":" + password);
Tomáš Peckad9e741f2021-02-10 15:51:17 +010052 auto shadow = velia::utils::readFileToString(etc_shadow);
Václav Kubernátbabbab92021-01-27 09:25:05 +010053 utils::safeWriteFile(BACKUP_ETC_SHADOW_FILE, shadow);
54}
Jan Kundrátb25ef242021-02-18 14:56:54 +010055
56auto file_open(const char* filename, const char* mode)
57{
Jan Kundrát343011f2023-11-13 17:40:08 +010058 auto res = std::unique_ptr<std::FILE, decltype([](auto fp) { std::fclose(fp); })>(std::fopen(filename, mode));
Jan Kundrátb25ef242021-02-18 14:56:54 +010059 if (!res.get()) {
60 throw std::system_error{errno, std::system_category(), "fopen("s + filename + ") failed"};
61 }
62 return res;
63}
Václav Kubernátbabbab92021-01-27 09:25:05 +010064}
65
66std::string Authentication::homeDirectory(const std::string& username)
67{
Jan Kundrátb25ef242021-02-18 14:56:54 +010068 auto passwdFile = impl::file_open(m_etc_passwd.c_str(), "r");
Václav Kubernátbabbab92021-01-27 09:25:05 +010069 passwd entryBuf;
70 size_t bufLen = 10;
71 auto buffer = std::make_unique<char[]>(bufLen);
72 passwd* entry;
73
74 while (true) {
Václav Kubernát515cef02021-03-25 05:48:57 +010075 auto pos = ftell(passwdFile.get());
Jan Kundrátb25ef242021-02-18 14:56:54 +010076 auto ret = fgetpwent_r(passwdFile.get(), &entryBuf, buffer.get(), bufLen, &entry);
Václav Kubernátbabbab92021-01-27 09:25:05 +010077 if (ret == ERANGE) {
78 bufLen += 100;
79 buffer = std::make_unique<char[]>(bufLen);
Václav Kubernát515cef02021-03-25 05:48:57 +010080 fseek(passwdFile.get(), pos, SEEK_SET);
Václav Kubernátbabbab92021-01-27 09:25:05 +010081 continue;
82 }
Jan Kundrátb25ef242021-02-18 14:56:54 +010083
84 if (ret == ENOENT) {
85 break;
Václav Kubernátbabbab92021-01-27 09:25:05 +010086 }
87
Jan Kundrátb25ef242021-02-18 14:56:54 +010088 if (ret != 0) {
89 throw std::system_error{ret, std::system_category(), "fgetpwent_r() failed"};
90 }
91
92 assert(entry);
93
94 if (username != entry->pw_name) {
95 continue;
96 }
97
98 return entry->pw_dir;
Václav Kubernátbabbab92021-01-27 09:25:05 +010099 }
100
101 throw std::runtime_error("User " + username + " doesn't exist");
102}
103
Václav Kubernát8ea630e2021-02-18 16:55:25 +0100104std::map<std::string, std::optional<std::string>> Authentication::lastPasswordChanges()
Václav Kubernátbabbab92021-01-27 09:25:05 +0100105{
Jan Kundrátb25ef242021-02-18 14:56:54 +0100106 auto shadowFile = impl::file_open(m_etc_shadow.c_str(), "r");
Václav Kubernátbabbab92021-01-27 09:25:05 +0100107 spwd entryBuf;
108 size_t bufLen = 10;
109 auto buffer = std::make_unique<char[]>(bufLen);
110 spwd* entry;
111
Václav Kubernát8ea630e2021-02-18 16:55:25 +0100112 std::map<std::string, std::optional<std::string>> res;
Václav Kubernátbabbab92021-01-27 09:25:05 +0100113 while (true) {
Václav Kubernát515cef02021-03-25 05:48:57 +0100114 auto pos = ftell(shadowFile.get());
Jan Kundrátb25ef242021-02-18 14:56:54 +0100115 auto ret = fgetspent_r(shadowFile.get(), &entryBuf, buffer.get(), bufLen, &entry);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100116 if (ret == ERANGE) {
117 bufLen += 100;
118 buffer = std::make_unique<char[]>(bufLen);
Václav Kubernát515cef02021-03-25 05:48:57 +0100119 fseek(shadowFile.get(), pos, SEEK_SET);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100120 continue;
121 }
122
Jan Kundrátb25ef242021-02-18 14:56:54 +0100123 if (ret == ENOENT) {
124 break;
Václav Kubernátbabbab92021-01-27 09:25:05 +0100125 }
126
Jan Kundrátb25ef242021-02-18 14:56:54 +0100127 if (ret != 0) {
128 throw std::system_error{ret, std::system_category(), "fgetspent_r() failed"};
129 }
130
131 assert(entry);
132
Jan Kundrátb25ef242021-02-18 14:56:54 +0100133 using namespace std::chrono_literals;
Tomáš Pecka72f540e2024-07-08 13:45:22 +0200134 using TimeType = std::chrono::time_point<std::chrono::system_clock, std::chrono::seconds>;
135 res.emplace(entry->sp_namp, libyang::yangTimeFormat(TimeType(24h * entry->sp_lstchg), libyang::TimezoneInterpretation::Local));
Václav Kubernátbabbab92021-01-27 09:25:05 +0100136 }
137
Václav Kubernát8ea630e2021-02-18 16:55:25 +0100138 return res;
Václav Kubernátbabbab92021-01-27 09:25:05 +0100139}
140
141std::string Authentication::authorizedKeysPath(const std::string& username)
142{
143 using namespace fmt::literals;
Tomáš Peckaaf4abff2023-01-18 12:23:53 +0100144 return fmt::format(fmt::runtime(m_authorized_keys_format), "USER"_a=username, "HOME"_a=homeDirectory(username));
Václav Kubernátbabbab92021-01-27 09:25:05 +0100145}
146
147std::vector<std::string> Authentication::listKeys(const std::string& username)
148{
149 std::vector<std::string> res;
150 std::ifstream ifs(authorizedKeysPath(username));
151 if (!ifs.is_open()) {
152 return res;
153 }
154 std::string line;
155 while (std::getline(ifs, line)) {
156 if (line.find_first_not_of(" \r\t") == std::string::npos) {
157 continue;
158 }
159
160 res.emplace_back(line);
161 }
162
163 return res;
164}
165
166std::vector<User> Authentication::listUsers()
167{
168 std::vector<User> res;
Jan Kundrátb25ef242021-02-18 14:56:54 +0100169 auto passwdFile = impl::file_open(m_etc_passwd.c_str(), "r");
Václav Kubernátbabbab92021-01-27 09:25:05 +0100170 passwd entryBuf;
171 size_t bufLen = 10;
172 auto buffer = std::make_unique<char[]>(bufLen);
173 passwd* entry;
174
Václav Kubernát8ea630e2021-02-18 16:55:25 +0100175 auto pwChanges = lastPasswordChanges();
Václav Kubernátbabbab92021-01-27 09:25:05 +0100176 while (true) {
Václav Kubernát515cef02021-03-25 05:48:57 +0100177 auto pos = ftell(passwdFile.get());
Jan Kundrátb25ef242021-02-18 14:56:54 +0100178 auto ret = fgetpwent_r(passwdFile.get(), &entryBuf, buffer.get(), bufLen, &entry);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100179 if (ret == ERANGE) {
180 bufLen += 100;
181 buffer = std::make_unique<char[]>(bufLen);
Václav Kubernát515cef02021-03-25 05:48:57 +0100182 fseek(passwdFile.get(), pos, SEEK_SET);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100183 continue;
184 }
185
186 if (ret == ENOENT) {
187 break;
188 }
Jan Kundrátb25ef242021-02-18 14:56:54 +0100189
190 if (ret != 0) {
191 throw std::system_error{ret, std::system_category(), "fgetpwent_r() failed"};
192 }
193
194 assert(entry);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100195 User user;
196 user.name = entry->pw_name;
197 user.authorizedKeys = listKeys(user.name);
Václav Kubernát8ea630e2021-02-18 16:55:25 +0100198 if (auto it = pwChanges.find(user.name); it != pwChanges.end()) {
199 user.lastPasswordChange = it->second;
200 }
Václav Kubernátbabbab92021-01-27 09:25:05 +0100201 res.emplace_back(user);
202 }
203
Václav Kubernátbabbab92021-01-27 09:25:05 +0100204 return res;
205}
206
207void Authentication::addKey(const std::string& username, const std::string& key)
208{
209 try {
210 utils::execAndWait(spdlog::get("system"), SSH_KEYGEN_EXECUTABLE, {"-l", "-f", "-"}, key, {utils::ExecOptions::DropRoot});
211 } catch (std::runtime_error& ex) {
212 using namespace fmt::literals;
213 throw AuthException(fmt::format("Key is not a valid SSH public key: {stderr}\n{key}", "stderr"_a=ex.what(), "key"_a=key));
214 }
215 auto currentKeys = listKeys(username);
216 currentKeys.emplace_back(key);
217 writeKeys(authorizedKeysPath(username), currentKeys);
218}
219
220void Authentication::removeKey(const std::string& username, const int index)
221{
222 auto currentKeys = listKeys(username);
223 if (currentKeys.size() == 1) {
224 // FIXME: maybe add an option to bypass this check?
225 throw AuthException("Can't remove last key.");
226 }
227 currentKeys.erase(currentKeys.begin() + index);
228 writeKeys(authorizedKeysPath(username), currentKeys);
229}
230}
231
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100232void usersToTree(libyang::Context ctx, const std::vector<velia::system::User> users, std::optional<libyang::DataNode>& out)
Václav Kubernátbabbab92021-01-27 09:25:05 +0100233{
Jan Kundrátb3e99982022-03-18 17:38:20 +0100234 out = ctx.newPath(authentication_container);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100235 for (const auto& user : users) {
Jan Kundrátb3e99982022-03-18 17:38:20 +0100236 auto userNode = out->newPath("users[name='" + user.name + "']", std::nullopt);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100237
238 decltype(user.authorizedKeys)::size_type entries = 0;
239 for (const auto& authorizedKey : user.authorizedKeys) {
Jan Kundrátb3e99982022-03-18 17:38:20 +0100240 auto entry = userNode->newPath("authorized-keys[index='" + std::to_string(entries) + "']");
241 entry->newPath("public-key", authorizedKey);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100242 entries++;
243 }
244
245 if (user.lastPasswordChange) {
Jan Kundrátb3e99982022-03-18 17:38:20 +0100246 userNode->newPath("password-last-change", user.lastPasswordChange);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100247 }
248 }
249}
250
251velia::system::Authentication::Authentication(
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100252 sysrepo::Session srSess,
Václav Kubernátbabbab92021-01-27 09:25:05 +0100253 const std::string& etc_passwd,
254 const std::string& etc_shadow,
255 const std::string& authorized_keys_format,
256 ChangePassword changePassword
257 )
258 : m_log(spdlog::get("system"))
259 , m_etc_passwd(etc_passwd)
260 , m_etc_shadow(etc_shadow)
261 , m_authorized_keys_format(authorized_keys_format)
262 , m_session(srSess)
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100263 , m_sub()
Václav Kubernátbabbab92021-01-27 09:25:05 +0100264{
265 m_log->debug("Initializing authentication");
266 m_log->debug("Using {} as passwd file", m_etc_passwd);
267 m_log->debug("Using {} as shadow file", m_etc_shadow);
268 m_log->debug("Using {} authorized_keys format", m_authorized_keys_format);
Jan Kundrát7a30cf42022-07-12 22:24:09 +0200269 utils::ensureModuleImplemented(srSess, "czechlight-system", "2022-07-08");
Václav Kubernátbabbab92021-01-27 09:25:05 +0100270
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100271 sysrepo::OperGetCb listUsersCb = [this] (auto session, auto, auto, auto, auto, auto, auto& out) {
Václav Kubernátbabbab92021-01-27 09:25:05 +0100272 m_log->debug("Listing users");
273
274 auto users = listUsers();
275 m_log->trace("got {} users", users.size());
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100276 usersToTree(session.getContext(), users, out);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100277
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100278 return sysrepo::ErrorCode::Ok;
Václav Kubernátbabbab92021-01-27 09:25:05 +0100279 };
280
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100281 sysrepo::RpcActionCb changePasswordCb = [this, changePassword] (auto, auto, auto, auto input, auto, auto, auto output) {
Václav Kubernátbabbab92021-01-27 09:25:05 +0100282
Jan Kundrátb3e99982022-03-18 17:38:20 +0100283 auto userNode = utils::getUniqueSubtree(input, authentication_container + "/users").value();
Tomáš Peckafd90efb2021-10-07 10:40:44 +0200284 auto name = utils::getValueAsString(utils::getUniqueSubtree(userNode, "name").value());
285 auto password = utils::getValueAsString(utils::getUniqueSubtree(userNode, "change-password/password-cleartext").value());
Václav Kubernátbabbab92021-01-27 09:25:05 +0100286 m_log->debug("Changing password for {}", name);
287 try {
Tomáš Peckad9e741f2021-02-10 15:51:17 +0100288 changePassword(name, password, m_etc_shadow);
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100289 output.newPath("result", "success", libyang::CreationOptions::Output);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100290 m_log->info("Changed password for {}", name);
291 } catch (std::runtime_error& ex) {
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100292 output.newPath("result", "failure", libyang::CreationOptions::Output);
293 output.newPath("message", ex.what(), libyang::CreationOptions::Output);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100294 m_log->info("Failed to change password for {}: {}", name, ex.what());
295 }
296
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100297 return sysrepo::ErrorCode::Ok;
Václav Kubernátbabbab92021-01-27 09:25:05 +0100298 };
299
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100300 sysrepo::RpcActionCb addKeyCb = [this] (auto, auto, auto, auto input, auto, auto, auto output) {
Václav Kubernátbabbab92021-01-27 09:25:05 +0100301
Jan Kundrátb3e99982022-03-18 17:38:20 +0100302 auto userNode = utils::getUniqueSubtree(input, authentication_container + "/users").value();
Tomáš Peckafd90efb2021-10-07 10:40:44 +0200303 auto name = utils::getValueAsString(utils::getUniqueSubtree(userNode, "name").value());
304 auto key = utils::getValueAsString(utils::getUniqueSubtree(userNode, "add-authorized-key/key").value());
Václav Kubernátbabbab92021-01-27 09:25:05 +0100305 m_log->debug("Adding key for {}", name);
306 try {
307 addKey(name, key);
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100308 output.newPath("result", "success", libyang::CreationOptions::Output);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100309 m_log->info("Added a key for {}", name);
310 } catch (AuthException& ex) {
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100311 output.newPath("result", "failure", libyang::CreationOptions::Output);
312 output.newPath("message", ex.what(), libyang::CreationOptions::Output);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100313 m_log->warn("Failed to add a key for {}: {}", name, ex.what());
314 }
315
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100316 return sysrepo::ErrorCode::Ok;
Václav Kubernátbabbab92021-01-27 09:25:05 +0100317 };
318
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100319 sysrepo::RpcActionCb removeKeyCb = [this] (auto, auto, auto, auto input, auto, auto, auto output) {
Jan Kundrátb3e99982022-03-18 17:38:20 +0100320 auto userNode = utils::getUniqueSubtree(input, authentication_container + "/users").value();
Tomáš Peckafd90efb2021-10-07 10:40:44 +0200321 auto name = utils::getValueAsString(utils::getUniqueSubtree(userNode, "name").value());
322 auto key = std::stol(utils::getValueAsString(utils::getUniqueSubtree(userNode, "authorized-keys/index").value()));
Václav Kubernátbabbab92021-01-27 09:25:05 +0100323 m_log->debug("Removing key for {}", name);
324 try {
325 removeKey(name, key);
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100326 output.newPath("result", "success", libyang::CreationOptions::Output);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100327 m_log->info("Removed key for {}", name);
328 } catch (AuthException& ex) {
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100329 output.newPath("result", "failure", libyang::CreationOptions::Output);
330 output.newPath("message", ex.what(), libyang::CreationOptions::Output);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100331 m_log->warn("Failed to remove a key for {}: {}", name, ex.what());
332 }
333
Václav Kubernát7efd6d52021-11-09 01:31:11 +0100334 return sysrepo::ErrorCode::Ok;
Václav Kubernátbabbab92021-01-27 09:25:05 +0100335 };
336
Jan Kundrátb3e99982022-03-18 17:38:20 +0100337 m_sub = m_session.onOperGet(czechlight_system_module, listUsersCb, authentication_container);
338 m_sub->onRPCAction(change_password_action, changePasswordCb);
339 m_sub->onRPCAction(add_key_action, addKeyCb);
340 m_sub->onRPCAction(remove_key_action, removeKeyCb);
Václav Kubernátbabbab92021-01-27 09:25:05 +0100341}