Bwi/bugfix round2 (#9)

* Fix log file access, refactor configuration reading class

* Remove bypass option in favor of failmode

* fix loging, print enrolment info

* Add EMAIL method

* Add yubi authentication method

* Add support for verification message

* Add verification

* Made changes in Vagrant's files to run different OSs

* Switch off tests and packages demands to run PAM on Debian 11

* Add authentication totp

* Changes in utils

* Remove unnessesary interface

* Changed vagrant files and postinstal script for Ubuntu 20 and 22

* Moved adding PasswordAuth to vagrant file from posinst

* Added ubuntu 24.04

* Set version

* Poprawki UI

* WebSocket implementation 

* Add totp authentication method

* fixup changes in utils

* Remove unnessesary interface and simplify code

* Remove "default" message handler from WebSocket class

* Change display names of known authentication methods

* Cleanup code in 'main' file

* Add CheckApplication

* Remove unused function

* Changed vagrant files and postinstal script for Ubuntu 20 and 22

* Moved adding PasswordAuth to vagrant file from posinst

* Added ubuntu 24.04

* Set version to 2.0.2

* Proper handle for missing configuration

* Fixup use value of optional object

* Add more vCPU/RAM to vagrant VM's + fix translations

* Minor WS fixes, translations

* Proper handler for Werification error

* Make use of prompt parameter

* Add max number of prompts

* remove unused code, fir includes

* Add Waiting status

* Add check application status check

---------

Co-authored-by: Madzik <m.w@linux.pl>
This commit is contained in:
rublon-bwi 2024-05-28 12:04:20 +02:00 committed by GitHub
parent 8ffa20fffa
commit 9415174eba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
63 changed files with 1442 additions and 659 deletions

0
.gitignore vendored Normal file → Executable file
View File

8
CMakeLists.txt Normal file → Executable file
View File

@ -5,9 +5,9 @@ project(rublon-ssh LANGUAGES CXX)
include(CTest) include(CTest)
include(GNUInstallDirs) include(GNUInstallDirs)
set(PROJECT_VERSION_MAJOR 0) set(PROJECT_VERSION_MAJOR 2)
set(PROJECT_VERSION_MINOR 1) set(PROJECT_VERSION_MINOR 0)
set(PROJECT_VERSION_PATCH 0) set(PROJECT_VERSION_PATCH 1)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED YES) set(CMAKE_CXX_STANDARD_REQUIRED YES)
@ -15,7 +15,7 @@ set(CMAKE_CXX_EXTENSIONS NO)
add_compile_options(-Wall -Wextra -Wpedantic -Wno-format-security) add_compile_options(-Wall -Wextra -Wpedantic -Wno-format-security)
option(ENABLE_TESTS "Enable tests" ON) option(ENABLE_TESTS "Enable tests" OFF)
add_custom_target(CONFIG_IDE SOURCES ${CMAKE_CURRENT_LIST_DIR}/rsc/rublon.config.defaults) add_custom_target(CONFIG_IDE SOURCES ${CMAKE_CURRENT_LIST_DIR}/rsc/rublon.config.defaults)
add_custom_target(INSTSCRIPTS_IDE SUORCES ${CMAKE_CURRENT_LIST_DIR}/service/postinst) add_custom_target(INSTSCRIPTS_IDE SUORCES ${CMAKE_CURRENT_LIST_DIR}/service/postinst)

View File

@ -5,6 +5,7 @@ set(INC
${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/core_handler_interface.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/core_handler_interface.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/curl.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/curl.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/error.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/error.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/finish.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/init.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/init.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/json.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/json.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/method/method_select.hpp ${CMAKE_CURRENT_SOURCE_DIR}/include/rublon/method/method_select.hpp
@ -46,7 +47,8 @@ install(
) )
add_subdirectory(lib) add_subdirectory(lib)
# add_subdirectory(bin)
if(${ENABLE_TESTS}) # if(${ENABLE_TESTS})
add_subdirectory(tests) # add_subdirectory(tests)
endif() # endif()

View File

@ -0,0 +1,30 @@
add_executable(rublon_check_application
rublon_check_application.cpp
)
target_link_options(rublon_check_application
PUBLIC
-fpic
-s
-Wl,--gc-sections
-flto
)
target_link_libraries(rublon_check_application
PUBLIC
rublon-ssh
-lcurl
-lssl
-lcrypto
)
include(GNUInstallDirs)
install(
TARGETS
rublon_check_application
DESTINATION
${CMAKE_INSTALL_SBINDIR}
COMPONENT
PAM
)

View File

@ -0,0 +1,23 @@
#include <rublon/utils.hpp>
#include <rublon/core_handler.hpp>
#include <rublon/curl.hpp>
int main(){
/// read system config
/// if valid send info
using namespace rublon;
details::initLog("CheckApplication");
auto config = ConfigurationFactory{}.systemConfig();
if(not config){
std::cout << "No valid rublon configuration available, check " << details::logPath() << " for more details";
return -1;
}
else{
// run some tests on
}
CoreHandler CH{*config};
}

0
PAM/ssh/extern/tl/expected.hpp vendored Normal file → Executable file
View File

View File

@ -1,13 +1,15 @@
#pragma once #pragma once
#include <cstddef>
#include <iterator>
#include <rublon/core_handler_interface.hpp> #include <rublon/core_handler_interface.hpp>
#include <rublon/pam.hpp> #include <rublon/pam.hpp>
#include <rublon/pam_action.hpp> #include <rublon/pam_action.hpp>
#include <rublon/utils.hpp> #include <rublon/utils.hpp>
#include <string_view>
namespace rublon { namespace rublon {
template < typename Impl >
class AuthenticationStep { class AuthenticationStep {
protected: protected:
std::string _systemToken; std::string _systemToken;
@ -16,19 +18,6 @@ class AuthenticationStep {
public: public:
AuthenticationStep() = default; AuthenticationStep() = default;
AuthenticationStep(std::string systemToken, std::string tid) : _systemToken{std::move(systemToken)}, _tid{std::move(tid)} {} AuthenticationStep(std::string systemToken, std::string tid) : _systemToken{std::move(systemToken)}, _tid{std::move(tid)} {}
template < typename Handler_t >
auto fire(const CoreHandlerInterface< Handler_t > & coreHandler) const {
log(LogLevel::Info, "Starting %s step", static_cast< const Impl * >(this)->name);
return static_cast< const Impl * >(this)->handle(coreHandler);
}
template < typename Handler_t, typename PamInfo_t = LinuxPam >
auto fire(const CoreHandlerInterface< Handler_t > & coreHandler, const PamInfo_t & pam) const {
log(LogLevel::Info, "Starting %s step", static_cast< const Impl * >(this)->name);
return static_cast< const Impl * >(this)->handle(coreHandler, pam);
}
protected: protected:
void addSystemToken(Document & body) const { void addSystemToken(Document & body) const {
auto & alloc = body.GetAllocator(); auto & alloc = body.GetAllocator();
@ -39,6 +28,11 @@ class AuthenticationStep {
auto & alloc = body.GetAllocator(); auto & alloc = body.GetAllocator();
body.AddMember("tid", Value{this->_tid.c_str(), alloc}, alloc); body.AddMember("tid", Value{this->_tid.c_str(), alloc}, alloc);
} }
void addAccessToken(Document & body, std::string_view token) const {
auto & alloc = body.GetAllocator();
body.AddMember("accessToken", Value{token.data(), static_cast< unsigned >(token.length()), alloc}, alloc);
}
}; };
} // namespace rublon } // namespace rublon

View File

@ -0,0 +1,114 @@
#pragma once
#include <rapidjson/prettywriter.h>
#include <rapidjson/rapidjson.h>
#include <rublon/core_handler.hpp>
#include <rublon/curl.hpp>
#include <rublon/json.hpp>
#include <rublon/utils.hpp>
#include <string_view>
namespace rublon {
class Status {
std::string_view _statusDirPath = "/var/lib/rublon";
std::string_view _statusFilePath = "/var/lib/rublon/install.json";
RapidJSONPMRStackAlloc< 4 * 1024 > _alloc;
Document _data;
bool _statusUpdated;
std::string_view _appVersionKey = "/appVer";
std::string_view _appTypeKey = "/type";
std::string_view _paramSystemName = "/params/os";
std::string_view _paramSSHDUsePamName = "/params/sshd_config/usePam";
public:
Status() : _data{&_alloc} {
if(not details::exists(_statusFilePath.data())) {
log(LogLevel::Info, "application first run, creating status file at %s", _statusFilePath.data());
details::mkdir(_statusDirPath.data());
details::touch(_statusFilePath.data());
}
std::ifstream ifs{_statusFilePath.data()};
if(!ifs.is_open()) {
/// TODO handle no file error
}
rapidjson::IStreamWrapper isw{ifs};
_data.ParseStream(isw);
}
void updateAppVersion(std::string_view newVersion) {
RapidJSONPMRStackAlloc< 128 > stackAlloc;
auto version = JSONPointer{_appVersionKey.data(), &stackAlloc}.Get(_data);
if(not version || version->GetString() != newVersion) {
_statusUpdated = true;
auto version = Value{newVersion.data(), _data.GetAllocator()};
JSONPointer{_appVersionKey.data(), &stackAlloc}.Set(_data, version);
}
}
void updateSystemVersion(std::string_view system) {
RapidJSONPMRStackAlloc< 128 > stackAlloc;
auto version = JSONPointer{_paramSystemName.data(), &stackAlloc}.Get(_data);
if(not version || version->GetString() != system) {
_statusUpdated = true;
auto version = Value{system.data(), _data.GetAllocator()};
JSONPointer{_paramSystemName.data(), &stackAlloc}.Set(_data, version);
}
}
bool updated() const {
return _statusUpdated;
}
void save() {
if(updated()) {
memory::Monotonic_1k_HeapResource tmpResource;
RapidJSONPMRAlloc alloc{&tmpResource};
FileWriter s{_statusFilePath};
rapidjson::PrettyWriter< FileWriter, rapidjson::UTF8<>, rapidjson::UTF8<>, RapidJSONPMRAlloc > writer{s, &alloc};
writer.SetIndent(' ', 2);
_data.Accept(writer);
}
}
Document & data() {
return _data;
}
};
class CheckApplication {
tl::expected< bool, Error > persistStatus(Status & status) const {
status.data().RemoveMember("systemToken");
status.save();
return true;
}
public:
template < typename Hander_t >
tl::expected< int, Error > call(const CoreHandlerInterface< Hander_t > & coreHandler, std::string_view systemToken) const {
memory::Monotonic_1k_HeapResource mr;
RapidJSONPMRStackAlloc< 2048 > alloc{};
constexpr std::string_view api = "/api/app/init";
Status status;
const auto persist = [&](const auto /*ok*/) { return this->persistStatus(status); };
status.updateAppVersion("2.0.2");
status.updateSystemVersion(details::osName(&mr));
if(status.updated()) {
auto & alloc = status.data().GetAllocator();
status.data().AddMember("systemToken", Value{systemToken.data(), alloc}, alloc);
return coreHandler.request(alloc, api, status.data()).and_then(persist);
}
return 0;
}
};
} // namespace rublon

99
PAM/ssh/include/rublon/configuration.hpp Normal file → Executable file
View File

@ -7,20 +7,27 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include "utils.hpp" #include <rublon/utils.hpp>
#include <rublon/error.hpp>
#include <rublon/static_string.hpp>
template < class T >
struct is_std_array : std::is_array< T > {};
template < class T, std::size_t N >
struct is_std_array< std::array< T, N > > : std::true_type {};
template < typename T >
constexpr bool is_std_array_v = is_std_array< T >::value;
namespace rublon { namespace rublon {
class ConfigurationFactory; class ConfigurationFactory;
class ConfigurationError { enum class FailMode { bypass, deny };
public:
enum class Cause { NoDefaultValue };
const char * parameterName;
const char * cause;
};
class Configuration { class Configuration {
public: public:
// change to StaticString
std::array< char, 33 > systemToken{}; std::array< char, 33 > systemToken{};
std::array< char, 33 > secretKey{}; std::array< char, 33 > secretKey{};
std::array< char, 300 > apiServer{}; std::array< char, 300 > apiServer{};
@ -28,8 +35,7 @@ class Configuration {
bool enablePasswdEmail{}; bool enablePasswdEmail{};
bool logging{}; bool logging{};
bool autopushPrompt{}; bool autopushPrompt{};
bool bypass{}; FailMode failMode{};
bool offlineBypas{};
}; };
namespace { namespace {
@ -39,19 +45,10 @@ namespace {
template < typename T > template < typename T >
tl::expected< T, ConfigurationError > to(std::string_view); tl::expected< T, ConfigurationError > to(std::string_view);
template <> template < class T >
auto to(std::string_view arg) -> tl::expected< std::array<char, 33>, ConfigurationError > { auto to_array(std::string_view arg) -> tl::expected< T, ConfigurationError > {
assert(arg.size()<=(33-1)); T value{};
assert(arg.size() <= (value.size() - 1));
std::array<char, 33> value{};
std::memcpy(value.data(), arg.data(), arg.size());
return value;
}
template <>
auto to(std::string_view arg) -> tl::expected< std::array<char, 300>, ConfigurationError > {
assert(arg.size()<=(300-1));
std::array<char, 300> value{};
std::memcpy(value.data(), arg.data(), arg.size()); std::memcpy(value.data(), arg.data(), arg.size());
return value; return value;
} }
@ -65,6 +62,28 @@ namespace {
return conv::to_uint32(arg).value_or(0); return conv::to_uint32(arg).value_or(0);
} }
template <>
auto to(std::string_view arg) -> tl::expected< FailMode, ConfigurationError > {
if(arg == "safe" || "bypass")
return FailMode::bypass;
if(arg == "secure" || arg == "deny")
return FailMode::deny;
return tl::unexpected{ConfigurationError{ConfigurationError::ErrorClass::BadFailMode}};
}
template < typename T >
auto parse(std::string_view arg) -> tl::expected< T, ConfigurationError > {
if(arg.empty()) {
return tl::unexpected{ConfigurationError::ErrorClass::Empty};
} else {
if constexpr(is_std_array_v< T >) {
return to_array< T >(arg);
} else {
return to< T >(arg);
}
}
}
} // namespace } // namespace
struct Entry { struct Entry {
enum class Source { UserInput, DefaultValue }; enum class Source { UserInput, DefaultValue };
@ -72,12 +91,20 @@ struct Entry {
static constexpr auto make_read_function() { static constexpr auto make_read_function() {
using pType = decltype(member_ptr_t(member)); using pType = decltype(member_ptr_t(member));
return [](const Entry * _this, return
Configuration * configuration, [](const Entry * _this, Configuration * configuration, std::string_view userInput) -> tl::expected< Source, ConfigurationError > {
std::string_view userInput) -> tl::expected< Source, ConfigurationError > { const auto setDefaultValue = [&](const ConfigurationError & error) -> tl::expected< Source, ConfigurationError > {
const auto setDefaultValue = [&](const auto & value) -> tl::expected< Source, ConfigurationError > { log(LogLevel::Warning, "applying user provided value for %s parameter, faild with %s", _this->name, error.what());
configuration->*member = value; if(_this->defaultValue != nullptr) {
configuration->*member = parse< pType >(_this->defaultValue).value();
return Source::DefaultValue; return Source::DefaultValue;
} else {
log(LogLevel::Error, "parameter %s has not been found and has no default value", _this->name);
if(userInput.empty())
return tl::unexpected{ConfigurationError::ErrorClass::RequiredValueNotFound};
else
return tl::unexpected{ConfigurationError::ErrorClass::BadInput};
}
}; };
const auto saveValue = [&](const auto & value) -> tl::expected< Source, ConfigurationError > { const auto saveValue = [&](const auto & value) -> tl::expected< Source, ConfigurationError > {
@ -85,19 +112,7 @@ struct Entry {
return Source::UserInput; return Source::UserInput;
}; };
const auto returnBadInput = [&](const auto & /*error*/) -> tl::expected< Source, ConfigurationError > { return parse< pType >(userInput).and_then(saveValue).or_else(setDefaultValue);
return tl::unexpected{ConfigurationError{"", ""}};
};
if(userInput.empty()) {
if(_this->defaultValue != nullptr) {
return to< pType >(_this->defaultValue).and_then(setDefaultValue).or_else(returnBadInput);
} else {
return tl::unexpected{ConfigurationError{_this->name, "No default value"}};
}
}
return to< pType >(userInput).and_then(saveValue).or_else(returnBadInput);
}; };
} }
@ -118,7 +133,7 @@ struct Entry {
const auto logError = [&](const auto & error) -> tl::expected< Source, ConfigurationError > { const auto logError = [&](const auto & error) -> tl::expected< Source, ConfigurationError > {
rublon::log(LogLevel::Error, rublon::log(LogLevel::Error,
"Configuration parameter '%s' is has no default value and is not provided in user configuraion, aborting", "Configuration parameter '%s' has no default value and is not provided in user configuraion, aborting",
this->name); this->name);
return tl::unexpected{error}; return tl::unexpected{error};
}; };
@ -140,7 +155,7 @@ constexpr static inline std::array< Entry, 8 > configurationVariables = { //
make_entry< &Configuration::prompt >("prompt", "1"), make_entry< &Configuration::prompt >("prompt", "1"),
make_entry< &Configuration::enablePasswdEmail >("enablePasswdEmail", "true"), make_entry< &Configuration::enablePasswdEmail >("enablePasswdEmail", "true"),
make_entry< &Configuration::autopushPrompt >("autopushPrompt", "false"), make_entry< &Configuration::autopushPrompt >("autopushPrompt", "false"),
make_entry< &Configuration::offlineBypas >("failMode", "bypas")}; make_entry< &Configuration::failMode >("failMode", "deny")};
class ConfigurationFactory { class ConfigurationFactory {
public: public:

111
PAM/ssh/include/rublon/core_handler.hpp Normal file → Executable file
View File

@ -1,5 +1,6 @@
#pragma once #pragma once
#include <memory>
#include <memory_resource> #include <memory_resource>
#include <string> #include <string>
@ -7,61 +8,87 @@
#include <rublon/core_handler_interface.hpp> #include <rublon/core_handler_interface.hpp>
#include <rublon/curl.hpp> #include <rublon/curl.hpp>
#include <rublon/json.hpp> #include <rublon/json.hpp>
#include <rublon/pam_action.hpp>
#include <rublon/sign.hpp> #include <rublon/sign.hpp>
#include <rublon/utils.hpp>
#include <rublon/websockets.hpp> #include <rublon/websockets.hpp>
#include <rublon/pam_action.hpp>
#include <tl/expected.hpp> #include <tl/expected.hpp>
#include <rublon/core_handler_interface.hpp>
namespace rublon { namespace rublon {
template < typename HttpHandler = CURL > template < typename HttpHandler = CURL >
class CoreHandler : public CoreHandlerInterface< CoreHandler< HttpHandler > > { class CoreHandler : public CoreHandlerInterface< CoreHandler< HttpHandler > > {
std::string secretKey; std::string secretKey;
std::string url; std::string rublonCore;
bool bypass;
mutable std::unique_ptr< WebSocket > _ws;
void signRequest(Request & request) const { void signRequest(Request & request) const {
request.headers["X-Rublon-Signature"] = std::pmr::string{signData(request.body, secretKey).data(), request.headers.get_allocator()}; request.headers["X-Rublon-Signature"] = std::pmr::string{signData(request.body, secretKey).data(), request.headers.get_allocator()};
} }
bool responseSigned(const Response & response) const { bool hasSignature(const Response & response) const {
if(not response.headers.count("x-rublon-signature")) {
log(LogLevel::Error, "No x-rublon-signature");
return false;
}
return true;
}
// bool hasRateLimit(const Response & response)const {
// if(not response.headers.count("x-ratelimit-remaining")) {
// log(LogLevel::Error, "No x-ratelimit-remaining");
// return false;
// }
// return true;
// }
// bool rateIsAcceptable(const Response &response)const{
//// todo validate rate type and number
// }
bool signatureIsNatValid(const Response & response) const {
const auto & xRubResp = response.headers.at("x-rublon-signature"); const auto & xRubResp = response.headers.at("x-rublon-signature");
const auto & sign = signData(response.body, secretKey); const auto & sign = signData(response.body, secretKey);
const bool signatureMatch = xRubResp == sign.data(); const bool signatureMatch = xRubResp == sign.data();
if(not signatureMatch) if(not signatureMatch)
log(LogLevel::Error, "Signature mismatch %s != %s ", xRubResp.c_str(), sign.data()); log(LogLevel::Error, "Signature mismatch %s != %s ", xRubResp.c_str(), sign.data());
return signatureMatch; return not signatureMatch;
} }
bool containsException(const Document & coreResponse) const { bool hasException(const Document & coreResponse) const {
using namespace std::string_view_literals; using namespace std::string_view_literals;
return coreResponse.HasMember("status") and coreResponse["status"].GetString() == "ERROR"sv; return coreResponse.HasMember("status") and coreResponse["status"].GetString() == "ERROR"sv;
} }
bool isUnHealthy(const Document & coreResponse) const { bool isUnHealthy(const Document & coreResponse) const {
return coreResponse.HasParseError() or not coreResponse.HasMember("result"); return coreResponse.HasParseError();
} }
protected: protected:
HttpHandler http{}; HttpHandler http{};
public: public:
CoreHandler(const Configuration & config) CoreHandler(const Configuration & config) : secretKey{config.secretKey.data()}, rublonCore{config.apiServer.data()}, http{} {}
: secretKey{config.secretKey.data()}, url{config.apiServer.data()}, bypass{config.bypass}, http{} {}
tl::expected< std::reference_wrapper< const Response >, Error > validateSignature(const Response & response) const { tl::expected< std::reference_wrapper< const Response >, Error > validateSignature(const Response & response) const {
if(not responseSigned(response)) { if(hasSignature(response) and signatureIsNatValid(response)) {
log(LogLevel::Error, "rublon core response is not signed"); log(LogLevel::Error, "rublon core response is not signed properly");
return tl::unexpected{CoreHandlerError{CoreHandlerError::BadSigature}}; return tl::unexpected{CoreHandlerError{CoreHandlerError::BadSigature}};
} }
return response; return response;
} }
// tl::expected< std::reference_wrapper< const Response >, Error > validateRate(const Response & response) const {
// if(hasSignature(response) and signatureIsNatValid(response)) {
// log(LogLevel::Error, "rublon core response is not signed properly");
// return tl::unexpected{CoreHandlerError{CoreHandlerError::BadSigature}};
// }
// return response;
// }
tl::expected< Document, Error > validateResponse(RapidJSONPMRAlloc & alloc, const Response & response) const { tl::expected< Document, Error > validateResponse(RapidJSONPMRAlloc & alloc, const Response & response) const {
Document resp{&alloc}; Document resp{&alloc};
resp.Parse(response.body.c_str()); resp.Parse(response.body.c_str());
@ -71,40 +98,40 @@ class CoreHandler : public CoreHandlerInterface< CoreHandler< HttpHandler > > {
if(isUnHealthy(resp)) { if(isUnHealthy(resp)) {
log(LogLevel::Error, "Rublon Core responded with broken data"); log(LogLevel::Error, "Rublon Core responded with broken data");
return tl::unexpected{CoreHandlerError{CoreHandlerError::BrokenData}}; return tl::unexpected{CoreHandlerError{CoreHandlerError::BrokenData}};
} } else if(hasException(resp)) {
else if(containsException(resp)) {
const auto * exception = JSONPointer{"/result/exception", &alloc}.Get(resp); const auto * exception = JSONPointer{"/result/exception", &alloc}.Get(resp);
log(LogLevel::Error, "Rublon Core responded with '%s' exception", exception); log(LogLevel::Error, "Rublon Core responded with '%s' exception", exception->GetString());
return handleCoreException(exception->GetString()); return handleCoreException(exception->GetString());
} else if(not hasSignature(response)) {
// additional check for mallformed responses (A invalid response, without any x-rublon-signature will stop at this check)
return tl::unexpected{CoreHandlerError{CoreHandlerError::BadSigature}};
} }
return resp; return resp;
} }
tl::unexpected< Error > handleCoreException(std::string_view exceptionString) const { tl::unexpected< Error > handleCoreException(std::string_view exceptionString) const {
if(exceptionString == "UserBypassedException" or exceptionString == "UserNotFoundException") { // can happen only dyring check application step
return tl::unexpected{Error{RublonAuthenticationInterrupt{RublonAuthenticationInterrupt::UserBaypass}}}; if(auto error = RublonCheckApplicationException::fromString(exceptionString); error.has_value())
} else { return tl::unexpected{Error{error.value()}};
// auth problems likely bad configuration in rublon admin console
if(auto error = RublonAuthenticationInterrupt::fromString(exceptionString); error.has_value())
return tl::unexpected{Error{error.value()}};
// verification error wrong passcode etc.
if(auto error = WerificationError::fromString(exceptionString); error.has_value())
return tl::unexpected{Error{error.value()}};
// other exceptions, just "throw"
return tl::unexpected{ return tl::unexpected{
Error{CoreHandlerError{CoreHandlerError::RublonCoreException, std::string{exceptionString.data(), exceptionString.size()}}}}; Error{CoreHandlerError{CoreHandlerError::RublonCoreException, std::string{exceptionString.data(), exceptionString.size()}}}};
} }
}
tl::expected< Document, Error > handleError(const Error & error) const { tl::expected< Document, Error > handleError(const Error & error) const {
return tl::unexpected{Error{error}}; return tl::unexpected{Error{error}};
} }
template < typename T >
static void stringifyTo(const Document & body, T & to) {
memory::Monotonic_2k_HeapResource tmpResource;
RapidJSONPMRAlloc alloc{&tmpResource};
StringBuffer jsonStr{&alloc};
Writer writer{jsonStr, &alloc};
body.Accept(writer);
to = jsonStr.GetString();
}
tl::expected< Document, Error > request(RapidJSONPMRAlloc & mr, std::string_view path, const Document & body) const { tl::expected< Document, Error > request(RapidJSONPMRAlloc & mr, std::string_view path, const Document & body) const {
memory::StrictMonotonic_8k_HeapResource memoryResource; memory::StrictMonotonic_8k_HeapResource memoryResource;
@ -118,9 +145,10 @@ class CoreHandler : public CoreHandlerInterface< CoreHandler< HttpHandler > > {
request.headers["Content-Type"] = pmrs("application/json"); request.headers["Content-Type"] = pmrs("application/json");
request.headers["Accept"] = pmrs("application/json"); request.headers["Accept"] = pmrs("application/json");
stringifyTo(body, request.body); stringifyTo(body, request.body);
signRequest(request); signRequest(request);
std::pmr::string uri{url + path.data(), &memoryResource}; std::pmr::string uri{rublonCore + path.data(), &memoryResource};
return http return http
.request(uri, request, response) // .request(uri, request, response) //
@ -129,17 +157,16 @@ class CoreHandler : public CoreHandlerInterface< CoreHandler< HttpHandler > > {
.or_else(handleError); .or_else(handleError);
} }
///TODO use WS_Handler type to allow mocking / changind implementation of WS bool createWSConnection(std::string_view tid) const {
tl::expected< AuthenticationStatus, Error > waitForConfirmation(std::string_view tid) const { if(not _ws) {
bool aproved{}; _ws = std::make_unique< WebSocket >();
}
WebSocket ws; return _ws->AttachToCore(rublonCore, tid);
aproved = ws.connect(url, tid); }
log(LogLevel::Info, "websocket :s", aproved ? "approved" : "denied"); auto listen() const {
return _ws->listen();
return aproved ? AuthenticationStatus{AuthenticationStatus::Action::Confirmed} :
AuthenticationStatus{AuthenticationStatus::Action::Denied};
} }
}; };

13
PAM/ssh/include/rublon/core_handler_interface.hpp Normal file → Executable file
View File

@ -4,22 +4,27 @@
#include <rublon/error.hpp> #include <rublon/error.hpp>
#include <rublon/json.hpp> #include <rublon/json.hpp>
#include <rublon/utils.hpp>
#include <rublon/pam_action.hpp> #include <rublon/pam_action.hpp>
#include <rublon/utils.hpp>
namespace rublon { namespace rublon {
template < typename Impl > template < typename Impl >
class CoreHandlerInterface { class CoreHandlerInterface {
public: public:
tl::expected< Document, Error > request(RapidJSONPMRAlloc & mr, std::string_view path, const rublon::Document & body) const { tl::expected< Document, Error > request(RapidJSONPMRAlloc & mr, std::string_view path, const Document & body) const {
rublon::log(LogLevel::Debug, "%s", "CoreHandlerInterface::request"); rublon::log(LogLevel::Debug, "%s", "CoreHandlerInterface::request");
return static_cast< const Impl * >(this)->request(mr, path, body); return static_cast< const Impl * >(this)->request(mr, path, body);
} }
tl::expected< AuthenticationStatus, Error > waitForConfirmation(std::string_view tid) const { bool createWSConnection(std::string_view tid) const {
rublon::log(LogLevel::Debug, "%s", "CoreHandlerInterface::listen"); rublon::log(LogLevel::Debug, "%s", "CoreHandlerInterface::listen");
return static_cast< const Impl * >(this)->waitForConfirmation(tid); return static_cast< const Impl * >(this)->createWSConnection(tid);
}
auto listen() const {
rublon::log(LogLevel::Debug, "%s", "CoreHandlerInterface::listen");
return static_cast< const Impl * >(this)->listen();
} }
}; };
} // namespace rublon } // namespace rublon

8
PAM/ssh/include/rublon/curl.hpp Normal file → Executable file
View File

@ -6,10 +6,9 @@
#include <curl/curl.h> #include <curl/curl.h>
#include <functional> #include <functional>
#include <memory> #include <memory>
#include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <vector> #include <map>
#include <memory_resource> #include <memory_resource>
@ -78,10 +77,10 @@ class CURL {
tl::expected< std::reference_wrapper< Response >, ConnectionError > tl::expected< std::reference_wrapper< Response >, ConnectionError >
request(std::string_view uri, const Request & request, Response & response) const { request(std::string_view uri, const Request & request, Response & response) const {
memory::MonotonicStackResource< 8 * 1024 > stackResource; memory::MonotonicStackResource< 4 * 1024 > stackResource;
std::pmr::string response_data{&stackResource}; std::pmr::string response_data{&stackResource};
response_data.reserve(7000); response_data.reserve(3000);
/// TODO this can be done on stack using pmr /// TODO this can be done on stack using pmr
auto curl_headers = std::unique_ptr< curl_slist, void (*)(curl_slist *) >(nullptr, curl_slist_free_all); auto curl_headers = std::unique_ptr< curl_slist, void (*)(curl_slist *) >(nullptr, curl_slist_free_all);
@ -100,7 +99,6 @@ class CURL {
curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, WriteMemoryCallback); curl_easy_setopt(curl.get(), CURLOPT_WRITEFUNCTION, WriteMemoryCallback);
curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response_data); curl_easy_setopt(curl.get(), CURLOPT_WRITEDATA, &response_data);
log(LogLevel::Debug, "Sending request to %s", uri.data()); log(LogLevel::Debug, "Sending request to %s", uri.data());
for(const auto &[name, value] : request.headers){ for(const auto &[name, value] : request.headers){
log(LogLevel::Debug, "Header %s:%s", name.c_str(), value.c_str()); log(LogLevel::Debug, "Header %s:%s", name.c_str(), value.c_str());

149
PAM/ssh/include/rublon/error.hpp Normal file → Executable file
View File

@ -1,6 +1,10 @@
#pragma once #pragma once
#include <algorithm>
#include <cstddef>
#include <optional>
#include <rublon/non_owning_ptr.hpp> #include <rublon/non_owning_ptr.hpp>
#include <string_view>
#include <tl/expected.hpp> #include <tl/expected.hpp>
#include <string> #include <string>
@ -8,21 +12,41 @@
namespace rublon { namespace rublon {
template < typename... Types >
constexpr std::array< std::string_view, sizeof...(Types) > make_array(Types... names) {
return {std::forward< Types >(names)...};
}
constexpr auto test = make_array("one", "two");
#define names constexpr static inline const char * errorClassPrettyName[] #define names constexpr static inline const char * errorClassPrettyName[]
class ConfigurationError {
public:
enum class ErrorClass { RequiredValueNotFound, BadFailMode, BadInput, Empty };
constexpr static auto errorClassPrettyName = make_array("RequiredValueNotFound", "BadFailMode", "BadInput", "Empty");
constexpr static auto prettyName = "Configurtion Error";
constexpr ConfigurationError(ErrorClass e = ErrorClass::RequiredValueNotFound) : errorClass{e} {}
constexpr const char * what() const {
return errorClassPrettyName[static_cast< int >(errorClass)].data();
}
ErrorClass errorClass;
};
class ConnectionError { class ConnectionError {
public: public:
enum ErrorClass { Timeout, HttpError }; enum ErrorClass { Timeout, HttpError, ClientError };
names = {"Timeout", "Error"}; constexpr static auto errorClassPrettyName = make_array("Timeout", "Error", "Client Error");
constexpr static auto prettyName = "Connection Error";
constexpr static inline auto prettyName = "Connection Error";
constexpr ConnectionError() : errorClass{Timeout}, httpCode(200) {} constexpr ConnectionError() : errorClass{Timeout}, httpCode(200) {}
constexpr ConnectionError(ErrorClass e, long httpCode) : errorClass{e}, httpCode(httpCode) {} constexpr ConnectionError(ErrorClass e, long httpCode) : errorClass{e}, httpCode(httpCode) {}
constexpr const char * what() const { constexpr const char * what() const {
return errorClassPrettyName[static_cast< int >(errorClass)]; return errorClassPrettyName[static_cast< int >(errorClass)].data();
} }
ErrorClass errorClass; ErrorClass errorClass;
@ -31,33 +55,34 @@ class ConnectionError {
class CoreHandlerError { class CoreHandlerError {
public: public:
enum ErrorClass { BadSigature, RublonCoreException, BrokenData }; enum ErrorClass { BadSigature, RublonCoreException, BrokenData, APIException, TransactionException };
names = {"BadSigature", "RublonCoreException", "BrokenData"}; constexpr static auto errorClassPrettyName =
make_array("BadSigature", "RublonCoreException", "BrokenData", "APIException", "TransactionException");
constexpr static auto prettyName = "Core Handler Error";
constexpr static inline auto prettyName = "Core Handler Error"; // APIException -> code: 10
// TransactionException -> code: 11
CoreHandlerError(ErrorClass e = BadSigature) : errorClass{e} {} CoreHandlerError(ErrorClass e = BadSigature, std::string r = "") : errorClass{e}, reson{std::move(r)} {}
CoreHandlerError(ErrorClass e, std::string r) : errorClass{e}, reson{std::move(r)} {}
constexpr const char * what() const { constexpr const char * what() const {
return errorClassPrettyName[static_cast< int >(errorClass)]; return errorClassPrettyName[static_cast< int >(errorClass)].data();
} }
ErrorClass errorClass; ErrorClass errorClass;
std::string reson; std::string reson; // TODO dynamic mem
}; };
class MethodError { class MethodError {
public: public:
enum ErrorClass { BadMethod, BadUserInput, NoMethodAvailable }; enum ErrorClass { BadMethod, BadUserInput, NoMethodAvailable };
names = {"BadMethod", "BadUserInput", "NoMethodAvailable"}; constexpr static auto errorClassPrettyName = make_array("BadMethod", "BadUserInput", "NoMethodAvailable");
constexpr static auto prettyName = "Method Error";
constexpr static inline auto prettyName = "Method Error";
constexpr MethodError(ErrorClass e = BadMethod) : errorClass{e} {} constexpr MethodError(ErrorClass e = BadMethod) : errorClass{e} {}
constexpr const char * what() const { constexpr const char * what() const {
return errorClassPrettyName[static_cast< int >(errorClass)]; return errorClassPrettyName[static_cast< int >(errorClass)].data();
} }
ErrorClass errorClass; ErrorClass errorClass;
@ -65,15 +90,26 @@ class MethodError {
class WerificationError { class WerificationError {
public: public:
enum ErrorClass { WrongCode }; enum ErrorClass {
names = {"WrongCode"}; BadInput, // User input has incorrect characters or length
PasscodeException // Exception from core
};
constexpr static auto errorClassPrettyName = make_array("BadInput", "PasscodeException");
constexpr static inline auto prettyName = "Werification Error"; constexpr static inline auto prettyName = "Werification Error";
constexpr WerificationError(ErrorClass e = WrongCode) : errorClass{e} {} constexpr WerificationError(ErrorClass e = PasscodeException) : errorClass{e} {}
constexpr const char * what() const { constexpr const char * what() const {
return errorClassPrettyName[static_cast< int >(errorClass)]; return errorClassPrettyName[static_cast< int >(errorClass)].data();
}
static std::optional< WerificationError > fromString(std::string_view name) {
for(std::size_t i{}; i < errorClassPrettyName.size(); i++) {
if(errorClassPrettyName.at(i) == name) {
return std::make_optional(WerificationError{static_cast< ErrorClass >(i)});
}
}
return std::nullopt;
} }
ErrorClass errorClass; ErrorClass errorClass;
@ -81,35 +117,85 @@ class WerificationError {
class RublonAuthenticationInterrupt { class RublonAuthenticationInterrupt {
public: public:
enum ErrorClass { UserBaypass, UserDenied, UserPending }; enum ErrorClass { UserBaypass, UserDenied, UserPending, UserWaiting, UserNotFound };
names = {"UserBaypass", "UserDenied", "UserPending"}; constexpr static auto errorClassPrettyName =
make_array("UserBypassedException", "UserDenied", "UserPending", "UserWaiting", "UserNotFoundException");
constexpr static inline auto prettyName = "Rublon Authentication Interrupt"; constexpr static auto prettyName = "Rublon Authentication Interrupt";
RublonAuthenticationInterrupt(ErrorClass e = UserBaypass) : errorClass{e} {} RublonAuthenticationInterrupt(ErrorClass e = UserBaypass) : errorClass{e} {}
constexpr const char * what() const { constexpr const char * what() const {
return errorClassPrettyName[static_cast< int >(errorClass)]; return errorClassPrettyName[static_cast< int >(errorClass)].data();
}
static std::optional< RublonAuthenticationInterrupt > fromString(std::string_view name) {
for(std::size_t i{}; i < errorClassPrettyName.size(); i++) {
if(errorClassPrettyName.at(i) == name) {
return std::make_optional(RublonAuthenticationInterrupt{static_cast< ErrorClass >(i)});
}
}
return std::nullopt;
}
ErrorClass errorClass;
};
class RublonCheckApplicationException {
public:
enum ErrorClass { ApplicationNotFoundException, InvalidSignatureException, UnsupportedVersionException };
constexpr static auto errorClassPrettyName =
make_array("ApplicationNotFoundException", "InvalidSignatureException", "UnsupportedVersionException");
constexpr static auto prettyName = "Rublon Check Application Interrupt";
RublonCheckApplicationException(ErrorClass e = ApplicationNotFoundException) : errorClass{e} {}
constexpr const char * what() const {
return errorClassPrettyName[static_cast< int >(errorClass)].data();
}
static std::optional< RublonCheckApplicationException > fromString(std::string_view name) {
for(std::size_t i{}; i < errorClassPrettyName.size(); i++) {
if(errorClassPrettyName.at(i) == name) {
return std::make_optional(RublonCheckApplicationException{static_cast< ErrorClass >(i)});
}
}
return std::nullopt;
} }
ErrorClass errorClass; ErrorClass errorClass;
}; };
class Error { class Error {
using Error_t = using Error_t = std::variant< ConfigurationError,
std::variant< CoreHandlerError, ConnectionError, WerificationError, MethodError, RublonAuthenticationInterrupt >; CoreHandlerError,
ConnectionError,
WerificationError,
MethodError,
RublonAuthenticationInterrupt,
RublonCheckApplicationException >;
Error_t _error; Error_t _error;
public: public:
enum Category { k_CoreHandlerError, k_ConnectionError, k_WerificationError, k_MethodError, k_RublonAuthenticationInterrupt }; enum Category {
k_ConfigurationError,
k_CoreHandlerError,
k_ConnectionError,
k_WerificationError,
k_MethodError,
k_RublonAuthenticationInterrupt,
k_RublonCheckApplication
};
Error() = default; Error() = default;
Error(ConfigurationError error) : _error{error} {}
Error(CoreHandlerError error) : _error{error} {} Error(CoreHandlerError error) : _error{error} {}
Error(ConnectionError error) : _error{error} {} Error(ConnectionError error) : _error{error} {}
Error(MethodError error) : _error{error} {} Error(MethodError error) : _error{error} {}
Error(WerificationError error) : _error{error} {} Error(WerificationError error) : _error{error} {}
Error(RublonAuthenticationInterrupt error) : _error{error} {} Error(RublonAuthenticationInterrupt error) : _error{error} {}
Error(RublonCheckApplicationException error) : _error{error} {}
Error(const Error &) = default; Error(const Error &) = default;
Error(Error &&) = default; Error(Error &&) = default;
@ -147,6 +233,11 @@ class Error {
return category() == Error{E{}}.category(); return category() == Error{E{}}.category();
} }
template < typename E >
constexpr bool is(typename E::ErrorClass errorClass) const {
return is< E >() && hasSameErrorClassAs(errorClass);
}
template < typename E > template < typename E >
constexpr bool isSameCategoryAs(const E & e) const { constexpr bool isSameCategoryAs(const E & e) const {
return category() == Error{e}.category(); return category() == Error{e}.category();

View File

@ -0,0 +1,44 @@
#pragma once
#include <rublon/json.hpp>
#include <rublon/pam.hpp>
#include <rublon/authentication_step_interface.hpp>
#include <rublon/configuration.hpp>
#include <rublon/method/method_select.hpp>
#include <sys/utsname.h>
namespace rublon {
class Finish : public AuthenticationStep {
const char * apiPath = "/api/transaction/credentials";
const std::string _accessToken;
void addAccessToken(Document & coreRequest) const {
auto & alloc = coreRequest.GetAllocator();
coreRequest.AddMember("accessToken", Value{_accessToken.c_str(), alloc}, alloc);
}
tl::expected< bool, Error > returnOk(const Document & /*coreResponse*/) const {
return true;
}
public:
const char * name = "Finalization";
Finish(const rublon::Configuration & config, std::string accessToken) : AuthenticationStep(config.systemToken.data(), ""), _accessToken{std::move(accessToken)} {}
template < typename Hander_t >
tl::expected< bool, Error > handle(const CoreHandlerInterface< Hander_t > & coreHandler) const {
const auto returnOk = [&](const auto & coreResponse) { return this->returnOk(coreResponse); };
RapidJSONPMRStackAlloc< 2048 > alloc{};
Document body{rapidjson::kObjectType, &alloc};
this->addSystemToken(body);
this->addAccessToken(body);
return coreHandler
.request(alloc, apiPath, body) //
.and_then(returnOk);
}
};
} // namespace rublon

90
PAM/ssh/include/rublon/init.hpp Normal file → Executable file
View File

@ -2,59 +2,37 @@
#include <rublon/json.hpp> #include <rublon/json.hpp>
#include <rublon/pam.hpp> #include <rublon/pam.hpp>
#include <rublon/session.hpp>
#include <rublon/authentication_step_interface.hpp> #include <rublon/authentication_step_interface.hpp>
#include <rublon/configuration.hpp> #include <rublon/configuration.hpp>
#include <rublon/method/method_select.hpp> #include <rublon/method/method_select.hpp>
#include <string>
#include <sys/utsname.h> #include <sys/utsname.h>
namespace rublon { namespace rublon {
class Verify {}; /// TODO move to some other place
using PAM = LinuxPam;
using Session = SessionBase< PAM, CoreHandler< CURL > >;
using Transaction = TransactionBase< Session >;
} // namespace rublon } // namespace rublon
extern std::string g_tid;
namespace rublon { namespace rublon {
std::pmr::string osName(std::pmr::memory_resource *mr) {
memory::MonotonicStackResource< 8 * 1024 > stackResource;
std::ifstream file(std::filesystem::path{"/etc/os-release"});
if(not file.good())
return {"unknown", mr};
std::pmr::string line{&stackResource};
line.reserve(100);
while(std::getline(file, line)) {
std::pmr::string _key{&stackResource};
std::pmr::string _value{&stackResource};
if(!line.length())
continue;
if(line[0] == '#' || line[0] == ';')
continue;
auto posEqual = line.find('=');
_key = line.substr(0, posEqual);
_value = line.substr(posEqual + 1);
if(_key == "PRETTY_NAME"){
return {_value, mr};
}
}
return {"unknown", mr};
}
template < class MethodSelect_t = MethodSelect > template < class MethodSelect_t = MethodSelect >
class Init : public AuthenticationStep< Init< MethodSelect_t > > { class Init : public AuthenticationStep {
using base_t = AuthenticationStep< Init< MethodSelect_t > >; using base_t = AuthenticationStep;
const char * apiPath = "/api/transaction/init"; const char * apiPath = "/api/transaction/init";
tl::expected< MethodSelect_t, Error > createMethod(const Document & coreResponse) const { tl::expected< MethodSelect_t, Error > createMethod(const Document & coreResponse) const {
const auto & rublonResponse = coreResponse["result"]; const auto & rublonResponse = coreResponse["result"];
std::string tid = rublonResponse["tid"].GetString(); std::string tid = rublonResponse["tid"].GetString();
return MethodSelect_t{this->_systemToken, tid, rublonResponse["methods"]}; g_tid = tid;
return MethodSelect_t{this->_systemToken, tid, rublonResponse["methods"], _session.config().prompt};
} }
template < typename PamInfo_t > template < typename PamInfo_t >
@ -69,7 +47,7 @@ class Init : public AuthenticationStep< Init< MethodSelect_t > > {
std::pmr::string releaseInfo{&stackResource}; std::pmr::string releaseInfo{&stackResource};
auto & alloc = coreRequest.GetAllocator(); auto & alloc = coreRequest.GetAllocator();
const auto os = osName(&stackResource); const auto os = details::osName(&stackResource);
if(os == "unknown") { if(os == "unknown") {
log(LogLevel::Warning, "No OS information available"); log(LogLevel::Warning, "No OS information available");
@ -80,30 +58,26 @@ class Init : public AuthenticationStep< Init< MethodSelect_t > > {
Value params{rapidjson::kObjectType}; Value params{rapidjson::kObjectType};
params.AddMember("userIP", ip, alloc); params.AddMember("userIP", ip, alloc);
params.AddMember("appVer", "v.0.0.1", alloc); /// TODO add version to cmake params.AddMember("appVer", "2.0.2", alloc); /// TODO add version to cmake
params.AddMember("os", osNamePretty, alloc); params.AddMember("os", osNamePretty, alloc);
coreRequest.AddMember("params", std::move(params), alloc); coreRequest.AddMember("params", std::move(params), alloc);
} }
template < typename PamInfo_t > template<typename Pam_T>
tl::expected< std::reference_wrapper< const Document >, Error > checkEnrolement(const Document & coreResponse, tl::expected< std::reference_wrapper< const Document >, Error > checkEnrolement(const Document & coreResponse, const Pam_T pam) const {
const PamInfo_t & pam) const {
using namespace std::string_view_literals; using namespace std::string_view_literals;
const auto & resp = coreResponse; const auto & resp = coreResponse;
if(resp.HasMember("result") and resp["result"].IsObject() and resp["result"].HasMember("status")) { if(resp.HasMember("result") and resp["result"].IsObject() and resp["result"].HasMember("status")) {
const auto & status = resp["result"]["status"].GetString(); const auto & status = resp["result"]["status"].GetString();
log(LogLevel::Warning, "Got enrolement message with stats %s", status); log(LogLevel::Warning, "Got enrolement message with stats %s", status);
if(status == "pending"sv and resp["result"].HasMember("webURI")) { if((status == "pending"sv || status == "waiting"sv) and resp["result"].HasMember("webURI")) {
const auto & weburi = resp["result"]["webURI"].GetString(); const auto & weburi = resp["result"]["webURI"].GetString();
pam.print("It seams that your account is not configured properly,\nplease contact your administrator for more information"); pam.print("Visit %s", weburi);
pam.print("also, please visit %s", weburi);
return tl::unexpected{Error{RublonAuthenticationInterrupt{RublonAuthenticationInterrupt::UserPending}}}; return tl::unexpected{Error{RublonAuthenticationInterrupt{RublonAuthenticationInterrupt::UserPending}}};
} }
if(status == "denied"sv) { if(status == "denied"sv) {
pam.print("It seams that your account is disabled, please contact your administrator for more information");
return tl::unexpected{Error{RublonAuthenticationInterrupt{RublonAuthenticationInterrupt::UserDenied}}}; return tl::unexpected{Error{RublonAuthenticationInterrupt{RublonAuthenticationInterrupt::UserDenied}}};
} }
} }
@ -114,10 +88,15 @@ class Init : public AuthenticationStep< Init< MethodSelect_t > > {
public: public:
const char * name = "Initialization"; const char * name = "Initialization";
Init(const rublon::Configuration & config) : base_t(config.systemToken.data(), "") {} const Session & _session;
Init(const Session & session) : base_t(std::string{session.config().systemToken.data(), 32}, ""), _session{session} {
log(LogLevel::Debug, "Init");
}
template < typename Hander_t, typename PamInfo_t = LinuxPam > template < typename Hander_t, typename PamInfo_t = LinuxPam >
tl::expected< MethodSelect_t, Error > handle(const CoreHandlerInterface< Hander_t > & coreHandler, const PamInfo_t & pam) const { [[deprecated]] tl::expected< MethodSelect_t, Error > handle(const CoreHandlerInterface< Hander_t > & coreHandler,
const PamInfo_t & pam) const {
const auto createMethod = [&](const auto & coreResponse) { return this->createMethod(coreResponse); }; const auto createMethod = [&](const auto & coreResponse) { return this->createMethod(coreResponse); };
const auto checkEnrolement = [&](const auto & coreResponse) { return this->checkEnrolement(coreResponse,pam); }; const auto checkEnrolement = [&](const auto & coreResponse) { return this->checkEnrolement(coreResponse,pam); };
@ -133,5 +112,22 @@ class Init : public AuthenticationStep< Init< MethodSelect_t > > {
.and_then(checkEnrolement) .and_then(checkEnrolement)
.and_then(createMethod); .and_then(createMethod);
} }
// tl::expected< Transaction, Error > openTransaction() const {
// const auto createTransaction = [&](const auto & coreResponse) { return this->createTransaction(coreResponse); };
// const auto checkEnrolement = [&](const auto & coreResponse) { return this->checkEnrolement(coreResponse, _session.pam()); };
// RapidJSONPMRStackAlloc< 2048 > alloc{};
// Document body{rapidjson::kObjectType, &alloc};
// this->addSystemToken(body);
// this->addPamInfo(body, _session.pam());
// this->addParams(body, _session.pam());
// return _session.coreHandler()
// .request(alloc, apiPath, body) //
// .and_then(checkEnrolement)
// .and_then(createTransaction);
// }
}; };
} // namespace rublon } // namespace rublon

50
PAM/ssh/include/rublon/json.hpp Normal file → Executable file
View File

@ -3,6 +3,8 @@
#include <rapidjson/document.h> #include <rapidjson/document.h>
#include <rapidjson/pointer.h> #include <rapidjson/pointer.h>
#include <rapidjson/writer.h> #include <rapidjson/writer.h>
#include <rapidjson/prettywriter.h>
#include <rapidjson/istreamwrapper.h>
#include <cstring> #include <cstring>
#include <memory_resource> #include <memory_resource>
@ -77,7 +79,7 @@ struct RapidJSONPMRAlloc {
template < std::size_t N > template < std::size_t N >
struct RapidJSONPMRStackAlloc : public RapidJSONPMRAlloc { struct RapidJSONPMRStackAlloc : public RapidJSONPMRAlloc {
private: private:
char _buffer[N]; char _buffer[N]{};
std::pmr::monotonic_buffer_resource mr{_buffer, N}; std::pmr::monotonic_buffer_resource mr{_buffer, N};
public: public:
@ -95,4 +97,50 @@ using Writer = rapidjson::Writer< StringBuffer, rapidjson::UTF8<>, rapidjs
using JSONPointer = rapidjson::GenericPointer< Value, RapidJSONPMRAlloc >; using JSONPointer = rapidjson::GenericPointer< Value, RapidJSONPMRAlloc >;
template < typename Str_t >
struct StringWriter {
Str_t & _str;
using Ch = char;
StringWriter(Str_t & t) : _str{t} {}
void Put(Ch ch) {
_str += ch;
}
void Flush() {}
};
struct FileWriter {
std::unique_ptr< FILE, int (*)(FILE *) > _fp{nullptr,nullptr};
using Ch = char;
FileWriter(std::string_view filename) {
_fp = std::unique_ptr< FILE, int (*)(FILE *) >(fopen(filename.data(), "w"), fclose);
}
void Put(Ch ch) {
fputc(ch, _fp.get());
}
void Flush() {}
};
template < typename T >
static void stringifyTo(const Document & body, T & to) {
memory::Monotonic_1k_HeapResource tmpResource;
RapidJSONPMRAlloc alloc{&tmpResource};
StringWriter<T> s{to};
rapidjson::Writer< StringWriter<T>,rapidjson::UTF8<>, rapidjson::UTF8<>, RapidJSONPMRAlloc > writer{s, &alloc};
body.Accept(writer);
}
} // namespace rublon } // namespace rublon
namespace std {
inline auto begin(const rublon::Value & __ils) noexcept {
return __ils.Begin();
}
inline ::rublon::Value::ConstValueIterator end(const rublon::Value & __ils) noexcept {
return __ils.End();
}
[[nodiscard]] inline std::size_t size(const rublon::Value & __cont) {
return __cont.Size();
}
} // namespace std

6
PAM/ssh/include/rublon/memory.hpp Normal file → Executable file
View File

@ -4,16 +4,16 @@
namespace rublon { namespace rublon {
namespace memory { namespace memory {
struct holder { struct default_memory_resource {
static inline std::pmr::memory_resource * _mr = std::pmr::get_default_resource(); static inline std::pmr::memory_resource * _mr = std::pmr::get_default_resource();
}; };
inline void set_default_resource(std::pmr::memory_resource * memory_resource) { inline void set_default_resource(std::pmr::memory_resource * memory_resource) {
holder{}._mr = memory_resource; default_memory_resource{}._mr = memory_resource;
} }
inline std::pmr::memory_resource * default_resource() { inline std::pmr::memory_resource * default_resource() {
return holder{}._mr; return default_memory_resource{}._mr;
} }
template < std::size_t N > template < std::size_t N >

View File

@ -0,0 +1,19 @@
#pragma once
#include <tl/expected.hpp>
#include <rublon/authentication_step_interface.hpp>
#include <rublon/pam.hpp>
#include <rublon/pam_action.hpp>
#include <rublon/method/websocket_based_auth.hpp>
namespace rublon::method {
class EMAIL : public WebsocketBasedAuth {
public:
EMAIL(std::string systemToken, std::string tid)
: WebsocketBasedAuth(std::move(systemToken), std::move(tid), "Email Link") {}
};
} // namespace rublon::method

12
PAM/ssh/include/rublon/method/OTP.hpp Normal file → Executable file
View File

@ -12,8 +12,16 @@ namespace rublon::method {
class OTP : public PasscodeBasedAuth { class OTP : public PasscodeBasedAuth {
public: public:
OTP(std::string systemToken, std::string tid) OTP(std::string systemToken, std::string tid, int prompts)
: PasscodeBasedAuth(std::move(systemToken), std::move(tid), "OTP", "Enter code from Rublon Authenticator: ", 6, true) {} : PasscodeBasedAuth(std::move(systemToken),
std::move(tid),
"",
"Mobile Passcode",
"Enter the passcode from the Rublon Authenticator mobile app: ",
6,
true,
PasscodeBasedAuth::Endpoint::ConfirmCode,
prompts) {}
}; };
} // namespace rublon::method } // namespace rublon::method

2
PAM/ssh/include/rublon/method/PUSH.hpp Normal file → Executable file
View File

@ -13,7 +13,7 @@ namespace rublon::method {
class PUSH : public WebsocketBasedAuth { class PUSH : public WebsocketBasedAuth {
public: public:
PUSH(std::string systemToken, std::string tid) PUSH(std::string systemToken, std::string tid)
: WebsocketBasedAuth(std::move(systemToken), std::move(tid), "PUSH") {} : WebsocketBasedAuth(std::move(systemToken), std::move(tid), "Mobile PUSH") {}
}; };
} // namespace rublon::method } // namespace rublon::method

11
PAM/ssh/include/rublon/method/SMS.hpp Normal file → Executable file
View File

@ -12,7 +12,16 @@ namespace rublon::method {
class SMS : public PasscodeBasedAuth { class SMS : public PasscodeBasedAuth {
public: public:
SMS(std::string systemToken, std::string tid) : PasscodeBasedAuth(std::move(systemToken), std::move(tid), "SMS", "Enter SMS passcode: ", 6, true) {} SMS(std::string systemToken, std::string tid, int prompts)
: PasscodeBasedAuth(std::move(systemToken),
std::move(tid),
"",
"SMS",
"Enter SMS passcode: ",
6,
true,
PasscodeBasedAuth::Endpoint::ConfirmCode,
prompts) {}
}; };
} // namespace rublon::method } // namespace rublon::method

2
PAM/ssh/include/rublon/method/SmsLink.hpp Normal file → Executable file
View File

@ -13,7 +13,7 @@ namespace rublon::method {
class SmsLink : public WebsocketBasedAuth { class SmsLink : public WebsocketBasedAuth {
public: public:
SmsLink(std::string systemToken, std::string tid) SmsLink(std::string systemToken, std::string tid)
: WebsocketBasedAuth(std::move(systemToken), std::move(tid), "smsLink") {} : WebsocketBasedAuth(std::move(systemToken), std::move(tid), "SMS Link") {}
}; };
} // namespace rublon::method } // namespace rublon::method

11
PAM/ssh/include/rublon/method/YOTP.hpp Normal file → Executable file
View File

@ -12,8 +12,15 @@ namespace rublon::method {
class YOTP : public PasscodeBasedAuth { class YOTP : public PasscodeBasedAuth {
public: public:
YOTP(std::string systemToken, std::string tid) YOTP(std::string systemToken, std::string tid, std::string accessToken, int prompts)
: PasscodeBasedAuth(std::move(systemToken), std::move(tid), "YOTP", "Press Yubikey: ", 44, false) {} : PasscodeBasedAuth(std::move(systemToken),
std::move(tid),
std::move(accessToken),
"YubiKey OTP Security Key",
"Insert and tap your YubiKey: ",
44,
false,
PasscodeBasedAuth::Endpoint::SecurityKeySSH, prompts) {}
}; };
} // namespace rublon::method } // namespace rublon::method

176
PAM/ssh/include/rublon/method/method_select.hpp Normal file → Executable file
View File

@ -9,39 +9,19 @@
#include <rublon/pam.hpp> #include <rublon/pam.hpp>
#include <rublon/pam_action.hpp> #include <rublon/pam_action.hpp>
#include <rublon/method/OTP.hpp> #include <rublon/method/EMAIL.hpp>
#include <rublon/method/PUSH.hpp> #include <rublon/method/PUSH.hpp>
#include <rublon/method/SMS.hpp>
#include <rublon/method/SmsLink.hpp> #include <rublon/method/SmsLink.hpp>
#include <rublon/method/OTP.hpp>
#include <rublon/method/SMS.hpp>
#include <rublon/method/YOTP.hpp> #include <rublon/method/YOTP.hpp>
template < class F > extern std::string g_tid;
struct return_type;
template < class R, class... A >
struct return_type< R (*)(A...) > {
typedef R type;
};
template < typename T >
using return_type_t = typename return_type< T >::type;
namespace std {
inline ::rublon::Value::ConstValueIterator begin(const rublon::Value & __ils) noexcept {
return __ils.Begin();
}
inline ::rublon::Value::ConstValueIterator end(const rublon::Value & __ils) noexcept {
return __ils.End();
}
[[nodiscard]] inline std::size_t size(const rublon::Value & __cont) {
return __cont.Size();
}
} // namespace std
namespace rublon { namespace rublon {
class MethodProxy { class MethodProxy {
public: public:
template < typename Method_t > template < typename Method_t >
@ -49,41 +29,43 @@ class MethodProxy {
template < typename Handler_t, typename PamInfo_t = LinuxPam > template < typename Handler_t, typename PamInfo_t = LinuxPam >
tl::expected< AuthenticationStatus, Error > fire(const CoreHandlerInterface< Handler_t > & coreHandler, const PamInfo_t & pam) const { tl::expected< AuthenticationStatus, Error > fire(const CoreHandlerInterface< Handler_t > & coreHandler, const PamInfo_t & pam) const {
coreHandler.createWSConnection(g_tid);
return std::visit( return std::visit(
[&](const auto & method) { [&](const auto & method) {
rublon::log(LogLevel::Info, "Using '%s' method", method.name); rublon::log(LogLevel::Info, "Using '%s' method", method.name);
return method.fire(coreHandler, pam); return method.verify(coreHandler, pam);
}, },
_impl); _impl);
} }
private: private:
std::variant< method::OTP, method::SMS, method::PUSH, method::SmsLink, method::YOTP > _impl; std::variant< method::OTP, method::SMS, method::PUSH, method::EMAIL, method::SmsLink, method::YOTP > _impl;
}; };
class PostMethod : public rublon::AuthenticationStep< PostMethod > { class PostMethod : public AuthenticationStep {
using base_t = rublon::AuthenticationStep< PostMethod >; using base_t = AuthenticationStep;
const char * uri = "/api/transaction/methodSSH"; const char * uri = "/api/transaction/methodSSH";
std::string _method; std::string _method;
int _prompts;
tl::expected< MethodProxy, Error > createMethod(const Document & coreResponse) const { tl::expected< MethodProxy, Error > createMethod(const Document & coreResponse) const {
const auto & rublonResponse = coreResponse["result"]; const auto & rublonResponse = coreResponse["result"];
std::string tid = rublonResponse["tid"].GetString(); std::string tid = rublonResponse["tid"].GetString();
std::string token = rublonResponse.HasMember("token") ? rublonResponse["token"].GetString() : "";
if(_method == "totp") { if(_method == "totp") {
return MethodProxy{method::OTP{this->_systemToken, std::move(tid)}}; return MethodProxy{method::OTP{this->_systemToken, std::move(tid), _prompts}};
} else if(_method == "sms") { } else if(_method == "sms") {
return MethodProxy{method::SMS{this->_systemToken, std::move(tid)}}; return MethodProxy{method::SMS{this->_systemToken, std::move(tid), _prompts}};
} else if(_method == "push") { } else if(_method == "push") {
return MethodProxy{method::PUSH{this->_systemToken, std::move(tid)}}; return MethodProxy{method::PUSH{this->_systemToken, std::move(tid)}};
} else if(_method == "email") {
return MethodProxy{method::PUSH{this->_systemToken, std::move(tid)}};
} else if(_method == "smsLink") { } else if(_method == "smsLink") {
return MethodProxy{method::SmsLink{this->_systemToken, std::move(tid)}}; return MethodProxy{method::SmsLink{this->_systemToken, std::move(tid)}};
} else if(_method == "yotp") { } else if(_method == "yotp") {
return MethodProxy{method::YOTP{this->_systemToken, std::move(tid)}}; return MethodProxy{method::YOTP{this->_systemToken, std::move(tid), std::move(token), _prompts}};
} } else
else
return tl::unexpected{MethodError{MethodError::BadMethod}}; return tl::unexpected{MethodError{MethodError::BadMethod}};
} }
@ -95,8 +77,8 @@ class PostMethod : public rublon::AuthenticationStep< PostMethod > {
public: public:
const char * name = "Confirm Method"; const char * name = "Confirm Method";
PostMethod(std::string systemToken, std::string tid, std::string method) PostMethod(std::string systemToken, std::string tid, std::string method, int prompts)
: base_t(std::move(systemToken), std::move(tid)), _method{method} {} : base_t(std::move(systemToken), std::move(tid)), _method{method}, _prompts{prompts} {}
template < typename Hander_t, typename PamInfo_t = LinuxPam > template < typename Hander_t, typename PamInfo_t = LinuxPam >
tl::expected< MethodProxy, Error > handle(const CoreHandlerInterface< Hander_t > & coreHandler) const { tl::expected< MethodProxy, Error > handle(const CoreHandlerInterface< Hander_t > & coreHandler) const {
@ -117,14 +99,16 @@ class PostMethod : public rublon::AuthenticationStep< PostMethod > {
class MethodSelect { class MethodSelect {
std::string _systemToken; std::string _systemToken;
std::string _accessToken;
std::string _tid; std::string _tid;
int _prompts;
std::vector< std::string > _methods; std::vector< std::string > _methods;
public: public:
template < typename Array_t > template < typename Array_t >
MethodSelect(std::string systemToken, std::string tid, const Array_t & methodsAvailableForUser) MethodSelect(std::string systemToken, std::string tid, const Array_t & methodsAvailableForUser, int prompts)
: _systemToken{std::move(systemToken)}, _tid{std::move(tid)} { : _systemToken{std::move(systemToken)}, _tid{std::move(tid)}, _prompts{prompts} {
_methods.reserve(std::size(methodsAvailableForUser)); _methods.reserve(std::size(methodsAvailableForUser));
std::transform( std::transform(
std::begin(methodsAvailableForUser), std::end(methodsAvailableForUser), std::back_inserter(_methods), [](const auto & method) { std::begin(methodsAvailableForUser), std::end(methodsAvailableForUser), std::back_inserter(_methods), [](const auto & method) {
@ -136,9 +120,12 @@ class MethodSelect {
template < typename Pam_t > template < typename Pam_t >
tl::expected< PostMethod, Error > create(Pam_t & pam) const { tl::expected< PostMethod, Error > create(Pam_t & pam) const {
rublon::log(LogLevel::Debug, "prompting user to select method"); rublon::log(LogLevel::Debug, "prompting user to select method");
memory::StrictMonotonic_2k_HeapResource memoryResource; memory::StrictMonotonic_4k_HeapResource memoryResource;
std::pmr::map< int, std::string > methods_id{&memoryResource}; std::pmr::map< int, std::string > methods_id{&memoryResource};
pam.print("select method: "); std::pmr::map< int, std::string > methods_names{&memoryResource};
int prompts = _prompts;
pam.print("Select the authentication method to verify your identity: ");
auto logMethodAvailable = [](auto & method) { // auto logMethodAvailable = [](auto & method) { //
rublon::log(LogLevel::Debug, "Method %s found", method.c_str()); rublon::log(LogLevel::Debug, "Method %s found", method.c_str());
@ -152,30 +139,41 @@ class MethodSelect {
for(const auto & method : _methods) { for(const auto & method : _methods) {
if(method == "totp") { if(method == "totp") {
logMethodAvailable(method); logMethodAvailable(method);
pam.print("%d: Mobile TOTP", i + 1); pam.print("%d: Mobile Passcode", i + 1);
methods_id[++i] = method; methods_id[++i] = method;
methods_names[i] = "Mobile Passcode";
continue; continue;
} }
/// Needs changes in Core to work if(method == "email") {
// if(method == "yotp") { logMethodAvailable(method);
// logMethodAvailable(method); pam.print("%d: Email Link", i + 1);
// pam.print("%d: Yubikey", i + 1); methods_id[++i] = method;
// methods_id[++i] = method; methods_names[i] = "Email Link";
// continue; continue;
// } }
if(method == "yotp") {
logMethodAvailable(method);
pam.print("%d: YubiKey OTP Security Key", i + 1);
methods_id[++i] = method;
methods_names[i] = "YubiKey OTP Security Key";
continue;
}
if(method == "sms") { if(method == "sms") {
logMethodAvailable(method); logMethodAvailable(method);
pam.print("%d: SMS code", i + 1); pam.print("%d: SMS Passcode", i + 1);
methods_id[++i] = method; methods_id[++i] = method;
methods_names[i] = "SMS Passcode";
continue; continue;
} }
if(method == "push") { if(method == "push") {
logMethodAvailable(method); logMethodAvailable(method);
pam.print("%d: Mobile PUSH", i + 1); pam.print("%d: Mobile Push", i + 1);
methods_id[++i] = method; methods_id[++i] = method;
methods_names[i] = "Mobile Push";
continue; continue;
} }
@ -183,71 +181,87 @@ class MethodSelect {
logMethodAvailable(method); logMethodAvailable(method);
pam.print("%d: SMS Link", i + 1); pam.print("%d: SMS Link", i + 1);
methods_id[++i] = method; methods_id[++i] = method;
methods_names[i] = "SMS Link";
continue; continue;
} }
logMethodNotAvailable(method); logMethodNotAvailable(method);
} }
if(i == 0) { if(i == 0) {
log(LogLevel::Warning, "None of provided methods are supported by the connector");
return tl::unexpected(MethodError(MethodError::ErrorClass::NoMethodAvailable)); return tl::unexpected(MethodError(MethodError::ErrorClass::NoMethodAvailable));
} }
return i; return i;
}; };
const auto toMethodError = [&](conv::Error /*methodid*/) -> MethodError { const auto toMethodError = [&](conv::Error /*methodid*/) -> MethodError {
pam.print("Input is not an number, please correct"); // NaN or out of range
return MethodError{MethodError::BadUserInput}; return MethodError{MethodError::BadUserInput};
}; };
const auto createMethod = [&](std::uint32_t methodid) -> tl::expected< PostMethod, MethodError > { const auto createMethod = [&](std::uint32_t methodid) -> tl::expected< PostMethod, MethodError > {
auto hasMethod = methods_id.find(methodid) != methods_id.end(); auto hasMethod = methods_id.find(methodid) != methods_id.end();
pam.print("\t selected: %s", hasMethod ? methods_id.at(methodid).c_str() : "unknown option"); // pam.print("\t selected: %s", hasMethod ? methods_id.at(methodid).c_str() : "unknown option");
if(!hasMethod) { if(!hasMethod) {
log(LogLevel::Error, "User selected option %d, which is not corrent", methodid);
return tl::unexpected{MethodError(MethodError::BadMethod)}; return tl::unexpected{MethodError(MethodError::BadMethod)};
} else { } else {
log(LogLevel::Info, "User selected option %d{%s}", methodid, methods_id.at(methodid).c_str()); log(LogLevel::Info, "User selected option %d{%s}", methodid, methods_names.at(methodid).c_str());
return PostMethod{_systemToken, _tid, methods_id.at(methodid)}; return PostMethod{_systemToken, _tid, methods_id.at(methodid), _prompts};
} }
}; };
const auto askForMethodAgain = [&]() -> tl::expected< PostMethod, MethodError > {
printAvailableMethods();
return pam
.scan(conv::to_uint32, "\nSelect method [1-%d]: ", methods_id.size()) //
.transform_error(toMethodError)
.and_then(createMethod);
/// TODO or_else(printErrorAndDenyAccess); ??
};
const auto askForMethod = [&](int methods_number) -> tl::expected< uint32_t, MethodError > { const auto askForMethod = [&](int methods_number) -> tl::expected< uint32_t, MethodError > {
if(methods_number == 1) { if(methods_number == 1) {
pam.print("Only one method available"); pam.print("Automatically selected the only available authentication method: %s", methods_id.at(1).c_str());
return 1; return 1;
} }
return pam.scan(conv::to_uint32, "\nSelect method [1-%d]: ", methods_number).transform_error(toMethodError); return pam.scan(conv::to_uint32, "\nSelect method [1-%d]: ", methods_number).transform_error(toMethodError);
}; };
const auto handleErrors = [&](const MethodError & e) -> tl::expected< PostMethod, MethodError > { auto reducePromptCount = [&](int selected_method) -> tl::expected< uint32_t, MethodError > {
switch(e.errorClass) { prompts--;
case MethodError::BadMethod: // User provided a number but the number if not found return selected_method;
return askForMethodAgain();
case MethodError::BadUserInput: // User provided id is invalid (NAN)
return askForMethodAgain();
case MethodError::NoMethodAvailable:
/// TODO print enrolement
default:
return tl::unexpected(e);
}
}; };
auto disableAnyNewMethodPrompts = [&]() { prompts = 0; };
const auto printEnrolementInformation = [&]() {
log(LogLevel::Warning, "Enrolement send to admin about new user: %s", pam.username());
pam.print("Please check email");
};
const auto handleErrors = [&](const MethodError & e) -> tl::expected< PostMethod, MethodError > {
switch(e.errorClass) {
case MethodError::NoMethodAvailable:
disableAnyNewMethodPrompts();
printEnrolementInformation();
break;
case MethodError::BadMethod: // User provided a number but the number if not found
pam.print("Input is not a valid method number. Enter a valid number");
break;
case MethodError::BadUserInput: // User provided id is invalid (NAN)
pam.print("Input is not a number. Enter a valid number");
break;
}
return tl::unexpected(e);
};
const auto toGenericError = [](const MethodError & e) -> Error { return Error{e}; }; const auto toGenericError = [](const MethodError & e) -> Error { return Error{e}; };
return printAvailableMethods() // return [&]() {
while(true) {
auto method = printAvailableMethods() //
.and_then(reducePromptCount)
.and_then(askForMethod) .and_then(askForMethod)
.and_then(createMethod) .and_then(createMethod)
.or_else(handleErrors) .or_else(handleErrors);
if(not method && prompts > 0)
continue;
return method;
};
}()
.map_error(toGenericError); .map_error(toGenericError);
} }
}; };

109
PAM/ssh/include/rublon/method/passcode_based_auth.hpp Normal file → Executable file
View File

@ -1,21 +1,31 @@
#pragma once #pragma once
#include "rublon/utils.hpp"
#include <tl/expected.hpp> #include <tl/expected.hpp>
#include <rublon/authentication_step_interface.hpp> #include <rublon/authentication_step_interface.hpp>
#include <rublon/pam.hpp> #include <rublon/pam.hpp>
#include <rublon/pam_action.hpp> #include <rublon/pam_action.hpp>
#include <rublon/websockets.hpp>
namespace rublon::method { namespace rublon::method {
class PasscodeBasedAuth : public AuthenticationStep< PasscodeBasedAuth > { class PasscodeBasedAuth : public AuthenticationStep {
protected: protected:
using base_t = AuthenticationStep< PasscodeBasedAuth >; const char * uri;
const char * uri = "/api/transaction/confirmCode"; const char * confirmField;
static constexpr const char * confirmCodeEndpoint = "/api/transaction/confirmCode";
static constexpr const char * confirmSecuritySSHEndpoint = "/api/transaction/confirmSecurityKeySSH";
static constexpr const char * fieldVericode = "vericode";
static constexpr const char * fieldOtp = "otp";
const char * userMessage{nullptr}; const char * userMessage{nullptr};
const uint_fast8_t length; const uint_fast8_t length;
const bool onlyDigits; const bool onlyDigits;
int _prompts;
constexpr static bool isdigit(char ch) { constexpr static bool isdigit(char ch) {
return std::isdigit(static_cast< unsigned char >(ch)); return std::isdigit(static_cast< unsigned char >(ch));
@ -51,60 +61,101 @@ class PasscodeBasedAuth : public AuthenticationStep< PasscodeBasedAuth > {
auto vericode = pam.scan([](const char * userInput) { return std::string{userInput}; }, userMessage); auto vericode = pam.scan([](const char * userInput) { return std::string{userInput}; }, userMessage);
if(hasValidLength(vericode) and hasValidCharacters(vericode)) { if(hasValidLength(vericode) and hasValidCharacters(vericode)) {
body.AddMember("vericode", Value{vericode.c_str(), alloc}, alloc); Value confirmFieldValue(confirmField, alloc);
body.AddMember(confirmFieldValue, Value{vericode.c_str(), alloc}, alloc);
if(token.size()) {
this->addAccessToken(body, token);
}
return body; return body;
} }
return tl::unexpected{Error{WerificationError{WerificationError::WrongCode}}}; return tl::unexpected{Error{WerificationError{WerificationError::BadInput}}};
} }
template < typename PamInfo_t = LinuxPam > template < typename PamInfo_t = LinuxPam >
tl::expected< std::reference_wrapper< Document >, Error > askForPasscodeAgain(Document & body, const PamInfo_t & pam) const { tl::expected< AuthenticationStatus, Error > checkAuthenticationStatus(const Document & /*coreResponse*/, const PamInfo_t & pam) const {
pam.print("passcode has wrong number of digits or contains illegal characters, please correct"); pam.print("The passcode has been validated");
return readPasscode(body, pam);
}
template < typename PamInfo_t = LinuxPam >
tl::expected< AuthenticationStatus, Error > checkAuthenticationStatus(const Document & coreResponse, const PamInfo_t & pam) const {
RapidJSONPMRStackAlloc< 1024 > alloc;
auto error = JSONPointer{"/result/error", &alloc}.Get(coreResponse);
if(error) {
pam.print("Wrong code");
return tl::unexpected{Error{WerificationError{WerificationError::WrongCode}}};
}
pam.print("Verification code validated");
return AuthenticationStatus{AuthenticationStatus::Action::Confirmed}; return AuthenticationStatus{AuthenticationStatus::Action::Confirmed};
} }
tl::expected< AuthenticationStatus, Error > waitForCoreConfirmation(
std::shared_ptr< WebSocketSingleShotEventListener > eventListener) const {
log(LogLevel::Info, "Listening to confirmation event in PasscodeBasedAuth");
return eventListener->waitForEvent();
}
template < typename PamInfo_t = LinuxPam >
tl::expected< AuthenticationStatus, Error > errorHandler(Error error, const PamInfo_t & pam, int promptLeft) const {
if(promptLeft && error.is< WerificationError >()) {
switch(error.get< WerificationError >().errorClass) {
case WerificationError::ErrorClass::PasscodeException:
pam.print(R"(Incorrect passcode. Try again)");
break;
case WerificationError::ErrorClass::BadInput:
pam.print("The passcode has an incorrect number of digits or contains invalid characters. Try again");
break;
}
}
return tl::unexpected{error};
}
public: public:
const char * name; const char * name;
std::string token;
enum class Endpoint { ConfirmCode, SecurityKeySSH };
PasscodeBasedAuth(std::string systemToken, PasscodeBasedAuth(std::string systemToken,
std::string tid, std::string tid,
std::string token,
const char * name, const char * name,
const char * userMessage, const char * userMessage,
uint_fast8_t length, uint_fast8_t length,
bool numbersOnly) bool numbersOnly,
: base_t(std::move(systemToken), std::move(tid)), userMessage{userMessage}, length{length}, onlyDigits{numbersOnly}, name{name} {} Endpoint endpoint,
int prompts)
: AuthenticationStep(std::move(systemToken), std::move(tid)),
uri{(endpoint == Endpoint::ConfirmCode) ? confirmCodeEndpoint : confirmSecuritySSHEndpoint},
confirmField{(endpoint == Endpoint::ConfirmCode) ? fieldVericode : fieldOtp},
userMessage{userMessage},
length{length},
onlyDigits{numbersOnly},
_prompts{prompts},
name{name},
token{std::move(token)} {}
template < typename Hander_t, typename PamInfo_t = LinuxPam > template < typename Hander_t, typename PamInfo_t = LinuxPam >
tl::expected< AuthenticationStatus, Error > handle(const CoreHandlerInterface< Hander_t > & coreHandler, const PamInfo_t & pam) const { tl::expected< AuthenticationStatus, Error > verify(const CoreHandlerInterface< Hander_t > & coreHandler, const PamInfo_t & pam) const {
RapidJSONPMRStackAlloc< 2048 > alloc{}; RapidJSONPMRStackAlloc< 2048 > alloc{};
Document body{rapidjson::kObjectType, &alloc}; Document body{rapidjson::kObjectType, &alloc};
std::shared_ptr< WebSocketSingleShotEventListener > eventListener = coreHandler.listen();
int prompts = _prompts;
const auto checkCodeValidity = [&](const auto & coreResponse) { return this->checkAuthenticationStatus(coreResponse, pam); };
const auto requestAuthorization = [&](const auto & body) { return coreHandler.request(alloc, uri, body); }; const auto requestAuthorization = [&](const auto & body) { return coreHandler.request(alloc, uri, body); };
const auto askForPasscodeAgain = [&](const auto & /*error*/) { return this->askForPasscodeAgain(body, pam); }; const auto checkCodeValidity = [&](const auto & coreResponse) { return this->checkAuthenticationStatus(coreResponse, pam); };
const auto waitForCoreToConfirm = [&](const auto &) { return waitForCoreConfirmation(eventListener); };
const auto handleError = [&](const auto error) { return errorHandler(error, pam, prompts); };
this->addSystemToken(body); this->addSystemToken(body);
this->addTid(body); this->addTid(body);
return readPasscode(body, pam) // return [&]() {
.or_else(askForPasscodeAgain) while(true) {
prompts--;
auto access = readPasscode(body, pam) //
.and_then(requestAuthorization) .and_then(requestAuthorization)
.and_then(checkCodeValidity); .and_then(checkCodeValidity)
.and_then(waitForCoreToConfirm)
.or_else(handleError);
if(not access && prompts) {
continue;
} else {
return access;
}
};
}();
} }
}; };

14
PAM/ssh/include/rublon/method/websocket_based_auth.hpp Normal file → Executable file
View File

@ -11,21 +11,19 @@
namespace rublon::method { namespace rublon::method {
class WebsocketBasedAuth : public AuthenticationStep< WebsocketBasedAuth > { class WebsocketBasedAuth : public AuthenticationStep {
using base_t = AuthenticationStep< WebsocketBasedAuth >;
public: public:
const char * name = ""; const char * name = "";
WebsocketBasedAuth(std::string systemToken, std::string tid, const char * name) WebsocketBasedAuth(std::string systemToken, std::string tid, const char * name)
: base_t(std::move(systemToken), std::move(tid)), name{name} {} : AuthenticationStep(std::move(systemToken), std::move(tid)), name{name} {}
template < typename Hander_t, typename PamInfo_t = LinuxPam > template < typename Hander_t, typename PamInfo_t = LinuxPam >
tl::expected< AuthenticationStatus, Error > handle(const CoreHandlerInterface< Hander_t > & coreHandler, const PamInfo_t & pam) const { tl::expected< AuthenticationStatus, Error > verify(const CoreHandlerInterface< Hander_t > & coreHandler, const PamInfo_t & pam) const {
log(LogLevel::Info, "starting WS"); log(LogLevel::Info, "starting WS");
auto listener = coreHandler.listen();
pam.print("Waiting for approval"); pam.scan([](const auto/*ignored userinput*/){return "";}, "Rublon authentication initiated. Complete the authentication and press Enter to proceed");
return coreHandler.waitForConfirmation(_tid); return listener->waitForEvent();
} }
}; };

0
PAM/ssh/include/rublon/non_owning_ptr.hpp Normal file → Executable file
View File

9
PAM/ssh/include/rublon/pam.hpp Normal file → Executable file
View File

@ -43,8 +43,13 @@ class LinuxPam {
template < typename... Ti > template < typename... Ti >
void print(const char * fmt, Ti... ti) const noexcept { void print(const char * fmt, Ti... ti) const noexcept {
log(LogLevel::Debug, fmt, std::forward< Ti >(ti)...); char buf[256]={};
pam_prompt(pamh, PAM_TEXT_INFO, nullptr, fmt, std::forward< Ti >(ti)...); sprintf(buf, fmt, std::forward< Ti >(ti)...);
log(LogLevel::Debug, "pam_print: '%s'", buf);
if(auto r = pam_prompt(pamh, PAM_TEXT_INFO, nullptr, fmt, std::forward< Ti >(ti)...); r != PAM_SUCCESS){
log(LogLevel::Error, "pam_print returned with error code %d", r);
}
} }
template < typename Fun, typename... Ti > template < typename Fun, typename... Ti >

16
PAM/ssh/include/rublon/pam_action.hpp Normal file → Executable file
View File

@ -1,27 +1,33 @@
#pragma once #pragma once
#include <cstring>
#include <string>
#include <string_view>
namespace rublon { namespace rublon {
class AuthenticationStatus { class AuthenticationStatus {
public: public:
enum class Action { Denied, Confirmed, Bypass }; enum class Action { Denied, Confirmed, Bypass };
AuthenticationStatus(Action action) : _action{action} {} AuthenticationStatus(Action action, std::string authenticationToken = "") : _action{action}, _token{std::move(authenticationToken)} {}
constexpr bool userAuthorized() const { constexpr bool userAuthorized() const {
return _action == Action::Confirmed; return _action == Action::Confirmed;
} }
constexpr bool bypass() const {
return false;
}
Action action() const { Action action() const {
return _action; return _action;
} }
std::string_view accessToken() const {
return _token;
}
private: private:
Action _action; Action _action;
std::string _token;
}; };
} // namespace rublon } // namespace rublon

66
PAM/ssh/include/rublon/rublon.hpp Normal file → Executable file
View File

@ -1,72 +1,34 @@
#pragma once #pragma once
#include <rublon/init.hpp> #include <rublon/session.hpp>
#include <rublon/json.hpp> #include <rublon/json.hpp>
#include <rublon/pam.hpp> #include <rublon/pam.hpp>
#include <rublon/rublon.hpp>
#include <rublon/utils.hpp> #include <rublon/utils.hpp>
#include <rublon/finish.hpp>
namespace rublon { namespace rublon {
// template<typename Pam_t> using PAM = LinuxPam;
// static Configuration readConfig(const Pam_t&pam){ using Session = SessionBase<PAM, CoreHandler< CURL >>;
// auto rublonConfig = ConfigurationFactory{}.systemConfig(); using Transaction = TransactionBase<Session>;
// if(not rublonConfig.has_value()) {
// pam.print("\n");
// pam.print("Rublon configuration does not exists or is invalid");
// pam.print("\tcheck '%s' for more details\n", details::logPath());
// }
// }
class RublonSession {
public:
std::pmr::string _tid;
RublonSession() {
/// create memoryResource
///
}
void startTransaction(std::string_view tid) {}
};
class RublonExceptionHandler{
public:
};
template < typename Pam_T, typename CoreHandler_T >
class RublonBase {
const Pam_T & _pam;
Configuration _config{};
void initializeLogs() {
details::initLog();
}
public:
RublonBase(const Pam_T & pam, Configuration config) : _pam{pam}, _config{config} {}
AuthenticationStatus authenticate() const {
}
};
using RublonLinux = RublonBase< LinuxPam, CoreHandler< CURL > >;
class RublonFactory{ class RublonFactory{
public: public:
template <typename Pam_t> tl::expected<Session, Error> startSession(const PAM &pam){
tl::expected<RublonLinux, bool> create(const Pam_t &pam){ details::initLog();
auto config = ConfigurationFactory{}.systemConfig(); auto config = ConfigurationFactory{}.systemConfig();
if(not config.has_value()) { if(not config.has_value()) {
pam.print("\n"); pam.print("The configuration file does not exist or contains incorrect values");
pam.print("Rublon configuration does not exists or is invalid"); return tl::unexpected{ConfigurationError{}};
pam.print("\tcheck '%s' for more details\n", details::logPath());
return tl::unexpected{false};
} }
return RublonLinux{pam, *config}; return Session{pam, config.value()};
} }
}; };
} // namespace rublon } // namespace rublon
#include <rublon/init.hpp>

View File

@ -0,0 +1,40 @@
#pragma once
#include "rublon/utils.hpp"
#include <string>
#include <rublon/configuration.hpp>
namespace rublon{
template<typename Pam_t, typename CoreHandler_t>
class SessionBase {
const Pam_t & _pam;
const Configuration _config;
CoreHandler_t _coreHandler;
/// TODO log
/// TODO momory resource
public:
SessionBase(const Pam_t &pam, Configuration config) : _pam{pam}, _config{config}, _coreHandler{_config} {
log(LogLevel::Debug, __PRETTY_FUNCTION__);
}
const auto & coreHandler() const {return _coreHandler; }
const auto & pam() const { return _pam; }
const auto & config() const { return _config; }
};
template<typename Session_t>
class TransactionBase{
public:
const Session_t& _session;
std::string _tid;
TransactionBase(const Session_t& session, std::string_view tid):_session{session},_tid{tid.data(), tid.size()} {
}
};
}

0
PAM/ssh/include/rublon/sign.hpp Normal file → Executable file
View File

View File

@ -0,0 +1,35 @@
#pragma once
#include <array>
#include <cstddef>
#include <cstring>
// statically allocates a string buffer of (N+1) chars
template < size_t N >
class StaticString {
public:
constexpr StaticString() = default;
constexpr StaticString(const char * str) {
std::strncpy(m_str.data(), str, N);
}
void operator=(const char * str) {
std::strncpy(m_str.data(), str, N);
}
const char * c_str() const noexcept {
return &m_str[0];
}
const char * data() const noexcept {
return &m_str[0];
}
std::size_t size() const {
return strlen(m_str.data());
}
private:
std::array< char, N + 1 > m_str{};
};

136
PAM/ssh/include/rublon/utils.hpp Normal file → Executable file
View File

@ -8,15 +8,14 @@
#include <filesystem> #include <filesystem>
#include <fstream> #include <fstream>
#include <iostream> #include <iostream>
#include <map>
#include <memory> #include <memory>
#include <memory_resource> #include <memory_resource>
#include <sstream> #include <sstream>
#include <string_view> #include <string_view>
#include <alloca.h> #include <alloca.h>
#include <cassert>
#include <fcntl.h>
#include <security/pam_appl.h> #include <security/pam_appl.h>
#include <security/pam_modules.h> #include <security/pam_modules.h>
#include <sys/stat.h> #include <sys/stat.h>
@ -27,7 +26,6 @@
#include <rublon/memory.hpp> #include <rublon/memory.hpp>
namespace rublon { namespace rublon {
inline bool fileGood(const std::filesystem::path & path) { inline bool fileGood(const std::filesystem::path & path) {
std::ifstream file(path); std::ifstream file(path);
return file.good(); return file.good();
@ -47,8 +45,6 @@ inline bool readFile(const std::filesystem::path & path, T & destination) {
return true; return true;
} }
enum class LogLevel { Debug, Info, Warning, Error }; enum class LogLevel { Debug, Info, Warning, Error };
inline auto dateStr() { inline auto dateStr() {
@ -60,37 +56,129 @@ inline auto dateStr() {
} }
constexpr const char * LogLevelNames[]{"Debug", "Info", "Warning", "Error"}; constexpr const char * LogLevelNames[]{"Debug", "Info", "Warning", "Error"};
constexpr LogLevel g_level = LogLevel::Debug; LogLevel g_level = LogLevel::Debug;
constexpr bool syncLogFile = true; constexpr bool syncLogFile = true;
static const char * application = "";
// #include <openssl/md5.h>
// #include <sys/types.h>
// #include <sys/stat.h>
// #include <sys/mman.h>
// unsigned char result[MD5_DIGEST_LENGTH];
// // Print the MD5 sum as hex-digits.
// void print_md5_sum(unsigned char* md) {
// int i;
// for(i=0; i <MD5_DIGEST_LENGTH; i++) {
// printf("%02x",md[i]);
// }
// }
// // Get the size of the file by its file descriptor
// unsigned long get_size_by_fd(int fd) {
// struct stat statbuf;
// if(fstat(fd, &statbuf) < 0) exit(-1);
// return statbuf.st_size;
// }
// int md5(const char*filename) {
// int file_descript;
// unsigned long file_size;
// char* file_buffer;
// file_descript = open(filename, O_RDONLY);
// if(file_descript < 0) exit(-1);
// file_size = get_size_by_fd(file_descript);
// printf("file size:\t%lu\n", file_size);
// file_buffer =(char*)mmap(nullptr, file_size, PROT_READ, MAP_SHARED, file_descript, 0);
// MD5((unsigned char*) file_buffer, file_size, result);
// munmap(file_buffer, file_size);
// return 0;
// }
namespace details { namespace details {
std::pmr::string osName(std::pmr::memory_resource * mr) {
memory::MonotonicStackResource< 8 * 1024 > stackResource;
std::ifstream file(std::filesystem::path{"/etc/os-release"});
if(not file.good())
return {"unknown", mr};
std::pmr::string line{&stackResource};
line.reserve(100);
while(std::getline(file, line)) {
std::pmr::string _key{&stackResource};
std::pmr::string _value{&stackResource};
if(!line.length())
continue;
if(line[0] == '#' || line[0] == ';')
continue;
auto posEqual = line.find('=');
_key = line.substr(0, posEqual);
_value = line.substr(posEqual + 1);
if(_key == "PRETTY_NAME") {
_value.erase(std::remove_if(_value.begin(), _value.end(), [](auto ch) { return ch == '"'; }), _value.end());
return {_value, mr};
}
}
return {"unknown", mr};
}
constexpr const char * logPath() { constexpr const char * logPath() {
constexpr auto path = "/var/log/rublon-ssh.log"; constexpr auto path = "/var/log/rublon-ssh.log";
return path; return path;
} }
inline bool logExists() noexcept { inline void touch(const char * filename) {
return std::unique_ptr< FILE, int (*)(FILE *) >(fopen(logPath(), "r"), fclose).get(); close(open(filename, O_CREAT | O_RDWR, 0640));
} }
inline void initLog() noexcept { inline void mkdir(const char * dirname) {
if(not logExists()) { ::mkdir(dirname, O_CREAT | O_RDWR);
auto fp = fopen(logPath(), "w");
if(fp) {
fclose(fp);
chmod(logPath(), S_IRUSR | S_IWUSR | S_IRGRP);
} }
inline bool exists(const char * filename) {
return access(filename, F_OK) == 0;
} }
inline bool logMissing() noexcept {
return not exists(logPath());
}
inline const char * initLog(const char * app = nullptr) noexcept {
if(logMissing()) {
touch(logPath());
}
if(not app)
application = app;
return logPath();
} }
inline void doLog(LogLevel level, const char * line) noexcept { inline void doLog(LogLevel level, const char * line) noexcept {
auto fp = std::unique_ptr< FILE, int (*)(FILE *) >(fopen(logPath(), "a+"), fclose); auto fp = std::unique_ptr< FILE, int (*)(FILE *) >(fopen(initLog(), "a+"), fclose);
if(fp) { if(fp) {
/// TODO add transaction ID /// TODO add transaction ID
fprintf(fp.get(), "%s [%s] %s\n", dateStr().data(), LogLevelNames[( int ) level], line); fprintf(
fp.get(), "%s %s[%s] %s\n", dateStr().data(), application == nullptr ? "" : application, LogLevelNames[( int ) level], line);
if(syncLogFile) if(syncLogFile)
sync(); sync();
} }
// openlog ("auth", LOG_CONS | LOG_PID | LOG_NDELAY, LOG_AUTH);
// syslog (LOG_INFO, "[%s] %s", "pam_rublon", line);
// closelog ();
} }
} // namespace details } // namespace details
@ -134,16 +222,20 @@ class PrintUser {
}; };
namespace conv { namespace conv {
inline bool to_bool(std::string_view value) { enum class Error { OutOfRange, NotANumber };
auto * buf = ( char * ) alloca(value.size() + 1);
buf[value.size()] = '\0'; inline bool to_bool(std::string_view userinput) {
if(userinput.size() > 16) {
// todo return optional
return false;
}
std::array< char, 16 >buf{};
auto asciitolower = [](char in) { return in - ((in <= 'Z' && in >= 'A') ? ('Z' - 'z') : 0); }; auto asciitolower = [](char in) { return in - ((in <= 'Z' && in >= 'A') ? ('Z' - 'z') : 0); };
std::transform(value.cbegin(), value.cend(), buf, asciitolower); std::transform(userinput.cbegin(), userinput.cend(), buf.data(), asciitolower);
return strcmp(buf, "true") == 0; return strcmp(buf.data(), "true") == 0;
} }
enum class Error { OutOfRange, NotANumber };
inline tl::expected< std::uint32_t, Error > to_uint32(std::string_view userinput) noexcept { inline tl::expected< std::uint32_t, Error > to_uint32(std::string_view userinput) noexcept {
try { try {
return std::stoi(userinput.data()); return std::stoi(userinput.data());

176
PAM/ssh/include/rublon/websockets.hpp Normal file → Executable file
View File

@ -1,19 +1,20 @@
#pragma once #pragma once
#include <rublon/utils.hpp> #include <cstddef>
#include <memory>
#include <sio_client.h> #include <rublon/error.hpp>
#include <rublon/pam_action.hpp>
#include <rublon/utils.hpp>
#include <condition_variable> #include <condition_variable>
#include <mutex> #include <mutex>
#include <sio_client.h>
#include <string>
namespace rublon { namespace rublon {
#define TIMEOUT_SECONDS 90
/// {"status":"ERROR","code":400,"result":{"exception":"MobileAppMissingException","code":32,"errorMessage":"No mobile app class WebSocketConnectionListener : public std::enable_shared_from_this< WebSocketConnectionListener > {
/// detected","details":null}}
class WebSocketConnectionListener {
public: public:
enum class Status { Disconnected, Conected }; enum class Status { Disconnected, Conected };
@ -28,26 +29,25 @@ class WebSocketConnectionListener {
public: public:
WebSocketConnectionListener(sio::client & ws, std::mutex & lock, std::condition_variable_any & cond) WebSocketConnectionListener(sio::client & ws, std::mutex & lock, std::condition_variable_any & cond)
: _ws(ws), _lock{lock}, _cond{cond} { : _ws(ws), _lock{lock}, _cond{cond} {}
_ws.set_open_listener([this]() { this->on_connected(); });
_ws.set_close_listener([this](sio::client::close_reason const & reason) { this->on_close(reason); }); void makeCallbacks() {
_ws.set_fail_listener([this]() { this->on_fail(); }); auto _this = shared_from_this();
_ws.set_open_listener([_this]() { _this->on_connected(); });
_ws.set_close_listener([_this](sio::client::close_reason const & reason) { _this->on_close(reason); });
_ws.set_fail_listener([_this]() { _this->on_fail(); });
} }
tl::expected< Status, bool > waitForConnection() { tl::expected< Status, bool > waitForConnection() {
std::lock_guard lock{_lock}; std::lock_guard lock{_lock};
if(!handshake) {
log(LogLevel::Info, "Waiting for connection"); log(LogLevel::Info, "Waiting for connection");
_cond.wait(_lock); _cond.wait(_lock);
log(LogLevel::Info, "Connection OK!");
}
return _status; return _status;
} }
void on_connected() { void on_connected() {
std::unique_lock{_lock}; std::unique_lock{_lock};
log(LogLevel::Info, "WebSocket connection estamblished"); log(LogLevel::Info, "WebSocket connected");
handshake = true; handshake = true;
_status = Status::Conected; _status = Status::Conected;
_cond.notify_all(); _cond.notify_all();
@ -65,16 +65,16 @@ class WebSocketConnectionListener {
void on_fail() { void on_fail() {
std::unique_lock{_lock}; std::unique_lock{_lock};
log(LogLevel::Info, "WebSocket connection failed"); log(LogLevel::Info, "WebSocket connection fail");
handshake = true; handshake = true;
_status = Status::Disconnected; _status = Status::Disconnected;
_cond.notify_all(); _cond.notify_all();
} }
}; };
class WebSocketSingleShotEventListener { class WebSocketSingleShotEventListener : public std::enable_shared_from_this< WebSocketSingleShotEventListener > {
public: public:
enum class Status { Approved, Denied, Expired }; enum class Status { Unknown, Approved, Denied, Expired };
private: private:
sio::client & _ws; sio::client & _ws;
@ -83,128 +83,149 @@ class WebSocketSingleShotEventListener {
std::condition_variable_any & _cond; std::condition_variable_any & _cond;
bool event_received = false; bool event_received = false;
Status _status{}; Status _status{Status::Unknown};
public:
std::string _data{};
private:
void printobj(std::size_t i, const std::map< std::string, sio::message::ptr > & objects) { void printobj(std::size_t i, const std::map< std::string, sio::message::ptr > & objects) {
std::size_t o{};
for(const auto & [name, msg] : objects) { for(const auto & [name, msg] : objects) {
log(LogLevel::Debug, "Object %ld: %s", i, name.c_str()); log(LogLevel::Debug, "Object %ld: '%s'", i, name.c_str());
printMessage(i, msg); printMessage(i, o++, msg);
} }
}; };
void printMessage(std::size_t i, const sio::message::ptr & message) { void printMessage(std::size_t i, std::size_t o, const sio::message::ptr & message) {
switch(message->get_flag()) { switch(message->get_flag()) {
case sio::message::flag_integer: case sio::message::flag_integer:
log(LogLevel::Debug, "message %ld integer : %d", i, message->get_int()); log(LogLevel::Debug, "message %ld/%ld integer : %d", i, o, message->get_int());
break; break;
case sio::message::flag_double: case sio::message::flag_double:
log(LogLevel::Debug, "message %ld double : %f", i, message->get_double()); log(LogLevel::Debug, "message %ld/%ld double : %f", i, o, message->get_double());
break; break;
case sio::message::flag_string: case sio::message::flag_string:
log(LogLevel::Debug, "message %ld string : %s", i, message->get_string().c_str()); log(LogLevel::Debug, "message %ld/%ld string : %s", i, o, message->get_string().c_str());
break; break;
case sio::message::flag_binary: case sio::message::flag_binary:
log(LogLevel::Debug, "message %ld binary : %s", i, message->get_binary()->c_str()); log(LogLevel::Debug, "message %ld/%ld binary : %s", i, o, message->get_binary()->c_str());
break; break;
case sio::message::flag_array: case sio::message::flag_array:
log(LogLevel::Debug, "message %ld array : blah", i); log(LogLevel::Debug, "message %ld/%ld array : blah", i, o);
break; break;
case sio::message::flag_object: case sio::message::flag_object:
log(LogLevel::Debug, "message %ld object : obj ->", i); log(LogLevel::Debug, "message %ld/%ld object : obj ->", i, o);
printobj(i, message->get_map()); printobj(i, message->get_map());
break; break;
case sio::message::flag_boolean: case sio::message::flag_boolean:
log(LogLevel::Debug, "message %ld bool : %s", i, message->get_bool() ? "yes" : "no"); log(LogLevel::Debug, "message %ld/%ld bool : %s", i, o, message->get_bool() ? "yes" : "no");
break; break;
case sio::message::flag_null: case sio::message::flag_null:
log(LogLevel::Debug, "message %ld NULL", i); log(LogLevel::Debug, "message %ld/%ld NULL", i, o);
break; break;
default: default:
log(LogLevel::Info, "Message with unknown type"); log(LogLevel::Info, "Message with unknown type");
} }
}; };
void generic(sio::event & e, Status status) { void notify(Status status) {
std::lock_guard lock{_lock};
event_received = true; event_received = true;
_status = status; _status = status;
log(LogLevel::Debug, "event name : %s", e.get_name().c_str());
log(LogLevel::Debug, "event nsp : %s", e.get_nsp().c_str());
log(LogLevel::Debug, "event messages: %d", e.get_messages().size());
for(std::size_t i = 0; i < e.get_messages().size(); i++) {
const auto & message = e.get_messages().at(i);
printMessage(i, message);
}
_cond.notify_all(); _cond.notify_all();
} }
void onConfirmed(sio::event & e) { void onConfirmed(sio::event & e) {
generic(e, Status::Approved); std::lock_guard lock{_lock};
log(LogLevel::Info, "Autentication confirmed"); log(LogLevel::Info, "Autentication confirmed");
_data = e.get_messages().at(1)->get_map()["data"]->get_string();
notify(Status::Approved);
} }
void onDenied(sio::event & e) { void onDenied(sio::event & e) {
generic(e, Status::Denied); std::lock_guard lock{_lock};
log(LogLevel::Info, "Autentication denied by user"); log(LogLevel::Info, "Autentication denied by user");
// _data = e.get_messages().at(1)->get_map()["redirectUrl"]->get_string();
for(std::size_t i{}; i < e.get_messages().size(); i++)
printMessage(i, 0, e.get_messages().at(i));
notify(Status::Denied);
} }
void onExpired(sio::event & e) { void onExpired(sio::event & /*e*/) {
generic(e, Status::Expired); std::lock_guard lock{_lock};
log(LogLevel::Error, "Autentication call expierd"); log(LogLevel::Error, "Autentication call expierd");
notify(Status::Expired);
} }
void onErrorEvent(sio::message::ptr const & message) { void onErrorEvent(sio::message::ptr const & message) {
std::lock_guard lock{_lock}; std::lock_guard lock{_lock};
event_received = true;
log(LogLevel::Error, "Error: %s", message->get_string().c_str()); log(LogLevel::Error, "Error: %s", message->get_string().c_str());
_cond.notify_all();
}
void onAny(sio::event & e) {
std::lock_guard lock{_lock};
event_received = true; event_received = true;
log(LogLevel::Info, "onAny message received: name %s", e.get_name().c_str());
_cond.notify_all(); _cond.notify_all();
} }
public: public:
WebSocketSingleShotEventListener(sio::client & ws, std::mutex & lock, std::condition_variable_any & cond) WebSocketSingleShotEventListener(sio::client & ws, std::mutex & lock, std::condition_variable_any & cond)
: _ws{ws}, _lock{lock}, _cond{cond} { : _ws{ws}, _lock{lock}, _cond{cond} {}
_ws.socket()->on("transactionConfirmed", [this](sio::event & e) { this->onConfirmed(e); });
_ws.socket()->on("transactionDenied", [this](sio::event & e) { this->onDenied(e); });
_ws.socket()->on("transactionExpired", [this](sio::event & e) { this->onExpired(e); });
_ws.socket()->on_error([this](sio::message::ptr const & message) { this->onErrorEvent(message); }); void makeCallbacks() {
_ws.socket()->on_any([this](sio::event & e) { this->onAny(e); }); auto _this = shared_from_this();
_ws.socket()->on("transactionConfirmed", [_this](sio::event & e) { _this->onConfirmed(e); });
_ws.socket()->on("transactionDenied", [_this](sio::event & e) { _this->onDenied(e); });
_ws.socket()->on("transactionExpired", [_this](sio::event & e) { _this->onExpired(e); });
_ws.socket()->on_error([_this](sio::message::ptr const & message) { _this->onErrorEvent(message); });
// _ws.socket()->on_any([_this](sio::event & e) { _this->onAny(e); });
} }
/// TODO pass timeout /// TODO pass timeout
tl::expected< Status, bool > waitForEvent() { tl::expected< AuthenticationStatus, Error > waitForEvent() {
std::lock_guard lock{_lock}; std::lock_guard lock{_lock};
if(!event_received) { if(!event_received) {
log(LogLevel::Info, "Waiting for WS event"); log(LogLevel::Info, "Waiting for WS event");
if(_cond.wait_until(_lock, std::chrono::system_clock::now() + std::chrono::minutes{2}) == std::cv_status::timeout) { auto timeout = std::chrono::system_clock::now() + std::chrono::minutes{2};
while(!event_received) {
if(_cond.wait_until(_lock, timeout) == std::cv_status::timeout) {
log(LogLevel::Info, "Waiting for WS event failed due to WS timeout"); log(LogLevel::Info, "Waiting for WS event failed due to WS timeout");
break;
} else { } else {
if(event_received) {
log(LogLevel::Info, "Event arrived"); log(LogLevel::Info, "Event arrived");
break;
} else
log(LogLevel::Info, "Event arrived, but it's not an action");
} }
} }
} else {
log(LogLevel::Info, "event already received");
}
return _status; AuthenticationStatus status{
_status == Status::Approved ? AuthenticationStatus::Action::Confirmed : AuthenticationStatus::Action::Denied, _data};
return status;
} }
}; };
class WebSocket { class WebSocket {
public:
bool connect(std::string_view uri, std::string_view tid) {
std::mutex _lock; std::mutex _lock;
std::condition_variable_any _cond; std::condition_variable_any _cond;
sio::client ws; sio::client ws;
public:
WebSocket() = default;
~WebSocket() = default;
std::shared_ptr< WebSocketSingleShotEventListener > listen() {
const auto createEventListener = [&]() -> std::shared_ptr< WebSocketSingleShotEventListener > {
log(LogLevel::Debug, "Initializiing WebSocketSingleShotEventListener");
auto wsSingleShotEventListener = std::make_shared< WebSocketSingleShotEventListener >(ws, _lock, _cond);
wsSingleShotEventListener->makeCallbacks();
return wsSingleShotEventListener;
};
return createEventListener();
}
bool AttachToCore(std::string_view uri, std::string_view tid) {
/// needed here only for rublon core api URL, so 1k fine /// needed here only for rublon core api URL, so 1k fine
memory::StrictMonotonic_1k_HeapResource memoryResource; memory::StrictMonotonic_1k_HeapResource memoryResource;
std::pmr::string str{uri.data(), uri.size(), &memoryResource}; std::pmr::string str{uri.data(), uri.size(), &memoryResource};
@ -214,19 +235,19 @@ class WebSocket {
str.replace(str.find(httpsPrefix), httpsPrefix.size(), "wss://"); str.replace(str.find(httpsPrefix), httpsPrefix.size(), "wss://");
} }
str += "/ws/socket.io/"; str += "/ws/socket.io/";
// ws.set_logs_verbose();
log(LogLevel::Debug, "WS connect to %s", str.c_str()); // ws.set_logs_verbose();
ws.connect(str.c_str()); ws.connect(str.c_str());
const auto attachToTransactionConfirmationChannel = [&](const auto & /*status*/) -> tl::expected< bool, bool > { const auto attachToTransactionConfirmationChannel = [&](const auto & /*status*/) -> tl::expected< bool, bool > {
log(LogLevel::Debug, "Initializiing attachToTransactionConfirmationChannel");
memory::StrictMonotonic_1k_HeapResource resource; memory::StrictMonotonic_1k_HeapResource resource;
std::pmr::string channel{&resource}; std::pmr::string channel{&resource};
channel.reserve(100); channel.reserve(100);
channel += "transactionConfirmation."; channel += "transactionConfirmation.";
channel += tid; channel += tid;
/// TODO check status
auto message = std::dynamic_pointer_cast< sio::object_message >(sio::object_message::create()); auto message = std::dynamic_pointer_cast< sio::object_message >(sio::object_message::create());
message->insert("channel", channel.c_str()); message->insert("channel", channel.c_str());
log(LogLevel::Debug, "emiting %s message on subscribe channel", channel.c_str()); log(LogLevel::Debug, "emiting %s message on subscribe channel", channel.c_str());
@ -235,18 +256,15 @@ class WebSocket {
return true; /// TODO return true; /// TODO
}; };
const auto waitForUserAction = [&](const auto & /*connected*/) -> tl::expected< WebSocketSingleShotEventListener::Status, bool > { auto wsConnectionListener = std::make_shared< WebSocketConnectionListener >(ws, _lock, _cond);
WebSocketSingleShotEventListener eventListener{ws, _lock, _cond}; wsConnectionListener->makeCallbacks();
return eventListener.waitForEvent();
};
auto v = WebSocketConnectionListener{ws, _lock, _cond} auto v = wsConnectionListener
.waitForConnection() // ->waitForConnection() //
.and_then(attachToTransactionConfirmationChannel) .and_then(attachToTransactionConfirmationChannel);
.and_then(waitForUserAction)
.value_or(WebSocketSingleShotEventListener::Status::Denied);
return v == WebSocketSingleShotEventListener::Status::Approved; log(LogLevel::Debug, "WS Connected");
return v.value_or(false);
} }
}; };
} // namespace rublon } // namespace rublon

9
PAM/ssh/lib/CMakeLists.txt Normal file → Executable file
View File

@ -8,20 +8,13 @@ set_target_properties(rublon-ssh-pam PROPERTIES OUTPUT_NAME "pam_rublon")
target_compile_options(rublon-ssh-pam target_compile_options(rublon-ssh-pam
PUBLIC PUBLIC
-fwhole-program
-fvisibility=hidden
-ffunction-sections
-fdata-sections
-fno-unwind-tables
-fno-asynchronous-unwind-tables
-flto -flto
-Wno-deprecated-declarations
) )
target_link_options(rublon-ssh-pam target_link_options(rublon-ssh-pam
PUBLIC PUBLIC
-fpic -fpic
-s
-Wl,--gc-sections
-flto -flto
) )

117
PAM/ssh/lib/pam.cpp Normal file → Executable file
View File

@ -5,7 +5,11 @@
#include <security/pam_modules.h> #include <security/pam_modules.h>
#include <syslog.h> #include <syslog.h>
#include <rublon/check_application.hpp>
#include <rublon/error.hpp>
#include <rublon/finish.hpp>
#include <rublon/rublon.hpp> #include <rublon/rublon.hpp>
#include <rublon/utils.hpp>
#define DLL_PUBLIC __attribute__((visibility("default"))) #define DLL_PUBLIC __attribute__((visibility("default")))
@ -23,43 +27,26 @@ DLL_PUBLIC int pam_sm_acct_mgmt([[maybe_unused]] pam_handle_t * pamh,
return PAM_SUCCESS; return PAM_SUCCESS;
} }
std::string g_tid;
DLL_PUBLIC int DLL_PUBLIC int
pam_sm_authenticate(pam_handle_t * pamh, [[maybe_unused]] int flags, [[maybe_unused]] int argc, [[maybe_unused]] const char ** argv) { pam_sm_authenticate(pam_handle_t * pamh, [[maybe_unused]] int flags, [[maybe_unused]] int argc, [[maybe_unused]] const char ** argv) {
using namespace rublon; using namespace rublon;
// std::freopen(rublon::details::logPath(), "a+", stdout); details::initLog();
LinuxPam pam{pamh}; LinuxPam pam{pamh};
// std::freopen(rublon::details::logPath(), "a+", stderr);
auto config = ConfigurationFactory{}.systemConfig();
if(not config.has_value()) {
pam.print("\n");
pam.print("Rublon configuration does not exists or is invalid");
pam.print("\tcheck '%s' for more details\n", details::logPath());
return PAM_SUCCESS;
}
std::byte sharedMemory[32 * 1024] = {};
std::pmr::monotonic_buffer_resource mr{sharedMemory, std::size(sharedMemory)};
std::pmr::unsynchronized_pool_resource rublonPoolResource{&mr};
std::pmr::set_default_resource(&rublonPoolResource);
CoreHandler CH{*config};
auto selectMethod = [&](const MethodSelect & selector) { return selector.create(pam); };
auto confirmMethod = [&](const PostMethod & confirm) { return confirm.fire(CH); };
auto confirmCode = [&](const MethodProxy & method) { return method.fire(CH, pam); };
auto printAuthMessageAndExit = [&](const AuthenticationStatus status) { auto printAuthMessageAndExit = [&](const AuthenticationStatus status) {
switch(status.action()) { switch(status.action()) {
case AuthenticationStatus::Action::Bypass: case AuthenticationStatus::Action::Bypass:
pam.print("\n\tRUBLON authentication bypased"); pam.print("RUBLON authentication BYPASSED");
return PAM_SUCCESS; return PAM_SUCCESS;
case AuthenticationStatus::Action::Denied: case AuthenticationStatus::Action::Denied:
pam.print("\n\tRUBLON authentication FAILED"); pam.print("RUBLON authentication FAILED");
return PAM_MAXTRIES; return PAM_MAXTRIES;
case AuthenticationStatus::Action::Confirmed: case AuthenticationStatus::Action::Confirmed:
pam.print("\n\tRUBLON authentication SUCCESS"); pam.print("RUBLON authentication SUCCEEDED");
return PAM_SUCCESS; return PAM_SUCCESS;
} }
@ -67,19 +54,45 @@ pam_sm_authenticate(pam_handle_t * pamh, [[maybe_unused]] int flags, [[maybe_unu
return PAM_MAXTRIES; return PAM_MAXTRIES;
}; };
auto allowLogin = [&](const AuthenticationStatus & status) -> tl::expected< int, Error > { auto session = rublon::RublonFactory{}.startSession(pam);
return printAuthMessageAndExit(status); if(not session.has_value()) {
return printAuthMessageAndExit(AuthenticationStatus::Action::Bypass);
}
auto & CH = session.value().coreHandler();
auto selectMethod = [&](const MethodSelect & selector) { return selector.create(pam); };
auto confirmMethod = [&](const PostMethod & postMethod) { return postMethod.handle(CH); };
auto confirmCode = [&](const MethodProxy & method) mutable { return method.fire(CH, pam); };
auto finalizeTransaction = [&](const AuthenticationStatus & status) -> tl::expected< AuthenticationStatus, Error > {
if(status.userAuthorized()) {
auto tok = std::string{status.accessToken().substr(10, 60).data(), 60};
Finish finish{session.value().config(), std::move(tok)};
finish.handle(CH);
}
return status;
}; };
auto allowLogin = [&](const AuthenticationStatus & status) -> tl::expected< int, Error > { return printAuthMessageAndExit(status); };
auto mapError = [&](const Error & error) -> tl::expected< int, Error > { auto mapError = [&](const Error & error) -> tl::expected< int, Error > {
log(LogLevel::Error, "Authorization interrupted with {%s::%s}", error.errorClassName(), error.categoryName()); log(LogLevel::Error, "Process interrupted by {%s::%s}", error.errorClassName(), error.categoryName());
if(error.is< RublonAuthenticationInterrupt >()) { if(error.is< RublonAuthenticationInterrupt >()) {
switch(error.get< RublonAuthenticationInterrupt >().errorClass) { switch(error.get< RublonAuthenticationInterrupt >().errorClass) {
case RublonAuthenticationInterrupt::ErrorClass::UserBaypass: case RublonAuthenticationInterrupt::ErrorClass::UserBaypass:
return printAuthMessageAndExit(AuthenticationStatus::Action::Bypass); return printAuthMessageAndExit(AuthenticationStatus::Action::Bypass);
case RublonAuthenticationInterrupt::ErrorClass::UserDenied: case RublonAuthenticationInterrupt::ErrorClass::UserDenied:
pam.print("Access denied! Contact your administrator for more information");
return printAuthMessageAndExit(AuthenticationStatus::Action::Denied); return printAuthMessageAndExit(AuthenticationStatus::Action::Denied);
case RublonAuthenticationInterrupt::ErrorClass::UserWaiting:
case RublonAuthenticationInterrupt::ErrorClass::UserPending: case RublonAuthenticationInterrupt::ErrorClass::UserPending:
pam.print(
"Your account is awaiting administrator's approval. \n"
"Contact your administrator and ask them to approve your account");
return printAuthMessageAndExit(AuthenticationStatus::Action::Denied);
case RublonAuthenticationInterrupt::ErrorClass::UserNotFound:
return printAuthMessageAndExit(AuthenticationStatus::Action::Bypass); return printAuthMessageAndExit(AuthenticationStatus::Action::Bypass);
} }
} }
@ -95,33 +108,71 @@ pam_sm_authenticate(pam_handle_t * pamh, [[maybe_unused]] int flags, [[maybe_unu
} }
} }
if(error.is< ConnectionError >()) { if(error.is< ConnectionError >()) {
if(config->bypass) { if(session.value().config().failMode == FailMode::deny) {
pam.print("Connection Error, bypass enabled"); pam.print("Incorrect response from the Rublon API, user bypassed");
return printAuthMessageAndExit(AuthenticationStatus::Action::Bypass); return printAuthMessageAndExit(AuthenticationStatus::Action::Bypass);
} else { } else {
pam.print("Connection Error, bypass disabled"); pam.print("Incorrect response from the Rublon API, user access denied");
return printAuthMessageAndExit(AuthenticationStatus::Action::Denied); return printAuthMessageAndExit(AuthenticationStatus::Action::Denied);
} }
} }
if(error.is< CoreHandlerError >()) { if(error.is< CoreHandlerError >()) {
const auto & reson = error.get< CoreHandlerError >().reson; const auto & reson = error.get< CoreHandlerError >().reson;
pam.print("\n RUBLON server returned '%s' exception", reson.c_str()); pam.print("Something went wrong and authentication could not be completed, contact your administrator");
if(reson == "UserBypassedException" or reson == "UserNotFoundException") if(reson == "UserBypassedException" or reson == "UserNotFoundException")
return printAuthMessageAndExit(AuthenticationStatus::Action::Bypass); return printAuthMessageAndExit(AuthenticationStatus::Action::Bypass);
} }
if(error.is< WerificationError >()) {
switch(error.get< WerificationError >().errorClass) {
case WerificationError::ErrorClass::PasscodeException:
pam.print(R"(Incorrect passcode)");
return printAuthMessageAndExit(AuthenticationStatus::Action::Denied);
case WerificationError::ErrorClass::BadInput:
pam.print(R"(Ensure that the Secret Key is correct.)");
return printAuthMessageAndExit(AuthenticationStatus::Action::Denied);
}
}
if(error.is< RublonCheckApplicationException >()) {
switch(error.get< RublonCheckApplicationException >().errorClass) {
case RublonCheckApplicationException::ErrorClass::ApplicationNotFoundException:
log(LogLevel::Error, R"(Could not find the application in the Rublon Admin Console.)");
log(LogLevel::Error, R"(Ensure that the application exists and the SystemToken is correct.)");
return printAuthMessageAndExit(AuthenticationStatus::Action::Denied);
case RublonCheckApplicationException::ErrorClass::InvalidSignatureException:
log(LogLevel::Error, R"(Could not verify the signature.)");
log(LogLevel::Error, R"(Ensure that the Secret Key is correct.)");
return printAuthMessageAndExit(AuthenticationStatus::Action::Denied);
case RublonCheckApplicationException::ErrorClass::UnsupportedVersionException:
log(LogLevel::Error, R"(The provided version of the app is unsupported.)");
log(LogLevel::Error, R"(Try changing the app version.)");
return printAuthMessageAndExit(AuthenticationStatus::Action::Denied);
}
}
return printAuthMessageAndExit(AuthenticationStatus::Action::Denied); return printAuthMessageAndExit(AuthenticationStatus::Action::Denied);
}; };
auto ret = Init{config.value()} {
.fire(CH, pam) // CheckApplication ca;
auto ret =
ca.call(CH, {session.value().config().systemToken.data(), session.value().config().systemToken.size()}).or_else(mapError);
if(not ret.has_value()) {
log(LogLevel::Error, "Check Application step failed, check configration");
return PAM_MAXTRIES;
}
}
auto ret = Init{session.value()}
.handle(CH, pam) //
.and_then(selectMethod) .and_then(selectMethod)
.and_then(confirmMethod) .and_then(confirmMethod)
.and_then(confirmCode) .and_then(confirmCode)
.and_then(finalizeTransaction)
.and_then(allowLogin) .and_then(allowLogin)
.or_else(mapError); .or_else(mapError);

0
PAM/ssh/tests/CMakeLists.txt Normal file → Executable file
View File

2
PAM/ssh/tests/authentication_step_common_tests.cpp Normal file → Executable file
View File

@ -1,3 +1,5 @@
#include <gtest/gtest.h> #include <gtest/gtest.h>
#include <rublon/authentication_step_interface.hpp> #include <rublon/authentication_step_interface.hpp>
#include <tl/expected.hpp> #include <tl/expected.hpp>
std::string g_tid="";

0
PAM/ssh/tests/core_handler_mock.hpp Normal file → Executable file
View File

0
PAM/ssh/tests/core_handler_tests.cpp Normal file → Executable file
View File

0
PAM/ssh/tests/core_response_generator.hpp Normal file → Executable file
View File

0
PAM/ssh/tests/gtest_matchers.hpp Normal file → Executable file
View File

6
PAM/ssh/tests/http_mock.hpp Normal file → Executable file
View File

@ -20,8 +20,7 @@ rublon::Configuration conf{rublon::Configuration{//
true, true,
true, true,
false, false,
false, rublon::FailMode::safe}};
true}};
rublon::Configuration confBypass{rublon::Configuration{// rublon::Configuration confBypass{rublon::Configuration{//
{"320BAB778C4D4262B54CD243CDEFFAFD"}, {"320BAB778C4D4262B54CD243CDEFFAFD"},
{"39e8d771d83a2ed3cc728811911c25"}, {"39e8d771d83a2ed3cc728811911c25"},
@ -30,8 +29,7 @@ rublon::Configuration confBypass{rublon::Configuration{//
true, true,
true, true,
false, false,
true, rublon::FailMode::safe}};
true}};
inline std::string randomTID() { inline std::string randomTID() {
static const char alphanum[] = static const char alphanum[] =

0
PAM/ssh/tests/init_test.cpp Normal file → Executable file
View File

0
PAM/ssh/tests/method_select_tests.cpp Normal file → Executable file
View File

0
PAM/ssh/tests/pam_info_mock.hpp Normal file → Executable file
View File

0
PAM/ssh/tests/passcode_auth_tests.cpp Normal file → Executable file
View File

0
PAM/ssh/tests/sign_tests.cpp Normal file → Executable file
View File

0
PAM/ssh/tests/utils_tests.cpp Normal file → Executable file
View File

View File

@ -36,7 +36,7 @@ Vagrant.configure("2") do |config|
config.vm.provision "shell", inline: <<-SHELL config.vm.provision "shell", inline: <<-SHELL
yum update yum update
yum install -y gcc openssl-devel curl-devel pam-devel git rapidjson-devel cmake policycoreutils-devel checkpolicy yum install -y gcc openssl-devel libcurl-devel pam-devel git rapidjson-devel cmake policycoreutils-devel checkpolicy
# get dependencies # get dependencies
git clone --recurse-submodules https://github.com/socketio/socket.io-client-cpp.git git clone --recurse-submodules https://github.com/socketio/socket.io-client-cpp.git

View File

@ -57,10 +57,10 @@ Vagrant.configure("2") do |config|
# Build project # Build project
cd /home/vagrant/Rublon-Linux cd /home/vagrant/Rublon-Linux
cmake -B build && cmake --build build cmake -B buildir && cmake --build buildir
# Install # Install
sudo cmake --install build sudo cmake --install buildir
sudo install -m 644 rsc/rublon.config.defaults /etc/rublon.config sudo install -m 644 rsc/rublon.config.defaults /etc/rublon.config
# Register Rublon pam # Register Rublon pam
@ -79,6 +79,4 @@ Vagrant.configure("2") do |config|
service sshd restart service sshd restart
SHELL SHELL
config.vm.provision "file", source: "/etc/rublon.config", destination: "/etc/rublon.config"
end end

View File

@ -25,6 +25,8 @@ Vagrant.configure("2") do |config|
config.vm.provider "virtualbox" do |vb| config.vm.provider "virtualbox" do |vb|
# Display the VirtualBox GUI when booting the machine # Display the VirtualBox GUI when booting the machine
vb.gui = true vb.gui = true
vb.memory = 1024
vb.cpus = 4
# Fix for 'SSH auth method: Private key' stuck # Fix for 'SSH auth method: Private key' stuck
vb.customize ["modifyvm", :id, "--cableconnected1", "on"] vb.customize ["modifyvm", :id, "--cableconnected1", "on"]
@ -47,6 +49,12 @@ Vagrant.configure("2") do |config|
rapidjson-dev \ rapidjson-dev \
cmake cmake
# get dependencies
git clone --recurse-submodules https://github.com/socketio/socket.io-client-cpp.git
mkdir socket.io-client-cpp/build; cd socket.io-client-cpp/build
cmake -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_BUILD_TYPE=Release ..
cmake --build . --target install
# Build project # Build project
cd /home/vagrant/Rublon-Linux cd /home/vagrant/Rublon-Linux
cmake -B build && cmake --build build cmake -B build && cmake --build build
@ -70,4 +78,5 @@ Vagrant.configure("2") do |config|
echo "bwi:bwi"|chpasswd echo "bwi:bwi"|chpasswd
service sshd restart service sshd restart
SHELL
end end

13
os/rhel/9/Vagrantfile vendored
View File

@ -38,8 +38,14 @@ Vagrant.configure("2") do |config|
config.vm.provision "shell", inline: <<-SHELL config.vm.provision "shell", inline: <<-SHELL
yum update yum update
yum install -y gcc openssl-devel curl-devel pam-devel git rapidjson-devel cmake yum install -y gcc openssl-devel libcurl systemd-pam git-review rapidjson-devel cmake
wget https://mirrors.edge.kernel.org/pub/software/scm/git/git-2.9.4.tar.xz
tar -xf git*
cd git*
sudo make configure
sudo ./configure --prefix=/usr
sudo make all
sudo make install
# get dependencies # get dependencies
git clone --recurse-submodules https://github.com/socketio/socket.io-client-cpp.git git clone --recurse-submodules https://github.com/socketio/socket.io-client-cpp.git
mkdir socket.io-client-cpp/build; cd socket.io-client-cpp/build mkdir socket.io-client-cpp/build; cd socket.io-client-cpp/build
@ -56,7 +62,7 @@ Vagrant.configure("2") do |config|
cmake -B build && cmake --build build cmake -B build && cmake --build build
# Install # Install
sudo cmake --install build sudo cmake --install build
sudo install -m 644 rsc/rublon.config.defaults /etc/rublon.config sudo install -m 644 /home/vagrant/Rublon-Linux/rsc/rublon.config.defaults /etc/rublon.config
#handle semodule #handle semodule
cd /home/vagrant/Rublon-Linux/service cd /home/vagrant/Rublon-Linux/service
@ -74,6 +80,5 @@ Vagrant.configure("2") do |config|
systemctl restart sshd.service systemctl restart sshd.service
SHELL SHELL
config.vm.provision "file", source: "/etc/rublon.config", destination: "/etc/rublon.config"
end end

View File

@ -23,8 +23,10 @@ Vagrant.configure("2") do |config|
config.vm.synced_folder "../../..", "/home/vagrant/Rublon-Linux" config.vm.synced_folder "../../..", "/home/vagrant/Rublon-Linux"
config.vm.provider "virtualbox" do |vb| config.vm.provider "virtualbox" do |vb|
vb.memory = 2560
vb.vcpu = 4
# Display the VirtualBox GUI when booting the machine # Display the VirtualBox GUI when booting the machine
vb.gui = true vb.gui = false
# Fix for 'SSH auth method: Private key' stuck # Fix for 'SSH auth method: Private key' stuck
vb.customize ["modifyvm", :id, "--cableconnected1", "on"] vb.customize ["modifyvm", :id, "--cableconnected1", "on"]
@ -34,10 +36,14 @@ Vagrant.configure("2") do |config|
# Ansible, Chef, Docker, Puppet and Salt are also available. Please see the # Ansible, Chef, Docker, Puppet and Salt are also available. Please see the
# documentation for more information about their specific syntax and use. # documentation for more information about their specific syntax and use.
config.vm.provision "shell", inline: <<-SHELL config.vm.provision "shell", inline: <<-SHELL
export BASE=/home/vagrant/Rublon-Linux
export DISTRO=ubuntu2004
export BUILDDIR=${BASE}/${DISTRO}
DEBIAN_FRONTEND=noniteracactive\ DEBIAN_FRONTEND=noniteracactive\
apt-get update && apt-get install -y \ apt-get update && apt-get install -y \
gcc \ gcc \
g++ \
build-essential \ build-essential \
openssh-server \ openssh-server \
libcurl4-openssl-dev \ libcurl4-openssl-dev \
@ -48,33 +54,24 @@ Vagrant.configure("2") do |config|
cmake cmake
# get dependencies # get dependencies
mkdir ${BUILDDIR} -p; cd ${BUILDDIR}
git clone --recurse-submodules https://github.com/socketio/socket.io-client-cpp.git git clone --recurse-submodules https://github.com/socketio/socket.io-client-cpp.git
mkdir socket.io-client-cpp/build; cd socket.io-client-cpp/build cmake -B socket.io-build -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_BUILD_TYPE=Release socket.io-client-cpp
cmake -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_BUILD_TYPE=Release .. cmake --build socket.io-build --target install -- -j
cmake --build . --target install ln -s /usr/bin/make /usr/bin/gmake
# Build project # Build project
cd /home/vagrant/Rublon-Linux cd ${BUILDDIR}
cmake -B build && cmake --build build cmake -B rublon_ssh-build ..
cmake --build rublon_ssh-build -- -j
# Install cmake --build rublon_ssh-build --target package -- -j
sudo cmake --install build
sudo install -m 644 rsc/rublon.config.defaults /etc/rublon.config
# Register Rublon pam # Register Rublon pam
SSHD_CONF=/etc/ssh/sshd_config SSHD_CONF=/etc/ssh/sshd_config
SSHD_PAM_CONF=/etc/pam.d/sshd grep -qe "^PasswordAuthentication" $SSHD_CONF && sed -i 's/^#*PasswordAuthentication[[:space:]]\+.*/PasswordAuthentication yes/' $SSHD_CONF
grep -qe "^PasswordAuthentication" $SSHD_CONF && sed -i 's/^#*PasswordAuthentication[[:space:]]\+.*/PasswordAuthentication yes/' $SSHD_CONF || echo "PasswordAuthentication yes" >> $SSHD_CONF
grep -qe "^ChallengeResponseAuthentication" $SSHD_CONF && sed -i 's/^#*ChallengeResponseAuthentication[[:space:]]\+.*/ChallengeResponseAuthentication yes/' $SSHD_CONF || echo "ChallengeResponseAuthentication yes" >> $SSHD_CONF
grep -qe "^UsePAM" $SSHD_CONF && sed -i 's/^#*UsePAM[[:space:]]\+.*/UsePAM yes/' $SSHD_CONF || echo "UsePAM yes" >> $SSHD_CONF
grep -qe 'auth required pam_rublon.so' $SSHD_PAM_CONF || sed -i '\$aauth required pam_rublon.so' $SSHD_PAM_CONF
grep -qe 'account required pam_rublon.so' $SSHD_PAM_CONF || sed -i '\$aaccount required pam_rublon.so' $SSHD_PAM_CONF
useradd -s /bin/bash -m bwi useradd -s /bin/bash -m bwi
echo "bwi:bwi"|chpasswd echo "bwi:bwi"|chpasswd
sudo dpkg -i /home/vagrant/Rublon-Linux/${BUILDDIR}/*pam_*
cp /home/vagrant/Rublon-Linux/rublon.config /etc/rublon.config
service sshd restart service sshd restart
SHELL SHELL
end end

View File

@ -23,8 +23,10 @@ Vagrant.configure("2") do |config|
config.vm.synced_folder "../../..", "/home/vagrant/Rublon-Linux" config.vm.synced_folder "../../..", "/home/vagrant/Rublon-Linux"
config.vm.provider "virtualbox" do |vb| config.vm.provider "virtualbox" do |vb|
vb.memory = 2560
vb.vcpu = 4
# Display the VirtualBox GUI when booting the machine # Display the VirtualBox GUI when booting the machine
vb.gui = true vb.gui = false
# Fix for 'SSH auth method: Private key' stuck # Fix for 'SSH auth method: Private key' stuck
vb.customize ["modifyvm", :id, "--cableconnected1", "on"] vb.customize ["modifyvm", :id, "--cableconnected1", "on"]
@ -64,17 +66,17 @@ Vagrant.configure("2") do |config|
# Register Rublon pam # Register Rublon pam
SSHD_CONF=/etc/ssh/sshd_config SSHD_CONF=/etc/ssh/sshd_config
SSHD_PAM_CONF=/etc/pam.d/sshd SSHD_PAM_CONF=/etc/pam.d/sshd
SSHD_CONFD=/etc/ssh/sshd_config.d/60-cloudimg-settings.conf
grep -qe "^PasswordAuthentication" $SSHD_CONF && sed -i 's/^#*PasswordAuthentication[[:space:]]\+.*/PasswordAuthentication yes/' $SSHD_CONF || echo "PasswordAuthentication yes" >> $SSHD_CONF grep -qe "^PasswordAuthentication" $SSHD_CONF && sed -i 's/^#*PasswordAuthentication[[:space:]]\+.*/PasswordAuthentication yes/' $SSHD_CONF
grep -qe "^ChallengeResponseAuthentication" $SSHD_CONF && sed -i 's/^#*ChallengeResponseAuthentication[[:space:]]\+.*/ChallengeResponseAuthentication yes/' $SSHD_CONF || echo "ChallengeResponseAuthentication yes" >> $SSHD_CONF sed -i 's/PasswordAuthentication no/PasswordAuthentication yes/' $SSHD_CONFD
grep -qe "^UsePAM" $SSHD_CONF && sed -i 's/^#*UsePAM[[:space:]]\+.*/UsePAM yes/' $SSHD_CONF || echo "UsePAM yes" >> $SSHD_CONF
grep -qe 'auth required pam_rublon.so' $SSHD_PAM_CONF || sed -i '\$aauth required pam_rublon.so' $SSHD_PAM_CONF
grep -qe 'account required pam_rublon.so' $SSHD_PAM_CONF || sed -i '\$aaccount required pam_rublon.so' $SSHD_PAM_CONF
useradd -s /bin/bash -m bwi useradd -s /bin/bash -m bwi
echo "bwi:bwi"|chpasswd echo "bwi:bwi"|chpasswd
cd ./build
make package
sudo dpkg -i /home/vagrant/Rublon-Linux/build/*pam_*
cp /home/vagrant/Rublon-Linux/rublon.config /etc/rublon.config
service sshd restart service sshd restart
SHELL SHELL
end end

84
os/ubuntu/24.04/Vagrantfile vendored Normal file
View File

@ -0,0 +1,84 @@
# -*- mode: ruby -*-
# vi: set ft=ruby :
# Default user
# ----------------------
# login: vagrant
# pass: vagrant
Vagrant.configure("2") do |config|
# Basic configuration
config.vm.box = "alvistack/ubuntu-24.04"
config.ssh.forward_agent = true
# Create a public network, which generally matched to bridged network.
# Bridged networks make the machine appear as another physical device on
# your network.
config.vm.network "public_network"
# Share an additional folder to the guest VM. The first argument is
# the path on the host to the actual folder. The second argument is
# the path on the guest to mount the folder. And the optional third
# argument is a set of non-required options.
config.vm.synced_folder "../../..", "/home/vagrant/Rublon-Linux"
config.vm.provider "virtualbox" do |vb|
vb.memory = 2560
vb.vcpu = 4
# Display the VirtualBox GUI when booting the machine
vb.gui = false
# Fix for 'SSH auth method: Private key' stuck
vb.customize ["modifyvm", :id, "--cableconnected1", "on"]
end
# Enable provisioning with a shell script. Additional provisioners such as
# Ansible, Chef, Docker, Puppet and Salt are also available. Please see the
# documentation for more information about their specific syntax and use.
config.vm.provision "shell", inline: <<-SHELL
DEBIAN_FRONTEND=noniteracactive\
apt-get update && apt-get install -y \
gcc \
build-essential \
openssh-server \
libcurl4-openssl-dev \
libpam0g-dev \
libssl-dev \
git \
rapidjson-dev \
cmake
# get dependencies
git clone --recurse-submodules https://github.com/socketio/socket.io-client-cpp.git
mkdir socket.io-client-cpp/build; cd socket.io-client-cpp/build
cmake -DCMAKE_POSITION_INDEPENDENT_CODE=ON -DCMAKE_BUILD_TYPE=Release ..
cmake --build . --target install
# Build project
cd /home/vagrant/Rublon-Linux
cmake -B build && cmake --build build
# Install
sudo cmake --install build
sudo install -m 644 rsc/rublon.config.defaults /etc/rublon.config
# Register Rublon pam
SSHD_CONF=/etc/ssh/sshd_config
SSHD_PAM_CONF=/etc/pam.d/sshd
grep -qe "^PasswordAuthentication" $SSHD_CONF && sed -i 's/^#*PasswordAuthentication[[:space:]]\+.*/PasswordAuthentication yes/' $SSHD_CONF || echo "PasswordAuthentication yes" >> $SSHD_CONF
grep -qe "^ChallengeResponseAuthentication" $SSHD_CONF && sed -i 's/^#*ChallengeResponseAuthentication[[:space:]]\+.*/ChallengeResponseAuthentication yes/' $SSHD_CONF || echo "ChallengeResponseAuthentication yes" >> $SSHD_CONF
grep -qe "^UsePAM" $SSHD_CONF && sed -i 's/^#*UsePAM[[:space:]]\+.*/UsePAM yes/' $SSHD_CONF || echo "UsePAM yes" >> $SSHD_CONF
grep -qe 'auth required pam_rublon.so' $SSHD_PAM_CONF || sed -i '\$aauth required pam_rublon.so' $SSHD_PAM_CONF
grep -qe 'account required pam_rublon.so' $SSHD_PAM_CONF || sed -i '\$aaccount required pam_rublon.so' $SSHD_PAM_CONF
useradd -s /bin/bash -m bwi
echo "bwi:bwi"|chpasswd
cd ./build
make package
sudo dpkg -i /home/vagrant/Rublon-Linux/build/*pam_*
cp /home/vagrant/Rublon-Linux/rublon.config /etc/rublon.config
service sshd restart
SHELL
end

4
pack.cmake Normal file → Executable file
View File

@ -12,7 +12,6 @@ set(CPACK_PACKAGE_VENDOR "Rublon")
#set(CPACK_VERBATIM_VARIABLES YES) #set(CPACK_VERBATIM_VARIABLES YES)
set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR}) set(CPACK_PACKAGE_VERSION_MAJOR ${PROJECT_VERSION_MAJOR})
set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR}) set(CPACK_PACKAGE_VERSION_MINOR ${PROJECT_VERSION_MINOR})
set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH}) set(CPACK_PACKAGE_VERSION_PATCH ${PROJECT_VERSION_PATCH})
@ -30,8 +29,7 @@ set(CPACK_DEBIAN_PACKAGE_SHLIBDEPS YES)
set(CPACK_GENERATOR "DEB") set(CPACK_GENERATOR "DEB")
# set(CPACK_DEBIAN_DEV_PACKAGE_DEPENDS "libcurl4(>= 7.0.0), libc(>= 2.0)") # set(CPACK_DEBIAN_DEV_PACKAGE_DEPENDS "libcurl4(>= 7.0.0), libc(>= 2.0)")
# set(CPACK_DEBIAN_PACKAGE_DEPENDS "libcurl4(>= 7.0.0), libc(>= 2.0)") # set(CPACK_DEBIAN_PACKAGE_DEPENDS "libcurl4(>= 7.0.0), libc(>= 2.0), libssl(>= 1.0)")
set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_CURRENT_SOURCE_DIR}/service/postinst") set(CPACK_DEBIAN_PACKAGE_CONTROL_EXTRA "${CMAKE_CURRENT_SOURCE_DIR}/service/postinst")
include(CPack) include(CPack)

3
rsc/rublon.config.defaults Normal file → Executable file
View File

@ -2,8 +2,7 @@ systemToken=
secretKey= secretKey=
userDomain= userDomain=
rublonApiServer=https://core.rublon.net rublonApiServer=https://core.rublon.net
failMode=bypass failMode=safe
offlineBypass=true
prompt=1 prompt=1
logging=true logging=true
enablePasswdEmail=true enablePasswdEmail=true

0
service/login_rublon.mod Normal file → Executable file
View File

0
service/login_rublon.pp Normal file → Executable file
View File

0
service/login_rublon.te Normal file → Executable file
View File

18
service/postinst Normal file → Executable file
View File

@ -6,33 +6,29 @@ RUBLON_CONFIG=/etc/rublon.config
if [ ! -f /etc/rublon.config ] if [ ! -f /etc/rublon.config ]
then then
mkdir -p /etc/rublon
cp -a /usr/share/rublon/rublon.config.defaults $RUBLON_CONFIG cp -a /usr/share/rublon/rublon.config.defaults $RUBLON_CONFIG
chown root:root $RUBLON_CONFIG chown root:root $RUBLON_CONFIG
chmod 640 $RUBLON_CONFIG chmod 640 $RUBLON_CONFIG
fi fi
# get system id if [ -f /etc/os-release ]
then
. /etc/os-release . /etc/os-release
fi
grep -qe "^PasswordAuthentication" $SSHD_CONF && \ grep -qe "^PasswordAuthentication" $SSHD_CONF && \
sed -i 's/^#*PasswordAuthentication[[:space:]]\+.*/PasswordAuthentication yes/' $SSHD_CONF || \ sed -i 's/^#*PasswordAuthentication[[:space:]]\+.*/PasswordAuthentication yes/' $SSHD_CONF || \
echo "PasswordAuthentication yes" >> $SSHD_CONF echo "PasswordAuthentication yes" >> $SSHD_CONF
grep -qe "^ChallengeResponseAuthentication" $SSHD_CONF && \ grep -qe "^ChallengeResponseAuthentication" $SSHD_CONF && \
sed -i 's/^#*ChallengeResponseAuthentication[[:space:]]\+.*/ChallengeResponseAuthentication yes/' $SSHD_CONF || \ sed -i 's/^#*ChallengeResponseAuthentication[[:space:]]\+.*/ChallengeResponseAuthentication yes/' $SSHD_CONF || \
echo "ChallengeResponseAuthentication yes" >> $SSHD_CONF echo "ChallengeResponseAuthentication yes" >> $SSHD_CONF
grep -qe "^UsePAM" $SSHD_CONF && \ grep -qe "^UsePAM" $SSHD_CONF && \
sed -i 's/^#*UsePAM[[:space:]]\+.*/UsePAM yes/' $SSHD_CONF || \ sed -i 's/^#*UsePAM[[:space:]]\+.*/UsePAM yes/' $SSHD_CONF || \
echo "UsePAM yes" >> $SSHD_CONF echo "UsePAM yes" >> $SSHD_CONF
sed -i 's/KbdInteractiveAuthentication/#KbdInteractiveAuthentication/' $SSHD_CONF
grep -qe 'auth required pam_rublon.so' $SSHD_PAM_CONF || sed -i '$aauth required pam_rublon.so' $SSHD_PAM_CONF grep -qe 'auth required pam_rublon.so' $SSHD_PAM_CONF || sed -i '$aauth required pam_rublon.so' $SSHD_PAM_CONF
grep -qe 'account required pam_rublon.so' $SSHD_PAM_CONF || sed -i '$aaccount required pam_rublon.so' $SSHD_PAM_CONF grep -qe 'account required pam_rublon.so' $SSHD_PAM_CONF || sed -i '$aaccount required pam_rublon.so' $SSHD_PAM_CONF
# ### UBUNTU 22.04 notes
# on ubuntu 22.04 options
# PasswordAuthentication yes
# ChallengeResponseAuthentication yes
# need to be set in configuration file AFTER UsePAM yes
# it will fail to use PAM (and will lock the machine) it this is not true