diff --git a/CMakeLists.txt b/CMakeLists.txt index d93d407..15c7bbd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -52,6 +52,7 @@ include_directories(/usr/local/include /usr/local/opt/openssl/include src inclu add_executable( ucentralsec build src/Daemon.h src/Daemon.cpp + src/MicroService.cpp src/MicroService.h src/SubSystemServer.cpp src/SubSystemServer.h src/RESTAPI_unknownRequestHandler.h src/RESTAPI_unknownRequestHandler.cpp src/RESTAPI_oauth2Handler.h src/RESTAPI_oauth2Handler.cpp @@ -66,7 +67,10 @@ add_executable( ucentralsec src/storage_identity.cpp src/Utils.cpp src/Utils.h src/storage_sqlite.cpp src/storage_odbc.cpp src/storage_sqlite.cpp src/storage_pgql.cpp src/storage_mysql.cpp - src/storage_tables.cpp) + src/storage_tables.cpp src/SMTPMailerService.cpp src/SMTPMailerService.h + src/RESTAPI_users_handler.cpp src/RESTAPI_users_handler.h + src/RESTAPI_user_handler.cpp src/RESTAPI_user_handler.h + src/RESTAPI_action_links.cpp src/RESTAPI_action_links.h) target_link_libraries(ucentralsec PUBLIC ${Poco_LIBRARIES} ${Boost_LIBRARIES} ${ZLIB_LIBRARIES} ${AWSSDK_LINK_LIBRARIES} CppKafka::cppkafka ) diff --git a/docs/design/email_verification.md b/docs/design/email_verification.md new file mode 100644 index 0000000..bd853f4 --- /dev/null +++ b/docs/design/email_verification.md @@ -0,0 +1,36 @@ +# User Validation Actions +The system uses email and links to offer user validation. Validation may include email verification, password reset +requests, etc. + +## Action links +All action links will come back to the endpoint +``` +https://site.com:port/api/v1/actions?command=payload +``` + +## Payload +The system encrypts the payload with its private key. A base64 encoder convert the encrypted message to something +valid for a URI. + +```json +{ + "id" : "uuid", + "email" : "email@host.com", + "type" : "verificationType" +} +``` + +- `id` : UUID of the request that should be outstanding +- `email` : email address of the user under verification +- `type` : could be one of "emailValidation", "passwordResetRequest", or other. + +## Templates +Email templates come in 2 flavors: txt and html. The system replaces the following occurrences with system variables: + +- {{action}} : the link that the user should press. That link should include the payload as above. +- {{name}} : the name of the user field. +- {{recipient}} : user email, asi-is + + + + diff --git a/docs/design/user_creation_flow.md b/docs/design/user_creation_flow.md new file mode 100644 index 0000000..bbf6e02 --- /dev/null +++ b/docs/design/user_creation_flow.md @@ -0,0 +1,109 @@ +# USER creation flow + +## pre-requisite +To create a user in the system, someone must first login as the super user as configured in the properties file. Once logged in as super user, +thata person should create another user and convey the super user bit to them too. From that point on, only that second +created super user should be used. We will call superuser root0 and the created superuser root1 + +## About usernames + +### email is your username +Your email address is your username. The username is case-insensitive. + +### ASCII characters only +Usernames must only use ASCII characters. + +### forcing domain names +You can allow only certain domain names by configuring the service with `email.includeonly` parameter +```asm +email.includeonly = mycorporatedomain.com +``` + +### excluding email domains +You may exclude e-mail domains you will not accept emails from in the configuration. +You could, for example, not allow people in gmail by adding +```asm +email.exclude = gmail.com +``` + +### precedence +If `email.includeonly` is used, `email.exclude` is ignored. + +## Creating a username +In order to create a username, root1 must use the `/user/0` API call. The creation of a username involves: +- the service will email the new user to verify her email address +- the username remains dormant until the email verification completes +- the email verification maybe canceled anytime by deleting the username +- the email verification process times-out after `email.verification.timeout` in minutes +- the new user must change her password using the `/oauth2?changePassword=true` and filling in a `WebTokenRequestChangePassword` request +- the system will not accept any other calls until the user has changed her password + +## Values accepted in user creation +The user creation request must provide the following in the `UserInfo` of the `post`. + +```asm + id required = 0 + name optional = a string for the user display + description: optional = a description of this user + avatar: optional = an avatar URI + email required = valid email address used as user name + validated: ignored + validationEmail: ignored + validationDate: ignored + created: ignored + valiadationURI: ignored + changePassword: ignored + lastLogin: ignored + currentLoginURI: ignored + lastPasswordChange: ignored + lastEmailCheck: ignored + currentPassword: ignored + lastPasswords: ignored + waitingForEmailCheck: ignored + notes: optional = cumulative notes that may be added in for this user + location: optionsl = UUID of a provisioning server location + owner: optional = UUID of a providioning server owner + suspended: optional = if true, the user can change password but not do anything else + blackListed: ignored + locale: optional = 2 letter code of country language, default to EN. If the language specified is not supported, EN is assumed. + userType: required = root/admin/csr/sub/system/special, defaults to sub + oauthType: optional = if using oauth, a recognized oauth provider + oauthUserInfo: ignored +``` + +## Values accepted during user update +When doing a `put`, these are the accepted fields. + +```asm + id required = must match the ID in the path + name optional = a string for the user display + description: optional = a description of this user + avatar: optional = an avatar URI + email ignored + validated: ignored + validationEmail: ignored + validationDate: ignored + created: ignored + valiadationURI: ignored + changePassword: optonal = set to true to force a password change for the user + lastLogin: ignored + currentLoginURI: ignored + lastPasswordChange: ignored + lastEmailCheck: ignored + currentPassword: ignored + lastPasswords: ignored + waitingForEmailCheck: ignored + notes: optional = cumulative notes that may be added in for this user + location: optionsl = UUID of a provisioning server location + owner: optional = UUID of a providioning server owner + suspended: optional = if true, the user can change password but not do anything else + blackListed: optional = if true, user cannot login/deleted + locale: optional = 2 letter code of country language, default to EN. If the language specified is not supported, EN is assumed. + userType: required = root/admin/csr/sub/system/special, defaults to sub + oauthType: optional = if using oauth, a recognized oauth provider + oauthUserInfo: ignored +``` + + + + diff --git a/openpapi/ucentralsec/ucentralsec.yaml b/openpapi/ucentralsec/ucentralsec.yaml index de9ddf7..9b96faf 100644 --- a/openpapi/ucentralsec/ucentralsec.yaml +++ b/openpapi/ucentralsec/ucentralsec.yaml @@ -2,7 +2,7 @@ openapi: 3.0.1 info: title: uCentral Security API description: A process to manage security logins - version: 0.0.1 + version: 0.0.2 license: name: BSD3 url: https://github.com/Telecominfraproject/wlan-cloud-ucentralgw/blob/master/LICENSE @@ -64,7 +64,6 @@ components: format: uuid schemas: - GenericErrorResponse: description: Typical error response properties: @@ -104,6 +103,28 @@ components: userId: support@example.com password: support + WebTokenRequestChangePassword: + description: User Id and password. + type: object + required: + - userId + - password + properties: + userId: + type: string + default: support@example.com + oldPassword: + type: string + default: support + newPassword: + type: string + default: support + refreshToken: + type: string + example: + userId: support@example.com + password: support + WebTokenResult: description: Login and Refresh Tokens to be used in subsequent API calls. type: object @@ -125,6 +146,8 @@ components: created: type: integer format: int64 + userMustChangePassword: + type: boolean aclTemplate: $ref: '#/components/schemas/WebTokenAclTemplate' @@ -203,6 +226,109 @@ components: items: $ref: '#/components/schemas/SystemEndpoint' + UserInfo: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + description: + type: string + avatar: + type: string + format: uri + email: + type: string + validated: + type: boolean + validationEmail: + type: string + validationDate: + type: integer + format: int64 + created: + type: integer + format: int64 + valiadationURI: + type: string + changePassword: + type: boolean + lastLogin: + type: integer + format: int64 + currentLoginURI: + type: string + lastPasswordChange: + type: integer + format: int64 + lastEmailCheck: + type: integer + format: int64 + currentPassword: + type: string + lastPasswords: + type: array + items: + type: string + waitingForEmailCheck: + type: boolean + notes: + type: string + location: + type: string + format: uuid + owner: + type: string + format: uuid + suspended: + type: boolean + blackListed: + type: boolean + locale: + type: string + userRole: + type: string + enum: + - root + - admin + - sub + - csr + - system + - special + oauthType: + type: string + enum: + - internal + - normal + - gmail + - facebook + - linkedin + - instagram + oauthUserInfo: + type: string + + UserList: + type: object + properties: + list: + type: array + items: + $ref: '#/components/schemas/UserInfo' + + + ######################################################################################### + ## + ## These are endpoints that all services in the uCentral stack must provide + ## + ######################################################################################### + AnyPayload: + type: object + properties: + Document: + type: string + StringList: type: object properties: @@ -249,6 +375,54 @@ components: - $ref: '#/components/schemas/StringList' - $ref: '#/components/schemas/TagValuePairList' + ProfileAction: + type: object + properties: + resource: + type: string + access: + type: string + enum: + - READ + - MODIFY + - DELETE + - CREATE + - TEST + - MOVE + + SecurityProfile: + type: object + properties: + id: + type: integer + format: int64 + name: + type: string + description: + type: string + policy: + type: array + items: + $ref: '#/components/schemas/ProfileAction' + role: + type: string + notes: + type: string + + SecurityProfileList: + type: object + properties: + profiles: + type: array + items: + $ref: '#/components/schemas/SecurityProfile' + + ######################################################################################### + ## + ## End of uCentral system wide values + ## + ######################################################################################### + paths: /oauth2: @@ -257,13 +431,21 @@ paths: - Authentication summary: Get access token - to be used as Bearer token header for all other API requests. operationId: getAccessToken + parameters: + - in: query + name: changePassword + schema: + type: string + required: false requestBody: description: User id and password required: true content: application/json: schema: - $ref: '#/components/schemas/WebTokenRequest' + oneOf: + - $ref: '#/components/schemas/WebTokenRequestChangePassword' + - $ref: '#/components/schemas/WebTokenRequest' responses: 200: description: successful operation @@ -319,6 +501,140 @@ paths: 404: $ref: '#/components/responses/NotFound' + /users: + get: + tags: + - User Management + summary: Retrieve a list of existing users as well as some information about them. + operationId: getUsers + parameters: + - in: query + name: offset + schema: + type: integer + format: int64 + required: false + - in: query + name: limit + schema: + type: integer + format: int64 + required: false + - in: query + description: Selecting this option means the newest record will be returned. Use limit to select how many. + name: filter + schema: + type: string + required: false + responses: + 200: + $ref: '#/components/schemas/UserList' + 403: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + + /user/{id}: + get: + tags: + - User Management + operationId: getUser + summary: Retrieve the information for a single user + parameters: + - in: path + name: id + schema: + type: integer + format: int64 + required: true + responses: + 200: + $ref: '#/components/schemas/UserInfo' + 403: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + + delete: + tags: + - User Management + operationId: deleteUser + summary: Delete s single user + parameters: + - in: path + name: id + schema: + type: integer + format: int64 + required: true + responses: + 200: + $ref: '#/components/responses/Success' + 403: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + + post: + tags: + - User Management + operationId: createUser + summary: Create a single user + parameters: + - in: path + name: id + #must be set to 0 for user creation + schema: + type: integer + format: int64 + required: true + requestBody: + description: User details (some fields are ignored during creation) + content: + application/json: + schema: + $ref: '#/components/schemas/UserInfo' + responses: + 200: + $ref: '#/components/schemas/UserInfo' + 403: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + + put: + tags: + - User Management + operationId: updateUser + summary: Modifying a single user + parameters: + - in: path + name: id + schema: + type: integer + format: int64 + required: true + requestBody: + description: User details (some fields are ignored during update) + content: + application/json: + schema: + $ref: '#/components/schemas/UserInfo' + responses: + 200: + $ref: '#/components/schemas/UserInfo' + 403: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + + + ######################################################################################### + ## + ## These are endpoints that all services in the uCentral stack must provide + ## + ######################################################################################### + /system: post: tags: @@ -342,3 +658,91 @@ paths: $ref: '#/components/responses/Unauthorized' 404: $ref: '#/components/responses/NotFound' + + /callbackChannel: + post: + tags: + - Callback + summary: Generic callback hook + operationId: postCallback + parameters: + - in: query + name: subscribe + schema: + type: boolean + required: false + - in: query + name: uri + schema: + type: string + format: uri + - in: query + name: key + schema: + type: string + - in: query + name: topics + schema: + type: string + - in: query + name: id + schema: + type: string + - in: query + name: topic + schema: + type: string + requestBody: + description: A generic JSONDocument, may be empty too {} + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/AnyPayload' + + responses: + 200: + $ref: '#/components/responses/Success' + 403: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + + /securityProfiles: + get: + tags: + - Security + summary: Retrieve the list of security profiles for a specific service type + operationId: getSecurituProfiles + parameters: + - in: query + description: Pagination start (starts at 1. If not specified, 1 is assumed) + name: offset + schema: + type: integer + required: false + - in: query + description: Maximum number of entries to return (if absent, no limit is assumed) + name: limit + schema: + type: integer + required: false + - in: query + description: Filter the results + name: filter + schema: + type: string + required: false + responses: + 200: + $ref: '#/components/schemas/SecurityProfileList' + 403: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + +######################################################################################### +## +## These are endpoints that all services in the uCentral stack must provide +## +######################################################################################### diff --git a/src/AuthService.cpp b/src/AuthService.cpp index c8cba67..e60e901 100644 --- a/src/AuthService.cpp +++ b/src/AuthService.cpp @@ -40,11 +40,6 @@ namespace uCentral { return 1; // some compilers complain... } - AuthService::AuthService() noexcept: SubSystemServer("Authentication", "AUTH-SVR", "authentication") - { - std::string E{"SHA512"}; - } - int AuthService::Start() { Signer_.setRSAKey(Daemon()->Key()); Signer_.addAllAlgorithms(); @@ -193,18 +188,17 @@ namespace uCentral { if(Mechanism_=="internal") { - if(((UserName == DefaultUserName_) && (Password == DefaultPassword_)) || !Secure_) + if(((UserName == DefaultUserName_) && (DefaultPassword_== ComputePasswordHash(UserName,Password))) || !Secure_) { - ACL.PortalLogin_ = ACL.Read_ = ACL.ReadWrite_ = ACL.ReadWriteCreate_ = ACL.Delete_ = true; + ACL.PortalLogin_ = ACL.Read_ = ACL.ReadWrite_ = ACL.ReadWriteCreate_ = ACL.Delete_ = true; CreateToken(UserName, ResultToken, ACL); return true; } } else if (Mechanism_=="db") { - SHA2_.update(Password + UserName); - auto EncryptedPassword = uCentral::Utils::ToHex(SHA2_.digest()); + auto PasswordHash = ComputePasswordHash(UserName, Password); std::string TUser{UserName}; - if(Storage()->GetIdentity(TUser,EncryptedPassword,USERNAME,ACL)) { + if(Storage()->GetIdentity(TUser,PasswordHash,USERNAME,ACL)) { CreateToken(UserName, ResultToken, ACL); return true; } @@ -212,4 +206,10 @@ namespace uCentral { return false; } + std::string AuthService::ComputePasswordHash(const std::string &UserName, const std::string &Password) { + std::string UName = Poco::trim(Poco::toLower(UserName)); + SHA2_.update(Password + UName); + return uCentral::Utils::ToHex(SHA2_.digest()); + } + } // end of namespace diff --git a/src/AuthService.h b/src/AuthService.h index 28b2deb..782192c 100644 --- a/src/AuthService.h +++ b/src/AuthService.h @@ -24,6 +24,7 @@ namespace uCentral{ class AuthService : public SubSystemServer { public: + typedef std::map WebTokenMap; enum ACCESS_TYPE { USERNAME, SERVER, @@ -48,18 +49,23 @@ namespace uCentral{ void Logout(const std::string &token); [[nodiscard]] std::string GenerateToken(const std::string & UserName, ACCESS_TYPE Type, int NumberOfDays); [[nodiscard]] bool ValidateToken(const std::string & Token, std::string & SessionToken, struct uCentral::Objects::WebToken & UserInfo ); - + [[nodiscard]] std::string ComputePasswordHash(const std::string &UserName, const std::string &Password); + [[nodiscard]] bool UpdatePassword(const std::string &Admin, const std::string &UserName, const std::string & OldPassword, const std::string &NewPassword); + [[nodiscard]] std::string ResetPassword(const std::string &Admin, const std::string &UserName); private: static AuthService *instance_; - std::map Tokens_; + WebTokenMap Tokens_; bool Secure_ = false ; std::string DefaultUserName_; std::string DefaultPassword_; std::string Mechanism_; - bool AutoProvisioning_ = false ; Poco::JWT::Signer Signer_; Poco::SHA2Engine SHA2_; - AuthService() noexcept; + + AuthService() noexcept: + SubSystemServer("Authentication", "AUTH-SVR", "authentication") + { + } }; inline AuthService * AuthService() { return AuthService::instance(); } diff --git a/src/Daemon.cpp b/src/Daemon.cpp index a090878..5206428 100644 --- a/src/Daemon.cpp +++ b/src/Daemon.cpp @@ -14,319 +14,46 @@ #include #include "Poco/Util/Application.h" -#include "Poco/Util/ServerApplication.h" #include "Poco/Util/Option.h" -#include "Poco/Util/OptionSet.h" -#include "Poco/Util/HelpFormatter.h" #include "Poco/Environment.h" -#include "Poco/Path.h" -#include "Poco/File.h" -#include "Poco/Net/SSLManager.h" -#include "Utils.h" +#include "Daemon.h" #include "ALBHealthCheckServer.h" #include "KafkaManager.h" #include "StorageService.h" #include "RESTAPI_server.h" +#include "SMTPMailerService.h" -#include "Daemon.h" namespace uCentral { class Daemon *Daemon::instance_ = nullptr; - void MyErrorHandler::exception(const Poco::Exception & E) { - Poco::Thread * CurrentThread = Poco::Thread::current(); - Daemon()->logger().log(E); - Daemon()->logger().error(Poco::format("Exception occurred in %s",CurrentThread->getName())); - } - - void MyErrorHandler::exception(const std::exception & E) { - Poco::Thread * CurrentThread = Poco::Thread::current(); - Daemon()->logger().warning(Poco::format("std::exception on %s",CurrentThread->getName())); - } - - void MyErrorHandler::exception() { - Poco::Thread * CurrentThread = Poco::Thread::current(); - Daemon()->logger().warning(Poco::format("exception on %s",CurrentThread->getName())); - } - - void Daemon::Exit(int Reason) { - std::exit(Reason); - } - - void Daemon::initialize(Application &self) { - - Poco::Net::initializeSSL(); - - SubSystems_ = Types::SubSystemVec{ - Storage(), - RESTAPI_Server(), - KafkaManager(), - ALBHealthCheckServer() - }; - - std::string Location = Poco::Environment::get(uCentral::DAEMON_CONFIG_ENV_VAR,"."); - Poco::Path ConfigFile; - - ConfigFile = ConfigFileName_.empty() ? Location + "/" + uCentral::DAEMON_PROPERTIES_FILENAME : ConfigFileName_; - - if(!ConfigFile.isFile()) - { - std::cerr << uCentral::DAEMON_APP_NAME << ": Configuration " << ConfigFile.toString() << " does not seem to exist. Please set " - << uCentral::DAEMON_ROOT_ENV_VAR << " env variable the path of the " - << uCentral::DAEMON_PROPERTIES_FILENAME << " file." << std::endl; - std::exit(Poco::Util::Application::EXIT_CONFIG); + class Daemon *Daemon::instance() { + if (instance_ == nullptr) { + instance_ = new Daemon(vDAEMON_PROPERTIES_FILENAME, + vDAEMON_ROOT_ENV_VAR, + vDAEMON_CONFIG_ENV_VAR, + vDAEMON_APP_NAME, + Types::SubSystemVec{ + Storage(), + RESTAPI_Server(), + KafkaManager(), + SMTPMailerService(), + ALBHealthCheckServer() + }); } - - static const char * LogFilePathKey = "logging.channels.c2.path"; - - loadConfiguration(ConfigFile.toString()); - - if(LogDir_.empty()) { - std::string OriginalLogFileValue = ConfigPath(LogFilePathKey); - config().setString(LogFilePathKey, OriginalLogFileValue); - } else { - config().setString(LogFilePathKey, LogDir_); - } - Poco::File DataDir(ConfigPath("ucentral.system.data")); - DataDir_ = DataDir.path(); - if(!DataDir.exists()) { - try { - DataDir.createDirectory(); - } catch (const Poco::Exception &E) { - logger().log(E); - } - } - std::string KeyFile = ConfigPath("ucentral.service.key"); - AppKey_ = Poco::SharedPtr(new Poco::Crypto::RSAKey("", KeyFile, "")); - ID_ = ConfigGetInt("ucentral.system.id",1); - if(!DebugMode_) - DebugMode_ = ConfigGetBool("ucentral.system.debug",false); - - InitializeSubSystemServers(); - logger().information("Starting..."); - ServerApplication::initialize(self); + return instance_; } - void Daemon::uninitialize() { - // add your own uninitialization code here - ServerApplication::uninitialize(); + void Daemon::initialize(Poco::Util::Application &self) { + MicroService::initialize(*this); } - void Daemon::reinitialize(Poco::Util::Application &self) { - ServerApplication::reinitialize(self); - // add your own reinitialization code here - } - - void Daemon::defineOptions(Poco::Util::OptionSet &options) { - ServerApplication::defineOptions(options); - - options.addOption( - Poco::Util::Option("help", "", "display help information on command line arguments") - .required(false) - .repeatable(false) - .callback(Poco::Util::OptionCallback(this, &Daemon::handleHelp))); - - options.addOption( - Poco::Util::Option("file", "", "specify the configuration file") - .required(false) - .repeatable(false) - .argument("file") - .callback(Poco::Util::OptionCallback(this, &Daemon::handleConfig))); - - options.addOption( - Poco::Util::Option("debug", "", "to run in debug, set to true") - .required(false) - .repeatable(false) - .callback(Poco::Util::OptionCallback(this, &Daemon::handleDebug))); - - options.addOption( - Poco::Util::Option("logs", "", "specify the log directory and file (i.e. dir/file.log)") - .required(false) - .repeatable(false) - .argument("dir") - .callback(Poco::Util::OptionCallback(this, &Daemon::handleLogs))); - - options.addOption( - Poco::Util::Option("version", "", "get the version and quit.") - .required(false) - .repeatable(false) - .callback(Poco::Util::OptionCallback(this, &Daemon::handleVersion))); - - } - - std::string Daemon::Version() { - std::string V = APP_VERSION; - std::string B = BUILD_NUMBER; - return V + "(" + B + ")"; - } - - void Daemon::handleHelp(const std::string &name, const std::string &value) { - HelpRequested_ = true; - displayHelp(); - stopOptionsProcessing(); - } - - void Daemon::handleVersion(const std::string &name, const std::string &value) { - HelpRequested_ = true; - std::cout << Version() << std::endl; - stopOptionsProcessing(); - } - - void Daemon::handleDebug(const std::string &name, const std::string &value) { - if(value == "true") - DebugMode_ = true ; - } - - void Daemon::handleLogs(const std::string &name, const std::string &value) { - LogDir_ = value; - } - - void Daemon::handleConfig(const std::string &name, const std::string &value) { - ConfigFileName_ = value; - } - - void Daemon::displayHelp() { - Poco::Util::HelpFormatter helpFormatter(options()); - helpFormatter.setCommand(commandName()); - helpFormatter.setUsage("OPTIONS"); - helpFormatter.setHeader("A " + std::string(uCentral::DAEMON_APP_NAME) + " implementation for TIP."); - helpFormatter.format(std::cout); - } - - void Daemon::InitializeSubSystemServers() { - for(auto i:SubSystems_) - addSubsystem(i); - } - - void Daemon::StartSubSystemServers() { - for(auto i:SubSystems_) - i->Start(); - } - - void Daemon::StopSubSystemServers() { - for(auto i=SubSystems_.rbegin(); i!=SubSystems_.rend(); ++i) - (*i)->Stop(); - } - - std::string Daemon::CreateUUID() { - return UUIDGenerator_.create().toString(); - } - - bool Daemon::SetSubsystemLogLevel(const std::string &SubSystem, const std::string &Level) { - try { - auto P = Poco::Logger::parseLevel(Level); - auto Sub = Poco::toLower(SubSystem); - - if (Sub == "all") { - for (auto i : SubSystems_) { - i->Logger().setLevel(P); - } - return true; - } else { - std::cout << "Sub:" << SubSystem << " Level:" << Level << std::endl; - for (auto i : SubSystems_) { - if (Sub == Poco::toLower(i->Name())) { - i->Logger().setLevel(P); - return true; - } - } - } - } catch (const Poco::Exception & E) { - std::cout << "Exception" << std::endl; - } - return false; - } - - Types::StringVec Daemon::GetSubSystems() const { - Types::StringVec Result; - for(auto i:SubSystems_) - Result.push_back(i->Name()); - return Result; - } - - Types::StringPairVec Daemon::GetLogLevels() const { - Types::StringPairVec Result; - - for(auto &i:SubSystems_) { - auto P = std::make_pair( i->Name(), Utils::LogLevelToString(i->GetLoggingLevel())); - Result.push_back(P); - } - return Result; - } - - const Types::StringVec & Daemon::GetLogLevelNames() const { - static Types::StringVec LevelNames{"none", "fatal", "critical", "error", "warning", "notice", "information", "debug", "trace" }; - return LevelNames; - } - - uint64_t Daemon::ConfigGetInt(const std::string &Key,uint64_t Default) { - return (uint64_t) config().getInt64(Key,Default); - } - - uint64_t Daemon::ConfigGetInt(const std::string &Key) { - return config().getInt(Key); - } - - uint64_t Daemon::ConfigGetBool(const std::string &Key,bool Default) { - return config().getBool(Key,Default); - } - - uint64_t Daemon::ConfigGetBool(const std::string &Key) { - return config().getBool(Key); - } - - std::string Daemon::ConfigGetString(const std::string &Key,const std::string & Default) { - return config().getString(Key, Default); - } - - std::string Daemon::ConfigGetString(const std::string &Key) { - return config().getString(Key); - } - - std::string Daemon::ConfigPath(const std::string &Key,const std::string & Default) { - std::string R = config().getString(Key, Default); - return Poco::Path::expand(R); - } - - std::string Daemon::ConfigPath(const std::string &Key) { - std::string R = config().getString(Key); - return Poco::Path::expand(R); - } - - int Daemon::main(const ArgVec &args) { - - Poco::ErrorHandler::set(&AppErrorHandler_); - - if (!HelpRequested_) { - Poco::Logger &logger = Poco::Logger::get(uCentral::DAEMON_APP_NAME ); - logger.notice(Poco::format("Starting %s version %s.",std::string(uCentral::DAEMON_APP_NAME), Version())); - - if(Poco::Net::Socket::supportsIPv6()) - logger.information("System supports IPv6."); - else - logger.information("System does NOT support IPv6."); - - if (config().getBool("application.runAsDaemon", false)) - { - logger.information("Starting as a daemon."); - } - - StartSubSystemServers(); - instance()->waitForTerminationRequest(); - StopSubSystemServers(); - - logger.notice(Poco::format("Stopped %s...",std::string(uCentral::DAEMON_APP_NAME))); - } - - return Application::EXIT_OK; - } } int main(int argc, char **argv) { try { - auto App = uCentral::Daemon::instance(); auto ExitCode = App->run(argc, argv); delete App; @@ -339,4 +66,5 @@ int main(int argc, char **argv) { } } + // end of namespace diff --git a/src/Daemon.h b/src/Daemon.h index 9b8bcd6..3e12091 100644 --- a/src/Daemon.h +++ b/src/Daemon.h @@ -17,85 +17,36 @@ #include "Poco/UUIDGenerator.h" #include "Poco/ErrorHandler.h" #include "Poco/Crypto/RSAKey.h" +#include "Poco/Crypto/CipherFactory.h" +#include "Poco/Crypto/Cipher.h" + -#include "SubSystemServer.h" #include "uCentralTypes.h" - -using Poco::Util::ServerApplication; +#include "MicroService.h" namespace uCentral { - static const char * DAEMON_PROPERTIES_FILENAME = "ucentralsec.properties"; - static const char * DAEMON_ROOT_ENV_VAR = "UCENTRALSEC_ROOT"; - static const char * DAEMON_CONFIG_ENV_VAR = "UCENTRALSEC_CONFIG"; - static const char * DAEMON_APP_NAME = "uCentralSec"; - - class MyErrorHandler : public Poco::ErrorHandler { + static const char * vDAEMON_PROPERTIES_FILENAME = "ucentralsec.properties"; + static const char * vDAEMON_ROOT_ENV_VAR = "UCENTRALSEC_ROOT"; + static const char * vDAEMON_CONFIG_ENV_VAR = "UCENTRALSEC_CONFIG"; + static const char * vDAEMON_APP_NAME = "uCentralSec"; + class Daemon : public MicroService { public: - void exception(const Poco::Exception & E) override; - void exception(const std::exception & E) override; - void exception() override; - private: - - }; - - class Daemon : public ServerApplication { - - public: - int main(const ArgVec &args) override; - void initialize(Application &self) override; - void uninitialize() override; - void reinitialize(Application &self) override; - void defineOptions(Poco::Util::OptionSet &options) override; - void handleHelp(const std::string &name, const std::string &value); - void handleVersion(const std::string &name, const std::string &value); - void handleDebug(const std::string &name, const std::string &value); - void handleLogs(const std::string &name, const std::string &value); - void handleConfig(const std::string &name, const std::string &value); - void displayHelp(); - - static Daemon *instance() { - if (instance_ == nullptr) { - instance_ = new Daemon; - } - return instance_; - } - - void InitializeSubSystemServers(); - void StartSubSystemServers(); - void StopSubSystemServers(); - void Exit(int Reason); - bool SetSubsystemLogLevel(const std::string & SubSystem, const std::string & Level); - [[nodiscard]] static std::string Version(); - [[nodiscard]] const Poco::SharedPtr & Key() { return AppKey_; } - [[nodiscard]] inline const std::string & DataDir() { return DataDir_; } - [[nodiscard]] std::string CreateUUID(); - [[nodiscard]] bool Debug() const { return DebugMode_; } - [[nodiscard]] uint64_t ID() const { return ID_; } - [[nodiscard]] Types::StringVec GetSubSystems() const; - [[nodiscard]] Types::StringPairVec GetLogLevels() const; - [[nodiscard]] const Types::StringVec & GetLogLevelNames() const; - [[nodiscard]] std::string ConfigGetString(const std::string &Key,const std::string & Default); - [[nodiscard]] std::string ConfigGetString(const std::string &Key); - [[nodiscard]] std::string ConfigPath(const std::string &Key,const std::string & Default); - [[nodiscard]] std::string ConfigPath(const std::string &Key); - [[nodiscard]] uint64_t ConfigGetInt(const std::string &Key,uint64_t Default); - [[nodiscard]] uint64_t ConfigGetInt(const std::string &Key); - [[nodiscard]] uint64_t ConfigGetBool(const std::string &Key,bool Default); - [[nodiscard]] uint64_t ConfigGetBool(const std::string &Key); + explicit Daemon(std::string PropFile, + std::string RootEnv, + std::string ConfigEnv, + std::string AppName, + Types::SubSystemVec SubSystems) : + MicroService( PropFile, RootEnv, ConfigEnv, AppName, SubSystems) {}; + bool AutoProvisioning() const { return AutoProvisioning_ ; } + [[nodiscard]] std::string IdentifyDevice(const std::string & Compatible) const; + void initialize(Poco::Util::Application &self); + static Daemon *instance(); private: static Daemon *instance_; - bool HelpRequested_ = false; - std::string LogDir_; - std::string ConfigFileName_; - Poco::UUIDGenerator UUIDGenerator_; - MyErrorHandler AppErrorHandler_; - uint64_t ID_ = 1; - Poco::SharedPtr AppKey_ = nullptr; - bool DebugMode_ = false; - std::string DataDir_; - Types::SubSystemVec SubSystems_; + bool AutoProvisioning_ = false; + Types::StringMapStringSet DeviceTypeIdentifications_; }; inline Daemon * Daemon() { return Daemon::instance(); } diff --git a/src/MicroService.cpp b/src/MicroService.cpp new file mode 100644 index 0000000..6ffc983 --- /dev/null +++ b/src/MicroService.cpp @@ -0,0 +1,319 @@ +// +// Created by stephane bourque on 2021-06-22. +// +#include +#include + +#include "Poco/Util/Application.h" +#include "Poco/Util/ServerApplication.h" +#include "Poco/Util/Option.h" +#include "Poco/Util/OptionSet.h" +#include "Poco/Util/HelpFormatter.h" +#include "Poco/Environment.h" +#include "Poco/Net/HTTPSStreamFactory.h" +#include "Poco/Net/HTTPStreamFactory.h" +#include "Poco/Net/FTPSStreamFactory.h" +#include "Poco/Net/FTPStreamFactory.h" +#include "Poco/Path.h" +#include "Poco/File.h" +#include "Poco/String.h" + +#include "MicroService.h" +#include "Utils.h" + +namespace uCentral { + + void MyErrorHandler::exception(const Poco::Exception & E) { + Poco::Thread * CurrentThread = Poco::Thread::current(); + App_.logger().log(E); + App_.logger().error(Poco::format("Exception occurred in %s",CurrentThread->getName())); + } + + void MyErrorHandler::exception(const std::exception & E) { + Poco::Thread * CurrentThread = Poco::Thread::current(); + App_.logger().warning(Poco::format("std::exception on %s",CurrentThread->getName())); + } + + void MyErrorHandler::exception() { + Poco::Thread * CurrentThread = Poco::Thread::current(); + App_.logger().warning(Poco::format("exception on %s",CurrentThread->getName())); + } + + void MicroService::Exit(int Reason) { + std::exit(Reason); + } + + void MicroService::initialize(Poco::Util::Application &self) { + Poco::Net::initializeSSL(); + Poco::Net::HTTPStreamFactory::registerFactory(); + Poco::Net::HTTPSStreamFactory::registerFactory(); + Poco::Net::FTPStreamFactory::registerFactory(); + Poco::Net::FTPSStreamFactory::registerFactory(); + std::string Location = Poco::Environment::get(DAEMON_CONFIG_ENV_VAR,"."); + Poco::Path ConfigFile; + + ConfigFile = ConfigFileName_.empty() ? Location + "/" + DAEMON_PROPERTIES_FILENAME : ConfigFileName_; + + if(!ConfigFile.isFile()) + { + std::cerr << DAEMON_APP_NAME << ": Configuration " + << ConfigFile.toString() << " does not seem to exist. Please set " + DAEMON_CONFIG_ENV_VAR + + " env variable the path of the " + DAEMON_PROPERTIES_FILENAME + " file." << std::endl; + std::exit(Poco::Util::Application::EXIT_CONFIG); + } + + static const char * LogFilePathKey = "logging.channels.c2.path"; + + loadConfiguration(ConfigFile.toString()); + + if(LogDir_.empty()) { + std::string OriginalLogFileValue = ConfigPath(LogFilePathKey); + config().setString(LogFilePathKey, OriginalLogFileValue); + } else { + config().setString(LogFilePathKey, LogDir_); + } + Poco::File DataDir(ConfigPath("ucentral.system.data")); + DataDir_ = DataDir.path(); + if(!DataDir.exists()) { + try { + DataDir.createDirectory(); + } catch (const Poco::Exception &E) { + logger().log(E); + } + } + std::string KeyFile = ConfigPath("ucentral.service.key"); + AppKey_ = Poco::SharedPtr(new Poco::Crypto::RSAKey("", KeyFile, "")); + Cipher_ = CipherFactory_.createCipher(*AppKey_); + ID_ = Utils::GetSystemId(); + if(!DebugMode_) + DebugMode_ = ConfigGetBool("ucentral.system.debug",false); + + InitializeSubSystemServers(); + ServerApplication::initialize(self); + } + + void MicroService::uninitialize() { + // add your own uninitialization code here + ServerApplication::uninitialize(); + } + + void MicroService::reinitialize(Poco::Util::Application &self) { + ServerApplication::reinitialize(self); + // add your own reinitialization code here + } + + void MicroService::defineOptions(Poco::Util::OptionSet &options) { + ServerApplication::defineOptions(options); + + options.addOption( + Poco::Util::Option("help", "", "display help information on command line arguments") + .required(false) + .repeatable(false) + .callback(Poco::Util::OptionCallback(this, &MicroService::handleHelp))); + + options.addOption( + Poco::Util::Option("file", "", "specify the configuration file") + .required(false) + .repeatable(false) + .argument("file") + .callback(Poco::Util::OptionCallback(this, &MicroService::handleConfig))); + + options.addOption( + Poco::Util::Option("debug", "", "to run in debug, set to true") + .required(false) + .repeatable(false) + .callback(Poco::Util::OptionCallback(this, &MicroService::handleDebug))); + + options.addOption( + Poco::Util::Option("logs", "", "specify the log directory and file (i.e. dir/file.log)") + .required(false) + .repeatable(false) + .argument("dir") + .callback(Poco::Util::OptionCallback(this, &MicroService::handleLogs))); + + options.addOption( + Poco::Util::Option("version", "", "get the version and quit.") + .required(false) + .repeatable(false) + .callback(Poco::Util::OptionCallback(this, &MicroService::handleVersion))); + + } + + std::string MicroService::Version() { + std::string V = APP_VERSION; + std::string B = BUILD_NUMBER; + return V + "(" + B + ")"; + } + + void MicroService::handleHelp(const std::string &name, const std::string &value) { + HelpRequested_ = true; + displayHelp(); + stopOptionsProcessing(); + } + + void MicroService::handleVersion(const std::string &name, const std::string &value) { + HelpRequested_ = true; + std::cout << Version() << std::endl; + stopOptionsProcessing(); + } + + void MicroService::handleDebug(const std::string &name, const std::string &value) { + if(value == "true") + DebugMode_ = true ; + } + + void MicroService::handleLogs(const std::string &name, const std::string &value) { + LogDir_ = value; + } + + void MicroService::handleConfig(const std::string &name, const std::string &value) { + ConfigFileName_ = value; + } + + void MicroService::displayHelp() { + Poco::Util::HelpFormatter helpFormatter(options()); + helpFormatter.setCommand(commandName()); + helpFormatter.setUsage("OPTIONS"); + helpFormatter.setHeader("A " + DAEMON_APP_NAME + " implementation for TIP."); + helpFormatter.format(std::cout); + } + + void MicroService::InitializeSubSystemServers() { + for(auto i:SubSystems_) + addSubsystem(i); + } + + void MicroService::StartSubSystemServers() { + for(auto i:SubSystems_) + i->Start(); + } + + void MicroService::StopSubSystemServers() { + for(auto i=SubSystems_.rbegin(); i!=SubSystems_.rend(); ++i) + (*i)->Stop(); + } + + std::string MicroService::CreateUUID() { + return UUIDGenerator_.create().toString(); + } + + bool MicroService::SetSubsystemLogLevel(const std::string &SubSystem, const std::string &Level) { + try { + auto P = Poco::Logger::parseLevel(Level); + auto Sub = Poco::toLower(SubSystem); + + if (Sub == "all") { + for (auto i : SubSystems_) { + i->Logger().setLevel(P); + } + return true; + } else { + std::cout << "Sub:" << SubSystem << " Level:" << Level << std::endl; + for (auto i : SubSystems_) { + if (Sub == Poco::toLower(i->Name())) { + i->Logger().setLevel(P); + return true; + } + } + } + } catch (const Poco::Exception & E) { + std::cout << "Exception" << std::endl; + } + return false; + } + + Types::StringVec MicroService::GetSubSystems() const { + Types::StringVec Result; + for(auto i:SubSystems_) + Result.push_back(i->Name()); + return Result; + } + + Types::StringPairVec MicroService::GetLogLevels() const { + Types::StringPairVec Result; + + for(auto &i:SubSystems_) { + auto P = std::make_pair( i->Name(), Utils::LogLevelToString(i->GetLoggingLevel())); + Result.push_back(P); + } + return Result; + } + + const Types::StringVec & MicroService::GetLogLevelNames() const { + static Types::StringVec LevelNames{"none", "fatal", "critical", "error", "warning", "notice", "information", "debug", "trace" }; + return LevelNames; + } + + uint64_t MicroService::ConfigGetInt(const std::string &Key,uint64_t Default) { + return (uint64_t) config().getInt64(Key,Default); + } + + uint64_t MicroService::ConfigGetInt(const std::string &Key) { + return config().getInt(Key); + } + + uint64_t MicroService::ConfigGetBool(const std::string &Key,bool Default) { + return config().getBool(Key,Default); + } + + uint64_t MicroService::ConfigGetBool(const std::string &Key) { + return config().getBool(Key); + } + + std::string MicroService::ConfigGetString(const std::string &Key,const std::string & Default) { + return config().getString(Key, Default); + } + + std::string MicroService::ConfigGetString(const std::string &Key) { + return config().getString(Key); + } + + std::string MicroService::ConfigPath(const std::string &Key,const std::string & Default) { + std::string R = config().getString(Key, Default); + return Poco::Path::expand(R); + } + + std::string MicroService::ConfigPath(const std::string &Key) { + std::string R = config().getString(Key); + return Poco::Path::expand(R); + } + + std::string MicroService::Encrypt(const std::string &S) { + return Cipher_->encryptString(S, Poco::Crypto::Cipher::Cipher::ENC_BASE64);; + } + + std::string MicroService::Decrypt(const std::string &S) { + return Cipher_->decryptString(S, Poco::Crypto::Cipher::Cipher::ENC_BASE64);; + } + + int MicroService::main(const ArgVec &args) { + + MyErrorHandler ErrorHandler(*this); + Poco::ErrorHandler::set(&ErrorHandler); + + if (!HelpRequested_) { + Poco::Logger &logger = Poco::Logger::get(DAEMON_APP_NAME); + logger.notice(Poco::format("Starting %s version %s.",DAEMON_APP_NAME, Version())); + + if(Poco::Net::Socket::supportsIPv6()) + logger.information("System supports IPv6."); + else + logger.information("System does NOT support IPv6."); + + if (config().getBool("application.runAsDaemon", false)) { + logger.information("Starting as a daemon."); + } + logger.information(Poco::format("System ID set to %Lu",ID_)); + StartSubSystemServers(); + waitForTerminationRequest(); + StopSubSystemServers(); + + logger.notice(Poco::format("Stopped %s...",DAEMON_APP_NAME)); + } + + return Application::EXIT_OK; + } + + + +} \ No newline at end of file diff --git a/src/MicroService.h b/src/MicroService.h new file mode 100644 index 0000000..92c2081 --- /dev/null +++ b/src/MicroService.h @@ -0,0 +1,109 @@ +// +// Created by stephane bourque on 2021-06-22. +// + +#ifndef UCENTRALGW_MICROSERVICE_H +#define UCENTRALGW_MICROSERVICE_H + +#include +#include +#include +#include +#include + +#include "Poco/Util/Application.h" +#include "Poco/Util/ServerApplication.h" +#include "Poco/Util/Option.h" +#include "Poco/Util/OptionSet.h" +#include "Poco/UUIDGenerator.h" +#include "Poco/ErrorHandler.h" +#include "Poco/Crypto/RSAKey.h" +#include "Poco/Crypto/CipherFactory.h" +#include "Poco/Crypto/Cipher.h" + +#include "uCentralTypes.h" +#include "SubSystemServer.h" + +namespace uCentral { + + class MyErrorHandler : public Poco::ErrorHandler { + public: + explicit MyErrorHandler(Poco::Util::Application &App) : App_(App) {} + void exception(const Poco::Exception & E) override; + void exception(const std::exception & E) override; + void exception() override; + private: + Poco::Util::Application &App_; + }; + + class MicroService : public Poco::Util::ServerApplication { + public: + explicit MicroService( std::string PropFile, + std::string RootEnv, + std::string ConfigVar, + std::string AppName, + Types::SubSystemVec Subsystems) : + DAEMON_PROPERTIES_FILENAME(std::move(PropFile)), + DAEMON_ROOT_ENV_VAR(std::move(RootEnv)), + DAEMON_CONFIG_ENV_VAR(std::move(ConfigVar)), + DAEMON_APP_NAME(std::move(AppName)), + SubSystems_(Subsystems) {} + + int main(const ArgVec &args) override; + void initialize(Application &self) override; + void uninitialize() override; + void reinitialize(Application &self) override; + void defineOptions(Poco::Util::OptionSet &options) override; + void handleHelp(const std::string &name, const std::string &value); + void handleVersion(const std::string &name, const std::string &value); + void handleDebug(const std::string &name, const std::string &value); + void handleLogs(const std::string &name, const std::string &value); + void handleConfig(const std::string &name, const std::string &value); + void displayHelp(); + + void InitializeSubSystemServers(); + void StartSubSystemServers(); + void StopSubSystemServers(); + void Exit(int Reason); + bool SetSubsystemLogLevel(const std::string & SubSystem, const std::string & Level); + [[nodiscard]] static std::string Version(); + [[nodiscard]] const Poco::SharedPtr & Key() { return AppKey_; } + [[nodiscard]] inline const std::string & DataDir() { return DataDir_; } + [[nodiscard]] std::string CreateUUID(); + [[nodiscard]] bool Debug() const { return DebugMode_; } + [[nodiscard]] uint64_t ID() const { return ID_; } + [[nodiscard]] Types::StringVec GetSubSystems() const; + [[nodiscard]] Types::StringPairVec GetLogLevels() const; + [[nodiscard]] const Types::StringVec & GetLogLevelNames() const; + [[nodiscard]] std::string ConfigGetString(const std::string &Key,const std::string & Default); + [[nodiscard]] std::string ConfigGetString(const std::string &Key); + [[nodiscard]] std::string ConfigPath(const std::string &Key,const std::string & Default); + [[nodiscard]] std::string ConfigPath(const std::string &Key); + [[nodiscard]] uint64_t ConfigGetInt(const std::string &Key,uint64_t Default); + [[nodiscard]] uint64_t ConfigGetInt(const std::string &Key); + [[nodiscard]] uint64_t ConfigGetBool(const std::string &Key,bool Default); + [[nodiscard]] uint64_t ConfigGetBool(const std::string &Key); + [[nodiscard]] std::string Encrypt(const std::string &S); + [[nodiscard]] std::string Decrypt(const std::string &S); + + private: + bool HelpRequested_ = false; + std::string LogDir_; + std::string ConfigFileName_; + Poco::UUIDGenerator UUIDGenerator_; + uint64_t ID_ = 1; + Poco::SharedPtr AppKey_ = nullptr; + bool DebugMode_ = false; + std::string DataDir_; + Types::SubSystemVec SubSystems_; + Poco::Crypto::CipherFactory & CipherFactory_ = Poco::Crypto::CipherFactory::defaultFactory(); + Poco::Crypto::Cipher * Cipher_ = nullptr; + + std::string DAEMON_PROPERTIES_FILENAME; + std::string DAEMON_ROOT_ENV_VAR; + std::string DAEMON_CONFIG_ENV_VAR; + std::string DAEMON_APP_NAME; + }; +} + +#endif // UCENTRALGW_MICROSERVICE_H diff --git a/src/RESTAPI_action_links.cpp b/src/RESTAPI_action_links.cpp new file mode 100644 index 0000000..5fadbdd --- /dev/null +++ b/src/RESTAPI_action_links.cpp @@ -0,0 +1,11 @@ +// +// Created by stephane bourque on 2021-06-22. +// + +#include "RESTAPI_action_links.h" + +namespace uCentral { + void RESTAPI_action_links:: handleRequest(Poco::Net::HTTPServerRequest &Request, + Poco::Net::HTTPServerResponse &Response) { + } +} diff --git a/src/RESTAPI_action_links.h b/src/RESTAPI_action_links.h new file mode 100644 index 0000000..c434991 --- /dev/null +++ b/src/RESTAPI_action_links.h @@ -0,0 +1,25 @@ +// +// Created by stephane bourque on 2021-06-22. +// + +#ifndef UCENTRALSEC_RESTAPI_ACTION_LINKS_H +#define UCENTRALSEC_RESTAPI_ACTION_LINKS_H + + +#include "RESTAPI_handler.h" + +namespace uCentral { + class RESTAPI_action_links : public RESTAPIHandler { + public: + RESTAPI_action_links(const RESTAPIHandler::BindingMap &bindings, Poco::Logger &L) + : RESTAPIHandler(bindings, L, + std::vector{ + Poco::Net::HTTPRequest::HTTP_GET, + Poco::Net::HTTPRequest::HTTP_POST, + Poco::Net::HTTPRequest::HTTP_OPTIONS}) {} + void handleRequest(Poco::Net::HTTPServerRequest &Request, + Poco::Net::HTTPServerResponse &Response) override; + }; +} + +#endif //UCENTRALSEC_RESTAPI_ACTION_LINKS_H diff --git a/src/RESTAPI_server.cpp b/src/RESTAPI_server.cpp index 76ea59f..d620f91 100644 --- a/src/RESTAPI_server.cpp +++ b/src/RESTAPI_server.cpp @@ -12,6 +12,9 @@ #include "RESTAPI_oauth2Handler.h" #include "RESTAPI_unknownRequestHandler.h" #include "RESTAPI_system_command.h" +#include "RESTAPI_user_handler.h" +#include "RESTAPI_users_handler.h" +#include "RESTAPI_action_links.h" #include "Utils.h" @@ -61,8 +64,14 @@ namespace uCentral { return new RESTAPI_oauth2Handler(bindings, Logger_); } else if (RESTAPIHandler::ParseBindings(Path, "/api/v1/oauth2", bindings)) { return new RESTAPI_oauth2Handler(bindings, Logger_); + } else if (RESTAPIHandler::ParseBindings(Path, "/api/v1/users", bindings)) { + return new RESTAPI_users_handler(bindings, Logger_); + } else if (RESTAPIHandler::ParseBindings(Path, "/api/v1/user", bindings)) { + return new RESTAPI_user_handler(bindings, Logger_); } else if (RESTAPIHandler::ParseBindings(Path, "/api/v1/system", bindings)) { return new RESTAPI_system_command(bindings, Logger_); + } else if (RESTAPIHandler::ParseBindings(Path, "/api/v1/actions", bindings)) { + return new RESTAPI_action_links(bindings, Logger_); } Logger_.error(Poco::format("INVALID-API-ENDPOINT: %s",Path)); diff --git a/src/RESTAPI_user_handler.cpp b/src/RESTAPI_user_handler.cpp new file mode 100644 index 0000000..fc8fae6 --- /dev/null +++ b/src/RESTAPI_user_handler.cpp @@ -0,0 +1,11 @@ +// +// Created by stephane bourque on 2021-06-21. +// + +#include "RESTAPI_user_handler.h" + +namespace uCentral { + void RESTAPI_user_handler::handleRequest(Poco::Net::HTTPServerRequest &Request, Poco::Net::HTTPServerResponse &Response) { + + } +} \ No newline at end of file diff --git a/src/RESTAPI_user_handler.h b/src/RESTAPI_user_handler.h new file mode 100644 index 0000000..3f4f320 --- /dev/null +++ b/src/RESTAPI_user_handler.h @@ -0,0 +1,28 @@ +// +// Created by stephane bourque on 2021-06-21. +// + +#ifndef UCENTRALSEC_RESTAPI_USER_HANDLER_H +#define UCENTRALSEC_RESTAPI_USER_HANDLER_H + +#include "RESTAPI_handler.h" + +namespace uCentral { + class RESTAPI_user_handler : public RESTAPIHandler { + public: + RESTAPI_user_handler(const RESTAPIHandler::BindingMap &bindings, Poco::Logger &L) + : RESTAPIHandler(bindings, L, + std::vector + {Poco::Net::HTTPRequest::HTTP_POST, + Poco::Net::HTTPRequest::HTTP_GET, + Poco::Net::HTTPRequest::HTTP_PUT, + Poco::Net::HTTPRequest::HTTP_DELETE, + Poco::Net::HTTPRequest::HTTP_OPTIONS}) {} + void handleRequest(Poco::Net::HTTPServerRequest &Request, Poco::Net::HTTPServerResponse &Response) override; + private: + + }; +} + + +#endif //UCENTRALSEC_RESTAPI_USER_HANDLER_H diff --git a/src/RESTAPI_users_handler.cpp b/src/RESTAPI_users_handler.cpp new file mode 100644 index 0000000..a94dddc --- /dev/null +++ b/src/RESTAPI_users_handler.cpp @@ -0,0 +1,11 @@ +// +// Created by stephane bourque on 2021-06-21. +// + +#include "RESTAPI_users_handler.h" + +namespace uCentral { + void RESTAPI_users_handler::handleRequest(Poco::Net::HTTPServerRequest &Request, Poco::Net::HTTPServerResponse &Response) { + + } +} \ No newline at end of file diff --git a/src/RESTAPI_users_handler.h b/src/RESTAPI_users_handler.h new file mode 100644 index 0000000..a13683a --- /dev/null +++ b/src/RESTAPI_users_handler.h @@ -0,0 +1,25 @@ +// +// Created by stephane bourque on 2021-06-21. +// + +#ifndef UCENTRALSEC_RESTAPI_USERS_HANDLER_H +#define UCENTRALSEC_RESTAPI_USERS_HANDLER_H + +#include "RESTAPI_handler.h" + +namespace uCentral { + class RESTAPI_users_handler : public RESTAPIHandler { + public: + RESTAPI_users_handler(const RESTAPIHandler::BindingMap &bindings, Poco::Logger &L) + : RESTAPIHandler(bindings, L, + std::vector + {Poco::Net::HTTPRequest::HTTP_GET, + Poco::Net::HTTPRequest::HTTP_OPTIONS}) {} + void handleRequest(Poco::Net::HTTPServerRequest &Request, Poco::Net::HTTPServerResponse &Response) override; + private: + + }; +}; + + +#endif //UCENTRALSEC_RESTAPI_USERS_HANDLER_H diff --git a/src/SMTPMailerService.cpp b/src/SMTPMailerService.cpp new file mode 100644 index 0000000..3a920d4 --- /dev/null +++ b/src/SMTPMailerService.cpp @@ -0,0 +1,94 @@ +// +// Created by stephane bourque on 2021-06-17. +// +#include + +#include "Poco/Net/MailMessage.h" +#include "Poco/Net/MailRecipient.h" +#include "Poco/Net/SMTPClientSession.h" +#include "Poco/Net/SecureSMTPClientSession.h" +#include "Poco/Net/StringPartSource.h" +#include "Poco/Path.h" +#include "Poco/Exception.h" +#include "Poco/Net/SSLManager.h" +#include "Poco/Net/Context.h" +#include "Poco/Net/InvalidCertificateHandler.h" +#include "Poco/Net/AcceptCertificateHandler.h" + +#include "SMTPMailerService.h" +#include "Daemon.h" + +namespace uCentral { + + class SMTPMailerService * SMTPMailerService::instance_ = nullptr; + + int SMTPMailerService::Start() { + MailHost_ = Daemon()->ConfigGetString("mailer.hostname"); + SenderLoginUserName_ = Daemon()->ConfigGetString("mailer.username"); + SenderLoginPassword_ = Daemon()->ConfigGetString("mailer.password"); + LoginMethod_ = Daemon()->ConfigGetString("mailer.loginmethod"); + MailHostPort_ = Daemon()->ConfigGetInt("mailer.port"); + return 0; + } + + void SMTPMailerService::Stop() { + + } + + bool SMTPMailerService::SendMessage(SMTPMailerService::MessageAttributes Attrs) { + return false; + } + + bool SMTPMailerService::SendIt() { + try + { + Poco::SharedPtr ptrHandler = new Poco::Net::AcceptCertificateHandler(false); + + /* + Poco::Net::MailMessage message; + message.setSender(Sender_); + message.addRecipient(MailRecipient(Poco::Net::MailRecipient::PRIMARY_RECIPIENT, recipient)); + message.setSubject("Hello from the POCO C++ Libraries"); + + std::string content; + + content += "Hello "; + content += recipient; + content += ",\r\n\r\n"; + content += "This is a greeting from the POCO C++ Libraries.\r\n\r\n"; + std::string logo(reinterpret_cast(PocoLogo), sizeof(PocoLogo)); + message.addContent(new Poco::Net::StringPartSource(content)); + message.addAttachment("logo", new Poco::Net::StringPartSource(logo, "image/gif")); + + Poco::Net::SecureSMTPClientSession session(MailHost_,MailHostPort_); + + Poco::Net::Context::Params P; + auto ptrContext = new Poco::Net::Context( Poco::Net::Context::CLIENT_USE, "", "", "", + Poco::Net::Context::VERIFY_RELAXED, 9, true, + "ALL:!ADH:!LOW:!EXP:!MD5:@STRENGTH"); + + Poco::Net::SSLManager::instance().initializeClient(nullptr, + ptrHandler, + ptrContext); + + session.login(); + session.startTLS(ptrContext); + session.login(MailHost_, + Poco::Net::SecureSMTPClientSession::AUTH_LOGIN, + SenderLoginUserName_, + SenderLoginPassword_ + ); + session.sendMessage(message); + session.close(); + */ + } + catch (const Poco::Exception& exc) + { + std::cerr << exc.displayText() << std::endl; + return 1; + } + return 0; + + } + +} \ No newline at end of file diff --git a/src/SMTPMailerService.h b/src/SMTPMailerService.h new file mode 100644 index 0000000..de1f5a5 --- /dev/null +++ b/src/SMTPMailerService.h @@ -0,0 +1,60 @@ +// +// Created by stephane bourque on 2021-06-17. +// + +#ifndef UCENTRALSEC_SMTPMAILERSERVICE_H +#define UCENTRALSEC_SMTPMAILERSERVICE_H + +#include "SubSystemServer.h" + +namespace uCentral { + class SMTPMailerService : public SubSystemServer { + public: + enum MESSAGE_ATTRIBUTES { + RECIPIENT_EMAIL, + RECIPIENT_FIRST_NAME, + RECIPIENT_LAST_NAME, + RECIPIENT_INITIALS, + RECIPIENT_FULL_NAME, + RECIPIENT_SALUTATION, + SUBJECT, + SIGNATURE, + TEMPLATE, + LOGO + }; + + typedef std::map MessageAttributes; + + static SMTPMailerService *instance() { + if (instance_ == nullptr) { + instance_ = new SMTPMailerService; + } + return instance_; + } + + int Start() override; + void Stop() override; + bool SendMessage(MessageAttributes Attrs); + bool SendIt(); + + private: + static SMTPMailerService * instance_; + std::string MailHost_; + std::string Sender_; + int MailHostPort_=25; + std::string SenderLoginUserName_; + std::string SenderLoginPassword_; + std::string LoginMethod_ = "login"; + std::string LogoFileName_; + + SMTPMailerService() noexcept: + SubSystemServer("SMTPMailer", "MAILER-SVR", "smtpmailer") + { + std::string E{"SHA512"}; + } + }; + + inline SMTPMailerService * SMTPMailerService() { return SMTPMailerService::instance(); } +} + +#endif //UCENTRALSEC_SMTPMAILERSERVICE_H diff --git a/src/StorageService.h b/src/StorageService.h index 6582c8e..da0dadd 100644 --- a/src/StorageService.h +++ b/src/StorageService.h @@ -36,10 +36,13 @@ namespace uCentral { odbc }; - enum CommandExecutionType { - COMMAND_PENDING, - COMMAND_EXECUTED, - COMMAND_COMPLETED + enum AUTH_ERROR { + SUCCESS, + PASSWORD_CHANGE_REQUIRED, + PASSWORD_DOES_NOT_MATCH, + PASSWORD_ALREADY_USED, + USERNAME_PENDING_VERIFICATION, + PASSWORD_INVALID }; static Storage *instance() { @@ -52,6 +55,10 @@ namespace uCentral { int Start() override; void Stop() override; + int CreateUser(const std::string & Admin, const std::string &UserName, const std::string &Password); + bool DeleteUser(const std::string & Admin, const std::string &UserName); + bool ChangePassword(const std::string & Admin, const std::string &UserName, const std::string &OldPassword, const std::string &NewPassword); + bool IdentityExists(std::string & Identity, AuthService::ACCESS_TYPE Type); bool AddIdentity(std::string & Identity, std::string & Password, AuthService::ACCESS_TYPE Type, uCentral::Objects::AclTemplate & ACL); bool GetIdentity(std::string & Identity, std::string & Password,AuthService::ACCESS_TYPE Type, uCentral::Objects::AclTemplate & ACL); @@ -86,6 +93,8 @@ namespace uCentral { #endif int Create_Tables(); + int Create_UserTable(); + int Create_APIKeyTable(); int Setup_SQLite(); [[nodiscard]] std::string ConvertParams(const std::string &S) const; diff --git a/src/Utils.cpp b/src/Utils.cpp index 17250ee..34a92a9 100644 --- a/src/Utils.cpp +++ b/src/Utils.cpp @@ -6,6 +6,8 @@ // Arilia Wireless Inc. // #include +#include +#include #include "Utils.h" @@ -17,8 +19,10 @@ #include "Poco/StringTokenizer.h" #include "Poco/Logger.h" #include "Poco/Message.h" +#include "Poco/File.h" #include "uCentralProtocol.h" +#include "Daemon.h" namespace uCentral::Utils { @@ -279,4 +283,110 @@ namespace uCentral::Utils { } } + bool SerialNumberMatch(const std::string &S1, const std::string &S2, int Bits) { + auto S1_i = SerialNumberToInt(S1); + auto S2_i = SerialNumberToInt(S2); + return ((S1_i>>Bits)==(S2_i>>Bits)); + } + + uint64_t SerialNumberToInt(const std::string & S) { + uint64_t R=0; + + for(const auto &i:S) + if(i>='0' && i<='9') { + R <<= 4; + R += (i-'0'); + } else if(i>='a' && i<='f') { + R <<= 4; + R += (i-'a') + 10 ; + } else if(i>='A' && i<='F') { + R <<= 4; + R += (i-'A') + 10 ; + } + return R; + } + + uint64_t SerialNumberToOUI(const std::string & S) { + uint64_t Result = 0 ; + int Digits=0; + + for(const auto &i:S) { + if(std::isxdigit(i)) { + if(i>='0' && i<='9') { + Result <<=4; + Result += i-'0'; + } else if(i>='A' && i<='F') { + Result <<=4; + Result += i-'A'+10; + } else if(i>='a' && i<='f') { + Result <<=4; + Result += i-'a'+10; + } + Digits++; + if(Digits==6) + break; + } + } + return Result; + } + + uint64_t GetDefaultMacAsInt64() { + uint64_t Result=0; + auto IFaceList = Poco::Net::NetworkInterface::list(); + + for(const auto &iface:IFaceList) { + if(iface.isRunning() && !iface.isLoopback()) { + auto MAC = iface.macAddress(); + for (auto const &i : MAC) { + Result <<= 8; + Result += (uint8_t)i; + } + if (Result != 0) + break; + } + } + return Result; + } + + void SaveSystemId(uint64_t Id) { + try { + std::ofstream O; + O.open(Daemon()->DataDir() + "/system.id",std::ios::binary | std::ios::trunc); + O << Id; + O.close(); + } catch (...) + { + std::cout << "Could not save system ID" << std::endl; + } + } + + uint64_t InitializeSystemId() { + uint64_t R = ~ std::rand(); + auto S = GetDefaultMacAsInt64() ^ R; + SaveSystemId(S); + return S; + } + + uint64_t GetSystemId() { + uint64_t ID=0; + + // if the system ID file exists, open and read it. + Poco::File SID( Daemon()->DataDir() + "/system.id"); + try { + if (SID.exists()) { + std::ifstream I; + I.open(SID.path()); + I >> ID; + I.close(); + if (ID == 0) + return InitializeSystemId(); + return ID; + } else { + return InitializeSystemId(); + } + } catch (...) { + return InitializeSystemId(); + } + } + } diff --git a/src/Utils.h b/src/Utils.h index f365f33..4679b0c 100644 --- a/src/Utils.h +++ b/src/Utils.h @@ -12,6 +12,8 @@ #include #include +#include "Poco/Net/NetworkInterface.h" + namespace uCentral::Utils { [[nodiscard]] std::vector Split(const std::string &List, char Delimiter=','); @@ -37,5 +39,12 @@ namespace uCentral::Utils { [[nodiscard]] bool ValidSerialNumber(const std::string &Serial); [[nodiscard]] std::string LogLevelToString(int Level); + + [[nodiscard]] bool SerialNumberMatch(const std::string &S1, const std::string &S2, int extrabits=2); + [[nodiscard]] uint64_t SerialNumberToInt(const std::string & S); + [[nodiscard]] uint64_t SerialNumberToOUI(const std::string & S); + + [[nodiscard]] uint64_t GetDefaultMacAsInt64(); + [[nodiscard]] uint64_t GetSystemId(); } #endif // UCENTRALGW_UTILS_H diff --git a/src/storage_tables.cpp b/src/storage_tables.cpp index a7fcd7f..915530c 100644 --- a/src/storage_tables.cpp +++ b/src/storage_tables.cpp @@ -9,4 +9,85 @@ namespace uCentral { int Storage::Create_Tables() { return 0; } + + int Storage::Create_UserTable() { + Poco::Data::Session Sess = Pool_->get(); + + try { + + if (dbType_ == mysql) { + Sess << "CREATE TABLE IF NOT EXISTS Users (" + "Id Id unique primary key, " + "name varchar, " + "description varchar, " + "avatar varchar, " + "email varchar, " + "validated int, " + "validationEmail varchar, " + "validationDate bigint, " + "creationDate bigint, " + "validationURI text, " + "changePassword int, " + "lastLogin bigint, " + "currentLoginURI varchar, " + "lastPasswordChange bigint, " + "lastEmailCheck bigint, " + "currentPassword varchar, " + "lastPasswords varchar," + "waitingForEmailCheck int, " + "locale varchar, " + "notes text, " + "location text, " + "owner varchar, " + "suspended int, " + "blackListed int, " + "userType varchar, " + "userTypeProprietaryInfo text" + " ,INDEX emailindex (email ASC)" + " ,INDEX nameindex (name ASC))", + Poco::Data::Keywords::now; + } else { + Sess << "CREATE TABLE IF NOT EXISTS Users (" + "Id Id unique primary key, " + "name varchar, " + "description varchar, " + "avatar varchar, " + "email varchar, " + "validated int, " + "validationEmail varchar, " + "validationDate bigint, " + "creationDate bigint, " + "validationURI text, " + "changePassword int, " + "lastLogin bigint, " + "currentLoginURI varchar, " + "lastPasswordChange bigint, " + "lastEmailCheck bigint, " + "currentPassword varchar, " + "lastPasswords varchar," + "waitingForEmailCheck int, " + "locale varchar, " + "notes text, " + "location text, " + "owner varchar, " + "suspended int, " + "blackListed int, " + "userType varchar, " + "userTypeProprietaryInfo text" + ")", + Poco::Data::Keywords::now; + Sess << "CREATE INDEX IF NOT EXISTS emailindex ON Users (email ASC)", Poco::Data::Keywords::now; + Sess << "CREATE INDEX IF NOT EXISTS nameindex ON Users (name ASC)", Poco::Data::Keywords::now; + } + return 0; + } catch (const Poco::Exception &E) { + Logger_.log(E); + } + return 1; + } + + int Storage::Create_APIKeyTable() { + + return 0; + } } diff --git a/ucentralsec.properties b/ucentralsec.properties index c326cce..d354245 100644 --- a/ucentralsec.properties +++ b/ucentralsec.properties @@ -35,7 +35,12 @@ ucentral.system.debug = true ucentral.system.uri = https://localhost:16001 ucentral.system.id = 1 ucentral.system.commandchannel = /tmp/app.ucentralgw - + +mailer.hostname = smtp.gmail.com +mailer.username = no-reply@arilia.com +mailer.password = pink-elephants-play-hockey +mailer.loginmethod = login +mailer.port = 587 # # Kafka @@ -82,7 +87,7 @@ storage.type.mysql.connectiontimeout = 60 # authentication.enabled = true authentication.default.username = tip@ucentral.com -authentication.default.password = openwifi +authentication.default.password = 13268b7daa751240369d125e79c873bd8dd3bef7981bdfd38ea03dbb1fbe7dcf authentication.default.access = master authentication.service.type = internal @@ -90,9 +95,13 @@ system.directory.data = $UCENTRALSEC_ROOT/data ucentral.system.debug = true ucentral.system.uri = https://localhost:16001 -ucentral.system.id = 1 ucentral.system.commandchannel = /tmp/app.ucentralgw +# email.includeonly = mydomain.com +# email.exclude = gmail.com + + + ######################################################################## ######################################################################## #