Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 1 | /* |
| 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 | |
| 22 | using namespace std::string_literals; |
| 23 | namespace { |
| 24 | const auto czechlight_system_module = "czechlight-system"s; |
| 25 | const auto authentication_container = "/" + czechlight_system_module + ":authentication"; |
| 26 | const auto change_password_action = "/" + czechlight_system_module + ":authentication/users/change-password"; |
| 27 | const auto add_key_action = "/" + czechlight_system_module + ":authentication/users/add-authorized-key"; |
| 28 | const auto remove_key_action = "/" + czechlight_system_module + ":authentication/users/authorized-keys/remove"; |
| 29 | } |
| 30 | |
| 31 | namespace velia::system { |
| 32 | namespace { |
| 33 | |
| 34 | void 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 | |
| 45 | namespace impl { |
Tomáš Pecka | d9e741f | 2021-02-10 15:51:17 +0100 | [diff] [blame] | 46 | void changePassword(const std::string& name, const std::string& password, const std::string& etc_shadow) |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 47 | { |
| 48 | utils::execAndWait(spdlog::get("system"), CHPASSWD_EXECUTABLE, {}, name + ":" + password); |
Tomáš Pecka | d9e741f | 2021-02-10 15:51:17 +0100 | [diff] [blame] | 49 | auto shadow = velia::utils::readFileToString(etc_shadow); |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 50 | utils::safeWriteFile(BACKUP_ETC_SHADOW_FILE, shadow); |
| 51 | } |
Jan Kundrát | b25ef24 | 2021-02-18 14:56:54 +0100 | [diff] [blame] | 52 | |
| 53 | auto 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át | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 61 | } |
| 62 | |
| 63 | std::string Authentication::homeDirectory(const std::string& username) |
| 64 | { |
Jan Kundrát | b25ef24 | 2021-02-18 14:56:54 +0100 | [diff] [blame] | 65 | auto passwdFile = impl::file_open(m_etc_passwd.c_str(), "r"); |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 66 | passwd entryBuf; |
| 67 | size_t bufLen = 10; |
| 68 | auto buffer = std::make_unique<char[]>(bufLen); |
| 69 | passwd* entry; |
| 70 | |
| 71 | while (true) { |
Jan Kundrát | b25ef24 | 2021-02-18 14:56:54 +0100 | [diff] [blame] | 72 | auto ret = fgetpwent_r(passwdFile.get(), &entryBuf, buffer.get(), bufLen, &entry); |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 73 | if (ret == ERANGE) { |
| 74 | bufLen += 100; |
| 75 | buffer = std::make_unique<char[]>(bufLen); |
| 76 | continue; |
| 77 | } |
Jan Kundrát | b25ef24 | 2021-02-18 14:56:54 +0100 | [diff] [blame] | 78 | |
| 79 | if (ret == ENOENT) { |
| 80 | break; |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 81 | } |
| 82 | |
Jan Kundrát | b25ef24 | 2021-02-18 14:56:54 +0100 | [diff] [blame] | 83 | if (ret != 0) { |
| 84 | throw std::system_error{ret, std::system_category(), "fgetpwent_r() failed"}; |
| 85 | } |
| 86 | |
| 87 | assert(entry); |
| 88 | |
| 89 | if (username != entry->pw_name) { |
| 90 | continue; |
| 91 | } |
| 92 | |
| 93 | return entry->pw_dir; |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 94 | } |
| 95 | |
| 96 | throw std::runtime_error("User " + username + " doesn't exist"); |
| 97 | } |
| 98 | |
Václav Kubernát | 8ea630e | 2021-02-18 16:55:25 +0100 | [diff] [blame] | 99 | std::map<std::string, std::optional<std::string>> Authentication::lastPasswordChanges() |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 100 | { |
Jan Kundrát | b25ef24 | 2021-02-18 14:56:54 +0100 | [diff] [blame] | 101 | auto shadowFile = impl::file_open(m_etc_shadow.c_str(), "r"); |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 102 | spwd entryBuf; |
| 103 | size_t bufLen = 10; |
| 104 | auto buffer = std::make_unique<char[]>(bufLen); |
| 105 | spwd* entry; |
| 106 | |
Václav Kubernát | 8ea630e | 2021-02-18 16:55:25 +0100 | [diff] [blame] | 107 | std::map<std::string, std::optional<std::string>> res; |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 108 | while (true) { |
Jan Kundrát | b25ef24 | 2021-02-18 14:56:54 +0100 | [diff] [blame] | 109 | auto ret = fgetspent_r(shadowFile.get(), &entryBuf, buffer.get(), bufLen, &entry); |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 110 | if (ret == ERANGE) { |
| 111 | bufLen += 100; |
| 112 | buffer = std::make_unique<char[]>(bufLen); |
| 113 | continue; |
| 114 | } |
| 115 | |
Jan Kundrát | b25ef24 | 2021-02-18 14:56:54 +0100 | [diff] [blame] | 116 | if (ret == ENOENT) { |
| 117 | break; |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 118 | } |
| 119 | |
Jan Kundrát | b25ef24 | 2021-02-18 14:56:54 +0100 | [diff] [blame] | 120 | if (ret != 0) { |
| 121 | throw std::system_error{ret, std::system_category(), "fgetspent_r() failed"}; |
| 122 | } |
| 123 | |
| 124 | assert(entry); |
| 125 | |
Jan Kundrát | b25ef24 | 2021-02-18 14:56:54 +0100 | [diff] [blame] | 126 | using namespace std::chrono_literals; |
| 127 | using TimeType = std::chrono::time_point<std::chrono::system_clock>; |
Václav Kubernát | 8ea630e | 2021-02-18 16:55:25 +0100 | [diff] [blame] | 128 | res.emplace(entry->sp_namp, velia::utils::yangTimeFormat(TimeType(24h * entry->sp_lstchg))); |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 129 | } |
| 130 | |
Václav Kubernát | 8ea630e | 2021-02-18 16:55:25 +0100 | [diff] [blame] | 131 | return res; |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 132 | } |
| 133 | |
| 134 | std::string Authentication::authorizedKeysPath(const std::string& username) |
| 135 | { |
| 136 | using namespace fmt::literals; |
| 137 | return fmt::format(m_authorized_keys_format, "USER"_a=username, "HOME"_a=homeDirectory(username)); |
| 138 | } |
| 139 | |
| 140 | std::vector<std::string> Authentication::listKeys(const std::string& username) |
| 141 | { |
| 142 | std::vector<std::string> res; |
| 143 | std::ifstream ifs(authorizedKeysPath(username)); |
| 144 | if (!ifs.is_open()) { |
| 145 | return res; |
| 146 | } |
| 147 | std::string line; |
| 148 | while (std::getline(ifs, line)) { |
| 149 | if (line.find_first_not_of(" \r\t") == std::string::npos) { |
| 150 | continue; |
| 151 | } |
| 152 | |
| 153 | res.emplace_back(line); |
| 154 | } |
| 155 | |
| 156 | return res; |
| 157 | } |
| 158 | |
| 159 | std::vector<User> Authentication::listUsers() |
| 160 | { |
| 161 | std::vector<User> res; |
Jan Kundrát | b25ef24 | 2021-02-18 14:56:54 +0100 | [diff] [blame] | 162 | auto passwdFile = impl::file_open(m_etc_passwd.c_str(), "r"); |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 163 | passwd entryBuf; |
| 164 | size_t bufLen = 10; |
| 165 | auto buffer = std::make_unique<char[]>(bufLen); |
| 166 | passwd* entry; |
| 167 | |
Václav Kubernát | 8ea630e | 2021-02-18 16:55:25 +0100 | [diff] [blame] | 168 | auto pwChanges = lastPasswordChanges(); |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 169 | while (true) { |
Jan Kundrát | b25ef24 | 2021-02-18 14:56:54 +0100 | [diff] [blame] | 170 | auto ret = fgetpwent_r(passwdFile.get(), &entryBuf, buffer.get(), bufLen, &entry); |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 171 | if (ret == ERANGE) { |
| 172 | bufLen += 100; |
| 173 | buffer = std::make_unique<char[]>(bufLen); |
| 174 | continue; |
| 175 | } |
| 176 | |
| 177 | if (ret == ENOENT) { |
| 178 | break; |
| 179 | } |
Jan Kundrát | b25ef24 | 2021-02-18 14:56:54 +0100 | [diff] [blame] | 180 | |
| 181 | if (ret != 0) { |
| 182 | throw std::system_error{ret, std::system_category(), "fgetpwent_r() failed"}; |
| 183 | } |
| 184 | |
| 185 | assert(entry); |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 186 | User user; |
| 187 | user.name = entry->pw_name; |
| 188 | user.authorizedKeys = listKeys(user.name); |
Václav Kubernát | 8ea630e | 2021-02-18 16:55:25 +0100 | [diff] [blame] | 189 | if (auto it = pwChanges.find(user.name); it != pwChanges.end()) { |
| 190 | user.lastPasswordChange = it->second; |
| 191 | } |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 192 | res.emplace_back(user); |
| 193 | } |
| 194 | |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 195 | return res; |
| 196 | } |
| 197 | |
| 198 | void Authentication::addKey(const std::string& username, const std::string& key) |
| 199 | { |
| 200 | try { |
| 201 | utils::execAndWait(spdlog::get("system"), SSH_KEYGEN_EXECUTABLE, {"-l", "-f", "-"}, key, {utils::ExecOptions::DropRoot}); |
| 202 | } catch (std::runtime_error& ex) { |
| 203 | using namespace fmt::literals; |
| 204 | throw AuthException(fmt::format("Key is not a valid SSH public key: {stderr}\n{key}", "stderr"_a=ex.what(), "key"_a=key)); |
| 205 | } |
| 206 | auto currentKeys = listKeys(username); |
| 207 | currentKeys.emplace_back(key); |
| 208 | writeKeys(authorizedKeysPath(username), currentKeys); |
| 209 | } |
| 210 | |
| 211 | void Authentication::removeKey(const std::string& username, const int index) |
| 212 | { |
| 213 | auto currentKeys = listKeys(username); |
| 214 | if (currentKeys.size() == 1) { |
| 215 | // FIXME: maybe add an option to bypass this check? |
| 216 | throw AuthException("Can't remove last key."); |
| 217 | } |
| 218 | currentKeys.erase(currentKeys.begin() + index); |
| 219 | writeKeys(authorizedKeysPath(username), currentKeys); |
| 220 | } |
| 221 | } |
| 222 | |
| 223 | void usersToTree(libyang::S_Context ctx, const std::vector<velia::system::User> users, libyang::S_Data_Node& out) |
| 224 | { |
| 225 | out = std::make_shared<libyang::Data_Node>( |
| 226 | ctx, |
| 227 | authentication_container.c_str(), |
| 228 | nullptr, |
| 229 | LYD_ANYDATA_CONSTSTRING, |
| 230 | 0); |
| 231 | for (const auto& user : users) { |
| 232 | auto userNode = out->new_path(ctx, ("users[name='" + user.name + "']").c_str(), nullptr, LYD_ANYDATA_CONSTSTRING, 0); |
| 233 | |
| 234 | decltype(user.authorizedKeys)::size_type entries = 0; |
| 235 | for (const auto& authorizedKey : user.authorizedKeys) { |
| 236 | auto entry = userNode->new_path(ctx, ("authorized-keys[index='" + std::to_string(entries) + "']").c_str(), nullptr, LYD_ANYDATA_CONSTSTRING, 0); |
| 237 | entry->new_path(ctx, "public-key", authorizedKey.c_str(), LYD_ANYDATA_CONSTSTRING, 0); |
| 238 | entries++; |
| 239 | } |
| 240 | |
| 241 | if (user.lastPasswordChange) { |
| 242 | userNode->new_path(ctx, "password-last-change", user.lastPasswordChange->c_str(), LYD_ANYDATA_CONSTSTRING, 0); |
| 243 | } |
| 244 | } |
| 245 | } |
| 246 | |
| 247 | velia::system::Authentication::Authentication( |
| 248 | sysrepo::S_Session srSess, |
| 249 | const std::string& etc_passwd, |
| 250 | const std::string& etc_shadow, |
| 251 | const std::string& authorized_keys_format, |
| 252 | ChangePassword changePassword |
| 253 | ) |
| 254 | : m_log(spdlog::get("system")) |
| 255 | , m_etc_passwd(etc_passwd) |
| 256 | , m_etc_shadow(etc_shadow) |
| 257 | , m_authorized_keys_format(authorized_keys_format) |
| 258 | , m_session(srSess) |
| 259 | , m_sub(std::make_shared<sysrepo::Subscribe>(srSess)) |
| 260 | { |
| 261 | m_log->debug("Initializing authentication"); |
| 262 | m_log->debug("Using {} as passwd file", m_etc_passwd); |
| 263 | m_log->debug("Using {} as shadow file", m_etc_shadow); |
| 264 | m_log->debug("Using {} authorized_keys format", m_authorized_keys_format); |
| 265 | utils::ensureModuleImplemented(srSess, "czechlight-system", "2021-01-13"); |
| 266 | |
| 267 | sysrepo::OperGetItemsCb listUsersCb = [this] ( |
Jan Kundrát | ef2b380 | 2021-02-18 09:57:05 +0100 | [diff] [blame] | 268 | auto session, |
| 269 | auto, |
| 270 | auto, |
| 271 | auto, |
| 272 | auto, |
| 273 | auto& out) { |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 274 | m_log->debug("Listing users"); |
| 275 | |
| 276 | auto users = listUsers(); |
| 277 | m_log->trace("got {} users", users.size()); |
| 278 | usersToTree(session->get_context(), users, out); |
| 279 | |
| 280 | return SR_ERR_OK; |
| 281 | }; |
| 282 | |
| 283 | sysrepo::RpcTreeCb changePasswordCb = [this, changePassword] ( |
Jan Kundrát | ef2b380 | 2021-02-18 09:57:05 +0100 | [diff] [blame] | 284 | auto session, |
| 285 | auto, |
| 286 | auto input, |
| 287 | auto, |
| 288 | auto, |
| 289 | auto output) { |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 290 | |
| 291 | auto userNode = getSubtree(input, (authentication_container + "/users" ).c_str()); |
| 292 | auto name = getValueAsString(getSubtree(userNode, "name")); |
| 293 | auto password = getValueAsString(getSubtree(userNode, "change-password/password-cleartext")); |
| 294 | m_log->debug("Changing password for {}", name); |
| 295 | try { |
Tomáš Pecka | d9e741f | 2021-02-10 15:51:17 +0100 | [diff] [blame] | 296 | changePassword(name, password, m_etc_shadow); |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 297 | output->new_path(session->get_context(), "result", "success", LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT); |
| 298 | m_log->info("Changed password for {}", name); |
| 299 | } catch (std::runtime_error& ex) { |
| 300 | output->new_path(session->get_context(), "result", "failure", LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT); |
| 301 | output->new_path(session->get_context(), "message", ex.what(), LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT); |
| 302 | m_log->info("Failed to change password for {}: {}", name, ex.what()); |
| 303 | } |
| 304 | |
| 305 | return SR_ERR_OK; |
| 306 | }; |
| 307 | |
| 308 | sysrepo::RpcTreeCb addKeyCb = [this] ( |
Jan Kundrát | ef2b380 | 2021-02-18 09:57:05 +0100 | [diff] [blame] | 309 | auto session, |
| 310 | auto, |
| 311 | auto input, |
| 312 | auto, |
| 313 | auto, |
| 314 | auto output) { |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 315 | |
| 316 | auto userNode = getSubtree(input, (authentication_container + "/users").c_str()); |
| 317 | auto name = getValueAsString(getSubtree(userNode, "name")); |
| 318 | auto key = getValueAsString(getSubtree(userNode, "add-authorized-key/key")); |
| 319 | m_log->debug("Adding key for {}", name); |
| 320 | try { |
| 321 | addKey(name, key); |
| 322 | output->new_path(session->get_context(), "result", "success", LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT); |
| 323 | m_log->info("Added a key for {}", name); |
| 324 | } catch (AuthException& ex) { |
| 325 | output->new_path(session->get_context(), "result", "failure", LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT); |
| 326 | output->new_path(session->get_context(), "message", ex.what(), LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT); |
| 327 | m_log->warn("Failed to add a key for {}: {}", name, ex.what()); |
| 328 | } |
| 329 | |
| 330 | return SR_ERR_OK; |
| 331 | }; |
| 332 | |
| 333 | sysrepo::RpcTreeCb removeKeyCb = [this] ( |
Jan Kundrát | ef2b380 | 2021-02-18 09:57:05 +0100 | [diff] [blame] | 334 | auto session, |
| 335 | auto, |
| 336 | auto input, |
| 337 | auto, |
| 338 | auto, |
| 339 | auto output) { |
Václav Kubernát | babbab9 | 2021-01-27 09:25:05 +0100 | [diff] [blame] | 340 | |
| 341 | auto userNode = getSubtree(input, (authentication_container + "/users").c_str()); |
| 342 | auto name = getValueAsString(getSubtree(userNode, "name")); |
| 343 | auto key = std::stol(getValueAsString(getSubtree(userNode, "authorized-keys/index"))); |
| 344 | m_log->debug("Removing key for {}", name); |
| 345 | try { |
| 346 | removeKey(name, key); |
| 347 | output->new_path(session->get_context(), "result", "success", LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT); |
| 348 | m_log->info("Removed key for {}", name); |
| 349 | } catch (AuthException& ex) { |
| 350 | output->new_path(session->get_context(), "result", "failure", LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT); |
| 351 | output->new_path(session->get_context(), "message", ex.what(), LYD_ANYDATA_CONSTSTRING, LYD_PATH_OPT_OUTPUT); |
| 352 | m_log->warn("Failed to remove a key for {}: {}", name, ex.what()); |
| 353 | } |
| 354 | |
| 355 | return SR_ERR_OK; |
| 356 | }; |
| 357 | |
| 358 | m_sub->oper_get_items_subscribe(czechlight_system_module.c_str(), listUsersCb, authentication_container.c_str()); |
| 359 | m_sub->rpc_subscribe_tree(change_password_action.c_str(), changePasswordCb); |
| 360 | m_sub->rpc_subscribe_tree(add_key_action.c_str(), addKeyCb); |
| 361 | m_sub->rpc_subscribe_tree(remove_key_action.c_str(), removeKeyCb); |
| 362 | } |