From 89256bb900c4958c081da2446b213b69360a3aa7 Mon Sep 17 00:00:00 2001 From: stephb9959 Date: Mon, 7 Nov 2022 13:37:06 -0800 Subject: [PATCH] https://telecominfraproject.atlassian.net/browse/WIFI-10918 Signed-off-by: stephb9959 --- CMakeLists.txt | 2 +- build | 2 +- openpapi/owsec.yaml | 172 +++++++++++++++++++- src/AuthService.cpp | 24 ++- src/AuthService.h | 1 + src/RESTAPI/RESTAPI_apiKey_handler.cpp | 159 ++++++++++++++++++ src/RESTAPI/RESTAPI_apiKey_handler.h | 34 ++++ src/RESTAPI/RESTAPI_routers.cpp | 5 +- src/RESTAPI/RESTAPI_user_handler.cpp | 1 + src/RESTAPI/RESTAPI_validate_apikey.cpp | 31 ++++ src/RESTAPI/RESTAPI_validate_apikey.h | 27 +++ src/RESTObjects/RESTAPI_SecurityObjects.cpp | 75 +++++++++ src/RESTObjects/RESTAPI_SecurityObjects.h | 39 +++++ src/StorageService.cpp | 2 + src/StorageService.h | 8 +- src/framework/AuthClient.cpp | 50 ++++++ src/framework/AuthClient.h | 21 ++- src/framework/RESTAPI_Handler.h | 38 ++++- src/framework/ow_constants.h | 6 + src/framework/utils.cpp | 4 + src/framework/utils.h | 2 +- src/storage/orm_apikeys.cpp | 101 ++++++++++++ src/storage/orm_apikeys.h | 39 +++++ 23 files changed, 825 insertions(+), 18 deletions(-) create mode 100644 src/RESTAPI/RESTAPI_apiKey_handler.cpp create mode 100644 src/RESTAPI/RESTAPI_apiKey_handler.h create mode 100644 src/RESTAPI/RESTAPI_validate_apikey.cpp create mode 100644 src/RESTAPI/RESTAPI_validate_apikey.h create mode 100644 src/storage/orm_apikeys.cpp create mode 100644 src/storage/orm_apikeys.h diff --git a/CMakeLists.txt b/CMakeLists.txt index deca207..e151a8c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -175,7 +175,7 @@ add_executable( owsec src/TotpCache.h src/RESTAPI/RESTAPI_subtotp_handler.cpp src/RESTAPI/RESTAPI_subtotp_handler.h src/RESTAPI/RESTAPI_signup_handler.cpp src/RESTAPI/RESTAPI_signup_handler.h - src/MessagingTemplates.cpp src/MessagingTemplates.h) + src/MessagingTemplates.cpp src/MessagingTemplates.h src/RESTAPI/RESTAPI_apiKey_handler.cpp src/RESTAPI/RESTAPI_apiKey_handler.h src/storage/orm_apikeys.cpp src/storage/orm_apikeys.h src/RESTAPI/RESTAPI_validate_apikey.cpp src/RESTAPI/RESTAPI_validate_apikey.h) if(NOT SMALL_BUILD) target_link_libraries(owsec PUBLIC diff --git a/build b/build index 9d60796..8e2afd3 100644 --- a/build +++ b/build @@ -1 +1 @@ -11 \ No newline at end of file +17 \ No newline at end of file diff --git a/openpapi/owsec.yaml b/openpapi/owsec.yaml index 04e7621..c2d574f 100644 --- a/openpapi/owsec.yaml +++ b/openpapi/owsec.yaml @@ -17,6 +17,7 @@ servers: security: - bearerAuth: [] - ApiKeyAuth: [] + - ApiToken: [] components: securitySchemes: @@ -28,6 +29,10 @@ components: type: http scheme: bearer bearerFormat: JWT + ApiToken: + type: apiKey + in: header + name: X-API-TOKEN responses: NotFound: @@ -164,18 +169,61 @@ components: aclTemplate: $ref: '#/components/schemas/AclTemplate' - ApiKeyCreationRequest: + ApiKeyAccessRight: type: object properties: + service: + type: string + access: + type: string + enum: + - read + - modify + - create + - delete + - noaccess + + ApiKeyAccessRightList: + type: object + properties: + acls: + type: array + items: + $ref: '#/components/schemas/ApiKeyAccessRight' + + ApiKeyEntry: + type: object + properties: + id: + type: string + format: uuid + userUuid: + type: string + format: uuid name: type: string description: type: string + apiKey: + type: string + salt: + type: string expiresOn: type: integer format: int64 + lastUse: + type: integer + format: int64 rights: - $ref: '#/components/schemas/AclTemplate' + $ref: '#/components/schemas/ApiKeyAccessRightList' + + ApiKeyEntryList: + type: object + properties: + apiKeys: + type: array + items: + $ref: '#/components/schemas/ApiKeyEntry' ApiKeyCreationAnswer: type: object @@ -194,7 +242,7 @@ components: apiKey: type: string rights: - $ref: '#/components/schemas/AclTemplate' + $ref: '#/components/schemas/ApiKeyAccessRights' AclTemplate: type: object @@ -1634,7 +1682,103 @@ paths: 404: $ref: '#/components/responses/NotFound' - + /apiKey/{uuid}: + get: + tags: + - API Tokens + summary: Retrieve all the APIKeys for a given user UUID + operationId: getApiKeyList + parameters: + - in: path + name: uuid + schema: + type: string + format: uuid + required: true + responses: + 200: + $ref: '#/components/schemas/ApiKeyEntryList' + 403: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + delete: + tags: + - API Tokens + summary: Retrieve all the APIKeys for a given user UUID + operationId: deleteApiKey + parameters: + - in: path + name: uuid + schema: + type: string + format: uuid + required: true + - in: query + name: keyUuid + schema: + type: string + required: true + responses: + 200: + $ref: '#/components/responses/Success' + 403: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + post: + tags: + - API Tokens + summary: Retrieve all the APIKeys for a given user UUID + operationId: createApiKey + parameters: + - in: path + name: uuid + schema: + type: string + format: uuid + required: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ApiKeyEntry' + responses: + 200: + $ref: '#/components/schemas/ApiKeyEntry' + 403: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + put: + tags: + - API Tokens + summary: Retrieve all the APIKeys for a given user UUID + operationId: modifyApiKey + parameters: + - in: path + name: uuid + schema: + type: string + format: uuid + required: true + - in: query + name: name + schema: + type: string + required: true + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ApiKeyEntry' + responses: + 200: + $ref: '#/components/schemas/ApiKeyEntry' + 403: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' ######################################################################################### ## @@ -1732,6 +1876,26 @@ paths: 404: $ref: '#/components/responses/NotFound' + /validateApiKey: + get: + tags: + - Security + summary: Allows an application to validate an API Key. + operationId: validateApiKey + parameters: + - in: query + name: token + schema: + type: string + required: true + responses: + 200: + $ref: '#/components/schemas/TokenValidationResult' + 403: + $ref: '#/components/responses/Unauthorized' + 404: + $ref: '#/components/responses/NotFound' + /system: post: tags: diff --git a/src/AuthService.cpp b/src/AuthService.cpp index 99968a1..fc6542a 100644 --- a/src/AuthService.cpp +++ b/src/AuthService.cpp @@ -751,9 +751,7 @@ namespace OpenWifi { WebToken = WT; return true; } - return false; } - // return IsValidSubToken(Token, WebToken, UserInfo, Expired); return false; } @@ -772,7 +770,27 @@ namespace OpenWifi { WebToken = WT; return true; } - return false; + } + return false; + } + + bool AuthService::IsValidApiKey(const std::string &ApiKey, SecurityObjects::WebToken &WebToken, + SecurityObjects::UserInfo &UserInfo, bool &Expired, std::uint64_t &expiresOn) { + + std::lock_guard G(Mutex_); + + std::string UserId; + SecurityObjects::WebToken WT; + SecurityObjects::ApiKeyEntry ApiKeyEntry; + if(StorageService()->ApiKeyDB().GetRecord("apiKey", ApiKey, ApiKeyEntry)) { + expiresOn = ApiKeyEntry.expiresOn; + Expired = ApiKeyEntry.expiresOn < Utils::Now(); + if(Expired) + return false; + if(StorageService()->UserDB().GetUserById(ApiKeyEntry.userUuid,UserInfo)) { + WebToken = WT; + return true; + } } return false; } diff --git a/src/AuthService.h b/src/AuthService.h index bdb3d84..4ca9921 100644 --- a/src/AuthService.h +++ b/src/AuthService.h @@ -77,6 +77,7 @@ namespace OpenWifi{ [[nodiscard]] std::string GenerateTokenJWT(const std::string & UserName, ACCESS_TYPE Type); [[nodiscard]] std::string GenerateTokenHMAC(const std::string & UserName, ACCESS_TYPE Type); + [[nodiscard]] bool IsValidApiKey(const std::string &ApiKey, SecurityObjects::WebToken &WebToken, SecurityObjects::UserInfo &UserInfo, bool & Expired, std::uint64_t & expiresOn); [[nodiscard]] std::string ComputeNewPasswordHash(const std::string &UserName, const std::string &Password); [[nodiscard]] bool ValidatePasswordHash(const std::string & UserName, const std::string & Password, const std::string &StoredPassword); [[nodiscard]] bool ValidateSubPasswordHash(const std::string & UserName, const std::string & Password, const std::string &StoredPassword); diff --git a/src/RESTAPI/RESTAPI_apiKey_handler.cpp b/src/RESTAPI/RESTAPI_apiKey_handler.cpp new file mode 100644 index 0000000..cf9a055 --- /dev/null +++ b/src/RESTAPI/RESTAPI_apiKey_handler.cpp @@ -0,0 +1,159 @@ +// +// Created by stephane bourque on 2022-11-04. +// + +#include "RESTAPI_apiKey_handler.h" +#include "RESTAPI/RESTAPI_db_helpers.h" + +namespace OpenWifi { + + void RESTAPI_apiKey_handler::DoGet() { + std::string user_uuid = GetBinding("uuid",""); + if(user_uuid.empty()) { + return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters); + } + if(user_uuid!=UserInfo_.userinfo.id && UserInfo_.userinfo.userRole!=SecurityObjects::ROOT) { + return UnAuthorized(RESTAPI::Errors::ACCESS_DENIED); + } + + SecurityObjects::ApiKeyEntryList List; + if(DB_.GetRecords(0,500, List.apiKeys, fmt::format(" userUuid='{}' ", user_uuid), " name ")) { + Poco::JSON::Object Answer; + List.to_json(Answer); + return ReturnObject(Answer); + } + return NotFound(); + } + + void RESTAPI_apiKey_handler::DoDelete() { + std::string user_uuid = GetBinding("uuid",""); + if(user_uuid.empty()) { + return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters); + } + + if(user_uuid!=UserInfo_.userinfo.id && UserInfo_.userinfo.userRole!=SecurityObjects::ROOT) { + return UnAuthorized(RESTAPI::Errors::ACCESS_DENIED); + } + + if(user_uuid!=UserInfo_.userinfo.id) { + if(!StorageService()->UserDB().Exists("id",user_uuid)) { + return NotFound(); + } + } + + std::string ApiKeyId= GetParameter("keyUuid",""); + if(ApiKeyId.empty()) { + return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters); + } + + SecurityObjects::ApiKeyEntry ApiKey; + if(StorageService()->ApiKeyDB().GetRecord("id",ApiKeyId,ApiKey)) { + if(ApiKey.userUuid==user_uuid) { + AuthService()->RemoveTokenSystemWide(ApiKey.apiKey); + DB_.DeleteRecord("id", ApiKeyId); + return OK(); + } + return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters); + } + return NotFound(); + } + + void RESTAPI_apiKey_handler::DoPost() { + std::string user_uuid = GetBinding("uuid",""); + + if(user_uuid.empty()) { + return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters); + } + + if(user_uuid!=UserInfo_.userinfo.id && UserInfo_.userinfo.userRole!=SecurityObjects::ROOT) { + return UnAuthorized(RESTAPI::Errors::ACCESS_DENIED); + } + + if(user_uuid!=UserInfo_.userinfo.id) { + // Must verify if the user exists + if(!StorageService()->UserDB().Exists("id",user_uuid)) { + return BadRequest(RESTAPI::Errors::UserMustExist); + } + } + + SecurityObjects::ApiKeyEntry NewKey; + if(!NewKey.from_json(ParsedBody_)) { + return BadRequest(RESTAPI::Errors::InvalidJSONDocument); + } + NewKey.lastUse = 0 ; + + if(!Utils::IsAlphaNumeric(NewKey.name) || NewKey.name.empty()) { + return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters); + } + + Poco::toLowerInPlace(NewKey.name); + NewKey.userUuid = user_uuid; + if(NewKey.expiresOn < Utils::Now()) { + return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters); + } + + // does a key of that name already exit for this user? + SecurityObjects::ApiKeyEntryList ExistingList; + if(DB_.GetRecords(0,500, ExistingList.apiKeys, fmt::format(" userUuid='{}' ", user_uuid))) { + if(std::find_if(ExistingList.apiKeys.begin(),ExistingList.apiKeys.end(), [NewKey](const SecurityObjects::ApiKeyEntry &E) -> bool { + return E.name==NewKey.name; + })!=ExistingList.apiKeys.end()) { + return BadRequest(RESTAPI::Errors::ApiKeyNameAlreadyExists); + } + } + + NewKey.id = MicroServiceCreateUUID(); + NewKey.userUuid = user_uuid; + NewKey.salt = std::to_string(Utils::Now()); + NewKey.apiKey = Utils::ComputeHash(NewKey.salt, UserInfo_.userinfo.id, UserInfo_.webtoken.access_token_ ); + NewKey.created = Utils::Now(); + + if(DB_.CreateRecord(NewKey)) { + Poco::JSON::Object Answer; + NewKey.to_json(Answer); + return ReturnObject(Answer); + } + return BadRequest(RESTAPI::Errors::RecordNotCreated); + } + + void RESTAPI_apiKey_handler::DoPut() { + std::string user_uuid = GetBinding("uuid",""); + if(user_uuid.empty()) { + return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters); + } + if(user_uuid!=UserInfo_.userinfo.id && UserInfo_.userinfo.userRole!=SecurityObjects::ROOT) { + return UnAuthorized(RESTAPI::Errors::ACCESS_DENIED); + } + SecurityObjects::ApiKeyEntry NewKey; + if(!NewKey.from_json(ParsedBody_)) { + return BadRequest(RESTAPI::Errors::InvalidJSONDocument); + } + + SecurityObjects::ApiKeyEntry ExistingKey; + if(!DB_.GetRecord("id",NewKey.id,ExistingKey)) { + return BadRequest(RESTAPI::Errors::ApiKeyDoesNotExist); + } + + if(ExistingKey.userUuid!=user_uuid) { + return BadRequest(RESTAPI::Errors::MissingUserID); + } + + // You can only change the description and the expiration +/* if(ParsedBody_->has("expiresOn")) { + if(NewKey.expiresOn < Utils::Now()) { + return BadRequest(RESTAPI::Errors::MissingOrInvalidParameters); + } + ExistingKey.expiresOn = NewKey.expiresOn; + } +*/ + AssignIfPresent(ParsedBody_,"description",ExistingKey.description); + + if(DB_.UpdateRecord("id",ExistingKey.id,ExistingKey)) { + Poco::JSON::Object Answer; + ExistingKey.to_json(Answer); + return ReturnObject(Answer); + } + BadRequest(RESTAPI::Errors::RecordNotUpdated); + } + +} \ No newline at end of file diff --git a/src/RESTAPI/RESTAPI_apiKey_handler.h b/src/RESTAPI/RESTAPI_apiKey_handler.h new file mode 100644 index 0000000..19a66a4 --- /dev/null +++ b/src/RESTAPI/RESTAPI_apiKey_handler.h @@ -0,0 +1,34 @@ +// +// Created by stephane bourque on 2022-11-04. +// + +#pragma once + +#include "framework/RESTAPI_Handler.h" +#include "StorageService.h" +namespace OpenWifi { + class RESTAPI_apiKey_handler : public RESTAPIHandler { + public: + RESTAPI_apiKey_handler(const RESTAPIHandler::BindingMap &bindings, Poco::Logger &L, RESTAPI_GenericServerAccounting &Server, uint64_t TransactionId, bool Internal) + : RESTAPIHandler(bindings, L, + std::vector{ + Poco::Net::HTTPRequest::HTTP_GET, + Poco::Net::HTTPRequest::HTTP_PUT, + Poco::Net::HTTPRequest::HTTP_POST, + Poco::Net::HTTPRequest::HTTP_DELETE, + Poco::Net::HTTPRequest::HTTP_OPTIONS}, + Server, + TransactionId, + Internal) {} + static auto PathName() { return std::list{"/api/v1/apiKey/{uuid}"}; }; + private: + ApiKeyDB &DB_=StorageService()->ApiKeyDB(); + + void DoGet() final; + void DoPut() final; + void DoPost() final; + void DoDelete() final; + + }; +} + diff --git a/src/RESTAPI/RESTAPI_routers.cpp b/src/RESTAPI/RESTAPI_routers.cpp index 78f8183..6fb58ea 100644 --- a/src/RESTAPI/RESTAPI_routers.cpp +++ b/src/RESTAPI/RESTAPI_routers.cpp @@ -23,6 +23,8 @@ #include "RESTAPI/RESTAPI_totp_handler.h" #include "RESTAPI/RESTAPI_subtotp_handler.h" #include "RESTAPI/RESTAPI_signup_handler.h" +#include "RESTAPI/RESTAPI_apiKey_handler.h" + #include "framework/RESTAPI_SystemCommand.h" #include "framework/RESTAPI_WebSocketServer.h" @@ -54,7 +56,8 @@ namespace OpenWifi { RESTAPI_signup_handler, RESTAPI_validate_sub_token_handler, RESTAPI_validate_token_handler, - RESTAPI_webSocketServer + RESTAPI_webSocketServer, + RESTAPI_apiKey_handler >(Path, Bindings, L, S,TransactionId); } diff --git a/src/RESTAPI/RESTAPI_user_handler.cpp b/src/RESTAPI/RESTAPI_user_handler.cpp index 0a252f0..acb55ee 100644 --- a/src/RESTAPI/RESTAPI_user_handler.cpp +++ b/src/RESTAPI/RESTAPI_user_handler.cpp @@ -66,6 +66,7 @@ namespace OpenWifi { StorageService()->AvatarDB().DeleteAvatar(UserInfo_.userinfo.email,Id); StorageService()->PreferencesDB().DeletePreferences(UserInfo_.userinfo.email,Id); StorageService()->UserTokenDB().RevokeAllTokens(Id); + StorageService()->ApiKeyDB().RemoveAllApiKeys(Id); Logger_.information(fmt::format("User '{}' deleted by '{}'.",Id,UserInfo_.userinfo.email)); OK(); } diff --git a/src/RESTAPI/RESTAPI_validate_apikey.cpp b/src/RESTAPI/RESTAPI_validate_apikey.cpp new file mode 100644 index 0000000..41854eb --- /dev/null +++ b/src/RESTAPI/RESTAPI_validate_apikey.cpp @@ -0,0 +1,31 @@ +// +// Created by stephane bourque on 2022-11-07. +// + +#include "RESTAPI_validate_apikey.h" +#include "AuthService.h" + +namespace OpenWifi { + + void RESTAPI_validate_apikey::DoGet() { + Poco::URI URI(Request->getURI()); + auto Parameters = URI.getQueryParameters(); + for(auto const &i:Parameters) { + if (i.first == "apikey") { + // can we find this token? + SecurityObjects::UserInfoAndPolicy SecObj; + bool Expired = false; + std::uint64_t expiresOn=0; + if (AuthService()->IsValidApiKey(i.second, SecObj.webtoken, SecObj.userinfo, Expired, expiresOn)) { + Poco::JSON::Object Answer; + SecObj.to_json(Answer); + Answer.set("expiresOn", expiresOn); + return ReturnObject(Answer); + } + return UnAuthorized(RESTAPI::Errors::ACCESS_DENIED); + } + } + return NotFound(); + } + +} // OpenWifi \ No newline at end of file diff --git a/src/RESTAPI/RESTAPI_validate_apikey.h b/src/RESTAPI/RESTAPI_validate_apikey.h new file mode 100644 index 0000000..3a21867 --- /dev/null +++ b/src/RESTAPI/RESTAPI_validate_apikey.h @@ -0,0 +1,27 @@ +// +// Created by stephane bourque on 2022-11-07. +// + +#pragma once + +#include "framework/RESTAPI_Handler.h" + +namespace OpenWifi { + class RESTAPI_validate_apikey : public RESTAPIHandler { + public: + RESTAPI_validate_apikey(const RESTAPIHandler::BindingMap &bindings, Poco::Logger &L, RESTAPI_GenericServerAccounting &Server, uint64_t TransactionId, bool Internal) + : RESTAPIHandler(bindings, L, + std::vector + {Poco::Net::HTTPRequest::HTTP_GET, + Poco::Net::HTTPRequest::HTTP_OPTIONS}, + Server, + TransactionId, + Internal) {}; + static auto PathName() { return std::list{"/api/v1/validateApiKey"}; }; + void DoGet() final; + void DoPost() final {}; + void DoDelete() final {}; + void DoPut() final {}; + }; +} + diff --git a/src/RESTObjects/RESTAPI_SecurityObjects.cpp b/src/RESTObjects/RESTAPI_SecurityObjects.cpp index 9b84be3..c02df59 100644 --- a/src/RESTObjects/RESTAPI_SecurityObjects.cpp +++ b/src/RESTObjects/RESTAPI_SecurityObjects.cpp @@ -619,5 +619,80 @@ namespace OpenWifi::SecurityObjects { field_to_json(Obj,"login",login); field_to_json(Obj,"logout",logout); } + + void ApiKeyAccessRight::to_json(Poco::JSON::Object &Obj) const { + field_to_json(Obj, "service", service); + field_to_json(Obj, "access", access); + } + + bool ApiKeyAccessRight::from_json(const Poco::JSON::Object::Ptr &Obj) { + try { + field_from_json(Obj, "service", service); + field_from_json(Obj, "access", access); + return true; + } catch(...) { + std::cout << "Cannot parse: Token" << std::endl; + } + return false; + } + + void ApiKeyAccessRightList::to_json(Poco::JSON::Object &Obj) const { + field_to_json(Obj, "acls", acls); + } + + bool ApiKeyAccessRightList::from_json(const Poco::JSON::Object::Ptr &Obj) { + try { + field_from_json(Obj, "acls", acls); + return true; + } catch(...) { + std::cout << "Cannot parse: Token" << std::endl; + } + return false; + } + + void ApiKeyEntry::to_json(Poco::JSON::Object &Obj) const { + field_to_json(Obj, "id", id); + field_to_json(Obj, "userUuid", userUuid); + field_to_json(Obj, "name", name); + field_to_json(Obj, "apiKey", apiKey); + field_to_json(Obj, "salt", salt); + field_to_json(Obj, "description", description); + field_to_json(Obj, "expiresOn", expiresOn); + field_to_json(Obj, "rights", rights); + field_to_json(Obj, "lastUse", lastUse); + } + + bool ApiKeyEntry::from_json(const Poco::JSON::Object::Ptr &Obj) { + try { + field_from_json(Obj, "id", id); + field_from_json(Obj, "userUuid", userUuid); + field_from_json(Obj, "name", name); + field_from_json(Obj, "apiKey", apiKey); + field_from_json(Obj, "salt", salt); + field_from_json(Obj, "description", description); + field_from_json(Obj, "expiresOn", expiresOn); + field_from_json(Obj, "rights", rights); + field_from_json(Obj, "lastUse", lastUse); + return true; + } catch(...) { + std::cout << "Cannot parse: Token" << std::endl; + } + return false; + } + + void ApiKeyEntryList::to_json(Poco::JSON::Object &Obj) const { + field_to_json(Obj, "apiKeys", apiKeys); + } + + bool ApiKeyEntryList::from_json(const Poco::JSON::Object::Ptr &Obj) { + try { + field_from_json(Obj, "apiKeys", apiKeys); + return true; + } catch(...) { + std::cout << "Cannot parse: Token" << std::endl; + } + return false; + } + } diff --git a/src/RESTObjects/RESTAPI_SecurityObjects.h b/src/RESTObjects/RESTAPI_SecurityObjects.h index 3cee8cc..a804128 100644 --- a/src/RESTObjects/RESTAPI_SecurityObjects.h +++ b/src/RESTObjects/RESTAPI_SecurityObjects.h @@ -325,5 +325,44 @@ namespace OpenWifi { void to_json(Poco::JSON::Object &Obj) const; }; + + struct ApiKeyAccessRight { + std::string service; + std::string access; + + void to_json(Poco::JSON::Object &Obj) const; + bool from_json(const Poco::JSON::Object::Ptr &Obj); + }; + + struct ApiKeyAccessRightList { + std::vector acls; + + void to_json(Poco::JSON::Object &Obj) const; + bool from_json(const Poco::JSON::Object::Ptr &Obj); + }; + + struct ApiKeyEntry { + Types::UUID_t id; + Types::UUID_t userUuid; + std::string name; + std::string description; + std::string apiKey; + std::string salt; + std::uint64_t created; + std::uint64_t expiresOn=0; + ApiKeyAccessRightList rights; + std::uint64_t lastUse=0; + + void to_json(Poco::JSON::Object &Obj) const; + bool from_json(const Poco::JSON::Object::Ptr &Obj); + }; + + struct ApiKeyEntryList { + std::vector apiKeys; + + void to_json(Poco::JSON::Object &Obj) const; + bool from_json(const Poco::JSON::Object::Ptr &Obj); + }; + } } diff --git a/src/StorageService.cpp b/src/StorageService.cpp index 44e6582..1c29be6 100644 --- a/src/StorageService.cpp +++ b/src/StorageService.cpp @@ -36,6 +36,7 @@ namespace OpenWifi { SubAvatarDB_ = std::make_unique("SubAvatars", "avs", dbType_,*Pool_, Logger()); LoginDB_ = std::make_unique("Logins", "lin", dbType_,*Pool_, Logger()); SubLoginDB_ = std::make_unique("SubLogins", "lis", dbType_,*Pool_, Logger()); + ApiKeyDB_ = std::make_unique("ApiKeys", "api", dbType_,*Pool_, Logger()); UserDB_->Create(); SubDB_->Create(); @@ -47,6 +48,7 @@ namespace OpenWifi { AvatarDB_->Create(); SubAvatarDB_->Create(); LoginDB_->Create(); + ApiKeyDB_->Create(); SubLoginDB_->Create(); OpenWifi::SpecialUserHelpers::InitializeDefaultUser(); diff --git a/src/StorageService.h b/src/StorageService.h index 54f4039..e37ebd2 100644 --- a/src/StorageService.h +++ b/src/StorageService.h @@ -6,8 +6,7 @@ // Arilia Wireless Inc. // -#ifndef UCENTRAL_USTORAGESERVICE_H -#define UCENTRAL_USTORAGESERVICE_H +#pragma once #include "RESTObjects/RESTAPI_SecurityObjects.h" #include "framework/StorageClass.h" @@ -21,6 +20,7 @@ #include "storage/orm_actionLinks.h" #include "storage/orm_avatar.h" #include "storage/orm_logins.h" +#include "storage/orm_apikeys.h" namespace OpenWifi { @@ -52,6 +52,7 @@ namespace OpenWifi { OpenWifi::AvatarDB & SubAvatarDB() { return *SubAvatarDB_; } OpenWifi::LoginDB & LoginDB() { return *LoginDB_; } OpenWifi::LoginDB & SubLoginDB() { return *SubLoginDB_; } + OpenWifi::ApiKeyDB & ApiKeyDB() { return *ApiKeyDB_; } private: @@ -66,6 +67,7 @@ namespace OpenWifi { std::unique_ptr SubAvatarDB_; std::unique_ptr LoginDB_; std::unique_ptr SubLoginDB_; + std::unique_ptr ApiKeyDB_; std::unique_ptr UserCache_; std::unique_ptr SubCache_; @@ -80,5 +82,3 @@ namespace OpenWifi { inline auto StorageService() { return StorageService::instance(); }; } // namespace - -#endif //UCENTRAL_USTORAGESERVICE_H diff --git a/src/framework/AuthClient.cpp b/src/framework/AuthClient.cpp index 619358d..3caa5c0 100644 --- a/src/framework/AuthClient.cpp +++ b/src/framework/AuthClient.cpp @@ -68,4 +68,54 @@ namespace OpenWifi { return RetrieveTokenInformation(SessionToken, UInfo, TID, Expired, Contacted, Sub); } + bool AuthClient::RetrieveApiKeyInformation(const std::string & SessionToken, + SecurityObjects::UserInfoAndPolicy & UInfo, + std::uint64_t TID, + bool & Expired, bool & Contacted) { + try { + Types::StringPairVec QueryData; + QueryData.push_back(std::make_pair("apikey",SessionToken)); + OpenAPIRequestGet Req( uSERVICE_SECURITY, + "/api/v1/validateApiKey" , + QueryData, + 10000); + Poco::JSON::Object::Ptr Response; + + auto StatusCode = Req.Do(Response); + if(StatusCode==Poco::Net::HTTPServerResponse::HTTP_GATEWAY_TIMEOUT) { + Contacted = false; + return false; + } + + Contacted = true; + if(StatusCode==Poco::Net::HTTPServerResponse::HTTP_OK) { + if(Response->has("tokenInfo") && Response->has("userInfo") && Response->has("expiresOn")) { + UInfo.from_json(Response); + Expired = false; + + ApiKeyCache_.update(SessionToken, ApiKeyCacheEntry{ .UserInfo = UInfo, .ExpiresOn = Response->get("expiresOn")}); + return true; + } else { + return false; + } + } + } catch (...) { + poco_error(Logger(),fmt::format("Failed to retrieve api key={} for TID={}", SessionToken, TID)); + } + Expired = false; + return false; + } + + bool AuthClient::IsValidApiKey(const std::string &SessionToken, SecurityObjects::UserInfoAndPolicy &UInfo, + std::uint64_t TID, bool &Expired, bool &Contacted) { + auto User = ApiKeyCache_.get(SessionToken); + if (!User.isNull()) { + if(User->ExpiresOn < Utils::Now()) + Expired = false; + UInfo = User->UserInfo; + return true; + } + return RetrieveApiKeyInformation(SessionToken, UInfo, TID, Expired, Contacted); + } + } // namespace OpenWifi \ No newline at end of file diff --git a/src/framework/AuthClient.h b/src/framework/AuthClient.h index 70aa0d9..44f16db 100644 --- a/src/framework/AuthClient.h +++ b/src/framework/AuthClient.h @@ -12,6 +12,7 @@ namespace OpenWifi { class AuthClient : public SubSystemServer { + public: explicit AuthClient() noexcept: SubSystemServer("Authentication", "AUTH-CLNT", "authentication") @@ -23,7 +24,12 @@ namespace OpenWifi { return instance_; } - inline int Start() override { + struct ApiKeyCacheEntry { + OpenWifi::SecurityObjects::UserInfoAndPolicy UserInfo; + std::uint64_t ExpiresOn; + }; + + inline int Start() override { return 0; } @@ -36,6 +42,7 @@ namespace OpenWifi { inline void RemovedCachedToken(const std::string &Token) { Cache_.remove(Token); + ApiKeyCache_.remove(Token); } inline static bool IsTokenExpired(const SecurityObjects::WebToken &T) { @@ -46,12 +53,24 @@ namespace OpenWifi { SecurityObjects::UserInfoAndPolicy & UInfo, std::uint64_t TID, bool & Expired, bool & Contacted, bool Sub=false); + + bool RetrieveApiKeyInformation(const std::string & SessionToken, + SecurityObjects::UserInfoAndPolicy & UInfo, + std::uint64_t TID, + bool & Expired, bool & Contacted); + bool IsAuthorized(const std::string &SessionToken, SecurityObjects::UserInfoAndPolicy & UInfo, std::uint64_t TID, bool & Expired, bool & Contacted, bool Sub = false); + bool IsValidApiKey(const std::string &SessionToken, SecurityObjects::UserInfoAndPolicy & UInfo, + std::uint64_t TID, + bool & Expired, bool & Contacted); + private: + Poco::ExpireLRUCache Cache_{512,1200000 }; + Poco::ExpireLRUCache ApiKeyCache_{512,1200000 }; }; inline auto AuthClient() { return AuthClient::instance(); } diff --git a/src/framework/RESTAPI_Handler.h b/src/framework/RESTAPI_Handler.h index d0161ec..7055ac1 100644 --- a/src/framework/RESTAPI_Handler.h +++ b/src/framework/RESTAPI_Handler.h @@ -26,6 +26,10 @@ #include "framework/AuthClient.h" #include "RESTObjects/RESTAPI_SecurityObjects.h" +#if defined(TIP_SECURITY_SERVICE) +#include "AuthService.h" +#endif + using namespace std::chrono_literals; namespace OpenWifi { @@ -640,7 +644,8 @@ namespace OpenWifi { }; #ifdef TIP_SECURITY_SERVICE - [[nodiscard]] bool AuthServiceIsAuthorized(Poco::Net::HTTPServerRequest & Request,std::string &SessionToken, SecurityObjects::UserInfoAndPolicy & UInfo, std::uint64_t TID, bool & Expired , bool Sub ); + [[nodiscard]] bool AuthServiceIsAuthorized(Poco::Net::HTTPServerRequest & Request,std::string &SessionToken, + SecurityObjects::UserInfoAndPolicy & UInfo, std::uint64_t TID, bool & Expired , bool Sub ); #endif inline bool RESTAPIHandler::IsAuthorized( bool & Expired , [[maybe_unused]] bool & Contacted , bool Sub ) { if(Internal_ && Request->has("X-INTERNAL-NAME")) { @@ -665,7 +670,36 @@ namespace OpenWifi { } } return Allowed; - } else { + } else if(!Internal_ && Request->has("X-API-KEY")) { + SessionToken_ = Request->get("X-API-KEY", ""); + std::uint64_t expiresOn; +#ifdef TIP_SECURITY_SERVICE + if (AuthService()->IsValidApiKey(SessionToken_, UserInfo_.webtoken, UserInfo_.userinfo, Expired, expiresOn)) { +#else + if (AuthClient()->IsValidApiKey( SessionToken_, UserInfo_, TransactionId_, Expired, Contacted, Sub)) { +#endif + REST_Requester_ = UserInfo_.userinfo.email; + if(Server_.LogIt(Request->getMethod(),true)) { + poco_debug(Logger_,fmt::format("X-REQ-ALLOWED({}): APIKEY-ACCESS TID={} User='{}@{}' Method={} Path={}", + UserInfo_.userinfo.email, + TransactionId_, + Utils::FormatIPv6(Request->clientAddress().toString()), + Request->clientAddress().toString(), + Request->getMethod(), + Request->getURI())); + } + return true; + } else { + if(Server_.LogBadTokens(true)) { + poco_debug(Logger_,fmt::format("X-REQ-DENIED({}): TID={} Method={} Path={}", + Utils::FormatIPv6(Request->clientAddress().toString()), + TransactionId_, + Request->getMethod(), + Request->getURI())); + } + } + return false; + } else { if (SessionToken_.empty()) { try { Poco::Net::OAuth20Credentials Auth(*Request); diff --git a/src/framework/ow_constants.h b/src/framework/ow_constants.h index cc6c5ae..15758aa 100644 --- a/src/framework/ow_constants.h +++ b/src/framework/ow_constants.h @@ -222,6 +222,12 @@ namespace OpenWifi::RESTAPI::Errors { static const struct msg DeviceRequiresSignature{1146,"Device requires device signature to be provided."}; + static const struct msg ApiKeyNameAlreadyExists{1147,"API Key name must be unique."}; + static const struct msg TooManyApiKeys{1148,"Too many API Keys have already been created."}; + static const struct msg UserMustExist{1149,"User must exist."}; + static const struct msg ApiKeyNameDoesNotExist{1150,"API Key name does not exist."}; + static const struct msg ApiKeyDoesNotExist{1150,"API Key does not exist."}; + } diff --git a/src/framework/utils.cpp b/src/framework/utils.cpp index 7983727..fc0f224 100644 --- a/src/framework/utils.cpp +++ b/src/framework/utils.cpp @@ -520,4 +520,8 @@ bool ExtractBase64CompressedData(const std::string &CompressedData, return false; } + bool IsAlphaNumeric(const std::string &s) { + return std::all_of(s.begin(),s.end(),[](char c) -> bool { return isalnum(c); }); + } + } diff --git a/src/framework/utils.h b/src/framework/utils.h index ce33fd1..d72f7ee 100644 --- a/src/framework/utils.h +++ b/src/framework/utils.h @@ -115,7 +115,7 @@ namespace OpenWifi::Utils { [[nodiscard]] std::string BinaryFileToHexString(const Poco::File &F); [[nodiscard]] std::string SecondsToNiceText(uint64_t Seconds); [[nodiscard]] bool wgets(const std::string &URL, std::string &Response); - + [[nodiscard]] bool IsAlphaNumeric(const std::string &s); template< typename T > std::string int_to_hex( T i ) { diff --git a/src/storage/orm_apikeys.cpp b/src/storage/orm_apikeys.cpp new file mode 100644 index 0000000..74e15dd --- /dev/null +++ b/src/storage/orm_apikeys.cpp @@ -0,0 +1,101 @@ +// +// Created by stephane bourque on 2022-11-04. +// + +#include "orm_apikeys.h" +#include "framework/RESTAPI_utils.h" +#include "RESTObjects/RESTAPI_SecurityObjects.h" +#include "framework/orm.h" +#include "AuthService.h" +#include "StorageService.h" +#include "fmt/format.h" + +namespace OpenWifi { + static ORM::FieldVec ApiKeyDB_Fields{ + ORM::Field{"id", 36, true}, + ORM::Field{"userUuid", ORM::FieldType::FT_TEXT}, + ORM::Field{"name", ORM::FieldType::FT_TEXT}, + ORM::Field{"description", ORM::FieldType::FT_TEXT}, + ORM::Field{"apiKey", ORM::FieldType::FT_TEXT}, + ORM::Field{"salt", ORM::FieldType::FT_TEXT}, + ORM::Field{"created", ORM::FieldType::FT_BIGINT}, + ORM::Field{"expiresOn", ORM::FieldType::FT_BIGINT}, + ORM::Field{"rights", ORM::FieldType::FT_TEXT}, + ORM::Field{"lastUse", ORM::FieldType::FT_BIGINT} + }; + + static ORM::IndexVec MakeIndices(const std::string & shortname) { + return ORM::IndexVec{ + {std::string(shortname + "_username_index"), + ORM::IndexEntryVec{ + { + std::string("userUuid"), + ORM::Indextype::ASC }}}, + {std::string(shortname + "_apikey_index"), + ORM::IndexEntryVec{ + { + std::string("apiKey"), + ORM::Indextype::ASC} + } + } + }; + }; + + ApiKeyDB::ApiKeyDB( const std::string &TableName, const std::string &Shortname ,OpenWifi::DBType T, + Poco::Data::SessionPool &P, Poco::Logger &L) : + DB(T, TableName.c_str(), ApiKeyDB_Fields, MakeIndices(Shortname), P, L, Shortname.c_str()) { + } + + bool ApiKeyDB::RemoveAllApiKeys(const std::string & user_uuid) { + SecurityObjects::ApiKeyEntryList Keys; + if(StorageService()->ApiKeyDB().GetRecords(0,500,Keys.apiKeys,fmt::format(" userUuid='{} ", user_uuid))) { + for(const auto &key:Keys.apiKeys) { + AuthService()->RemoveTokenSystemWide(key.apiKey); + } + } + return true; + } + + bool ApiKeyDB::Upgrade([[maybe_unused]] uint32_t from, uint32_t &to) { + to = Version(); + std::vector Script{ + }; + + for(const auto &i:Script) { + try { + auto Session = Pool_.get(); + Session << i , Poco::Data::Keywords::now; + } catch (...) { + + } + } + return true; + } + +} // OpenWifi + +template<> void ORM::DB::Convert(const OpenWifi::ApiKeyRecordTuple &In, OpenWifi::SecurityObjects::ApiKeyEntry &Out) { + Out.id = In.get<0>(); + Out.userUuid = In.get<1>(); + Out.name = In.get<2>(); + Out.description = In.get<3>(); + Out.apiKey = In.get<4>(); + Out.salt = In.get<5>(); + Out.created = In.get<6>(); + Out.expiresOn = In.get<7>(); + Out.rights.acls = OpenWifi::RESTAPI_utils::to_object_array(In.get<8>()); + Out.lastUse = In.get<9>(); +} + +template<> void ORM::DB::Convert(const OpenWifi::SecurityObjects::ApiKeyEntry &In, OpenWifi::ApiKeyRecordTuple &Out) { + Out.set<0>(In.id); + Out.set<1>(In.userUuid); + Out.set<2>(In.name); + Out.set<3>(In.description); + Out.set<4>(In.apiKey); + Out.set<5>(In.salt); + Out.set<6>(In.created); + Out.set<7>(In.expiresOn); + Out.set<8>(OpenWifi::RESTAPI_utils::to_string(In.rights.acls)); + Out.set<9>(In.lastUse); +} diff --git a/src/storage/orm_apikeys.h b/src/storage/orm_apikeys.h new file mode 100644 index 0000000..1349609 --- /dev/null +++ b/src/storage/orm_apikeys.h @@ -0,0 +1,39 @@ +// +// Created by stephane bourque on 2022-11-04. +// + +#pragma once + +#include "framework/orm.h" +#include "RESTObjects/RESTAPI_SecurityObjects.h" + +namespace OpenWifi { + + typedef Poco::Tuple< + std::string, // id + std::string, // userUuid + std::string, // name + std::string, // description + std::string, // apiKey + std::string, // salt + uint64_t, // created = 0; + uint64_t, // expiresOn = 0; + std::string, // rights + std::uint64_t // lastUse + > ApiKeyRecordTuple; + typedef std::vector ApiKeyRecordTupleTupleList; + + class ApiKeyDB : public ORM::DB { + public: + ApiKeyDB( const std::string &name, const std::string &shortname, OpenWifi::DBType T, Poco::Data::SessionPool & P, Poco::Logger &L); + virtual ~ApiKeyDB() {} + inline uint32_t Version() override { + return 1; + } + + bool Upgrade(uint32_t from, uint32_t &to) override; + bool RemoveAllApiKeys(const std::string & user_uuid); + + }; + +}