├── .codacy.yml ├── .codecov.yml ├── .gitattributes ├── .github └── workflows │ └── archive-docs.yml ├── .gitignore ├── .travis.yml ├── BUILD.md ├── CMakeLists.txt ├── LICENSE ├── README.md ├── RELEASENOTES.md ├── ci └── pipelines │ └── Build.jenkinsfile ├── conanfile.py ├── src └── RESTAPICore │ ├── CMakeLists.txt │ ├── Endpoint │ ├── ClaimConstants.h │ ├── EndpointRequestAuthorizationClaims.cpp │ ├── EndpointRequestAuthorizationClaims.h │ ├── EndpointRequestData.cpp │ ├── EndpointRequestData.h │ ├── EndpointRequestParams.cpp │ ├── EndpointRequestParams.h │ └── IEndpoint.h │ ├── RouteAccess │ ├── EpochTimeService.cpp │ ├── EpochTimeService.h │ ├── IEpochTimeService.h │ ├── IRouteAccessValidator.h │ ├── IUserRoleService.h │ ├── TokenExpirationAccessValidator.cpp │ ├── TokenExpirationAccessValidator.h │ ├── UserRoleRouteAccessValidator.cpp │ └── UserRoleRouteAccessValidator.h │ ├── Router │ ├── AuthorizationDataBuilder.cpp │ ├── AuthorizationDataBuilder.h │ ├── IAuthorizationDataBuilder.h │ ├── IRoute.h │ ├── IRoutesFactory.h │ ├── Route.cpp │ ├── Route.h │ ├── RouteFragment.cpp │ ├── RouteFragment.h │ ├── Router.cpp │ ├── Router.h │ ├── RoutesFactory.cpp │ └── RoutesFactory.h │ └── stdafx.h ├── test ├── RESTAPICoreTest │ ├── CMakeLists.txt │ ├── RESTAPICoreTest.cpp │ ├── Tests │ │ ├── RouteAccess │ │ │ ├── EpochTimeServiceTest.cpp │ │ │ ├── TokenExpirationAccessValidatorTest.cpp │ │ │ └── UserRoleRouteAccessValidatorTest.cpp │ │ ├── Router │ │ │ ├── AuthorizationDataBuilderTest.cpp │ │ │ ├── RouteTest.cpp │ │ │ └── RouterTest.cpp │ │ └── RouterComponentTest.cpp │ └── stdafx.h └── RESTAPICoreTestUtilities │ ├── Builders │ ├── EndpointRequestParamsBuilder.cpp │ └── EndpointRequestParamsBuilder.h │ ├── CMakeLists.txt │ ├── Comparators │ ├── EndpointRequestAuthorizationClaimsComparator.cpp │ ├── EndpointRequestDataComparator.cpp │ └── EndpointRequestParamsComparator.cpp │ ├── Mocks │ ├── Endpoint │ │ ├── MockEndpoint.cpp │ │ └── MockEndpoint.h │ ├── RouteAccess │ │ ├── MockEpochTimeService.cpp │ │ ├── MockEpochTimeService.h │ │ ├── MockRouteAccessValidator.cpp │ │ ├── MockRouteAccessValidator.h │ │ ├── MockUserRoleService.cpp │ │ └── MockUserRoleService.h │ └── Router │ │ ├── MockRoute.cpp │ │ ├── MockRoute.h │ │ ├── MockRoutesFactory.cpp │ │ └── MockRoutesFactory.h │ ├── Stubs │ ├── StubEpochTimeService.cpp │ └── StubEpochTimeService.h │ ├── conanfile.py │ ├── stdafx.h │ └── test_package │ ├── CMakeLists.txt │ ├── RESTAPICoreTestUtilitiesExample.cpp │ └── conanfile.py ├── test_package ├── CMakeLists.txt ├── RESTAPICoreExample.cpp └── conanfile.py └── vs2022.conanprofile /.codacy.yml: -------------------------------------------------------------------------------- 1 | exclude_paths: 2 | - "**.md" 3 | - test/**/* -------------------------------------------------------------------------------- /.codecov.yml: -------------------------------------------------------------------------------- 1 | ignore: 2 | - "build/.*" 3 | - "cmake/.*" 4 | - "extern/.*" 5 | - "test/.*" 6 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.cmake linguist-vendored 2 | *.txt linguist-vendored 3 | *.py linguist-vendored 4 | *.sh linguist-vendored -------------------------------------------------------------------------------- /.github/workflows/archive-docs.yml: -------------------------------------------------------------------------------- 1 | on: 2 | repository_dispatch: 3 | types: [doc-build] 4 | 5 | jobs: 6 | build_docs_job: 7 | runs-on: ubuntu-latest 8 | name: 'Automated generation of library documentation' 9 | steps: 10 | - name: 'Generate library documentation' 11 | id: archive-doc 12 | uses: systelab/cpp-library-doc-action@master 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | with: 16 | owner: systelab 17 | repo_name: cpp-rest-api-core 18 | library_name: 'RESTAPICore CSW library' 19 | tag_name: ${{ github.event.client_payload.tag }} 20 | configuration_name: ${{ github.event.client_payload.configuration }} 21 | ci_system: ${{ github.event.client_payload.ci }} 22 | job_id: ${{ github.event.client_payload.job }} 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build* 2 | extern -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - os: linux 4 | dist: xenial 5 | language: python 6 | compiler: gcc 7 | python: "3.7" 8 | addons: 9 | apt: 10 | sources: 11 | - ubuntu-toolchain-r-test 12 | packages: 13 | - gcc-7 14 | - g++-7 15 | env: 16 | - COMPILER_NAME=gcc-7 17 | - BUILD_TYPE=Release 18 | - CONAN_BUILD_TYPE=Release 19 | - COMPILER_LIBCXX=libstdc++11 20 | - GTEST_VERSION=1.8.1 21 | - OPENSSL_VERSION=1.0.2s 22 | 23 | - os: linux 24 | dist: xenial 25 | language: python 26 | compiler: gcc 27 | python: "3.7" 28 | addons: 29 | apt: 30 | sources: 31 | - ubuntu-toolchain-r-test 32 | packages: 33 | - gcc-7 34 | - g++-7 35 | env: 36 | - COMPILER_NAME=gcc-7 37 | - BUILD_TYPE=Debug 38 | - CONAN_BUILD_TYPE=Debug 39 | - COMPILER_LIBCXX=libstdc++11 40 | - GTEST_VERSION=1.8.1 41 | - OPENSSL_VERSION=1.0.2s 42 | 43 | - os: linux 44 | dist: xenial 45 | language: python 46 | compiler: gcc 47 | python: "3.7" 48 | addons: 49 | apt: 50 | sources: 51 | - ubuntu-toolchain-r-test 52 | packages: 53 | - gcc-7 54 | - g++-7 55 | env: 56 | - COMPILER_NAME=gcc-7 57 | - BUILD_TYPE=Coverage 58 | - CONAN_BUILD_TYPE=Debug 59 | - COMPILER_LIBCXX=libstdc++11 60 | - GTEST_VERSION=1.8.1 61 | - OPENSSL_VERSION=1.0.2s 62 | 63 | - os: osx 64 | osx_image: xcode12.2 65 | language: cpp 66 | compiler: clang 67 | env: 68 | - COMPILER_NAME=clang-MacOS 69 | - BUILD_TYPE=Release 70 | - CONAN_BUILD_TYPE=Release 71 | - COMPILER_LIBCXX=libc++ 72 | - GTEST_VERSION=1.8.1 73 | - OPENSSL_VERSION=1.0.2s 74 | - HOMEBREW_NO_AUTO_UPDATE=1 75 | cache: 76 | directories: 77 | - $HOME/Library/Caches/Homebrew 78 | - /usr/local/Homebrew 79 | 80 | install: 81 | - if [ $TRAVIS_OS_NAME == linux ]; then sudo update-alternatives --install /usr/bin/gcc gcc /usr/bin/gcc-7 90; fi 82 | - if [ $TRAVIS_OS_NAME == linux ]; then sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-7 90; fi 83 | - if [[ $TRAVIS_OS_NAME == linux && $BUILD_TYPE == Coverage ]]; then sudo apt-get install lcov; fi 84 | - if [ $TRAVIS_OS_NAME == linux ]; then pip install conan; fi 85 | - if [ $TRAVIS_OS_NAME == osx ]; then brew install conan; fi 86 | - conan user 87 | - conan remote add systelab-public https://systelab.jfrog.io/artifactory/api/conan/cpp-conan-production-local 88 | - conan --version 89 | 90 | script: 91 | - g++ --version 92 | - mkdir -p build 93 | - cd build 94 | - conan install .. -s build_type=${CONAN_BUILD_TYPE} -s compiler.libcxx=${COMPILER_LIBCXX} -o gtest=${GTEST_VERSION} -o openssl=${OPENSSL_VERSION} 95 | - cmake .. -DCMAKE_BUILD_TYPE=${BUILD_TYPE} 96 | - make 97 | - cd bin 98 | - if [ $TRAVIS_OS_NAME == osx ]; then cd ${BUILD_TYPE}; fi 99 | - if [ $TRAVIS_OS_NAME == osx ]; then cp -f *.dylib /usr/local/lib; fi 100 | - if [ $TRAVIS_OS_NAME == osx ]; then cd ..; fi 101 | - ./RESTAPICoreTest --gtest_output=xml:RESTAPICoreTest.xml 102 | - cd .. 103 | - if [[ $TRAVIS_OS_NAME == linux && $BUILD_TYPE == Coverage ]]; then make RESTAPICoreTestCoverage; fi 104 | - cd test 105 | - cd RESTAPICoreTest 106 | - ctest 107 | - cd .. 108 | - cd .. 109 | - | 110 | if [ -n "$TRAVIS_TAG" ]; then 111 | conan export-pkg ../conanfile.py "RESTAPICore/${TRAVIS_TAG:1}@systelab/stable" -s build_type=${CONAN_BUILD_TYPE} -s compiler.libcxx=${COMPILER_LIBCXX} -o gtest=${GTEST_VERSION} -o openssl=${OPENSSL_VERSION}; 112 | else 113 | conan export-pkg ../conanfile.py "RESTAPICore/0.0.${TRAVIS_BUILD_NUMBER}@systelab/stable" -s build_type=${CONAN_BUILD_TYPE} -s compiler.libcxx=${COMPILER_LIBCXX} -o gtest=${GTEST_VERSION} -o openssl=${OPENSSL_VERSION}; 114 | fi 115 | - | 116 | if [ "$BUILD_TYPE" == "Release" ] || [ "$BUILD_TYPE" == "Debug" ]; then 117 | if [ -n "$TRAVIS_TAG" ]; then 118 | conan test ../test_package/conanfile.py RESTAPICore/${TRAVIS_TAG:1}@systelab/stable -s build_type=${CONAN_BUILD_TYPE} -s compiler.libcxx=${COMPILER_LIBCXX} -o gtest=${GTEST_VERSION} -o openssl=${OPENSSL_VERSION}; 119 | else 120 | conan test ../test_package/conanfile.py RESTAPICore/0.0.${TRAVIS_BUILD_NUMBER}@systelab/stable -s build_type=${CONAN_BUILD_TYPE} -s compiler.libcxx=${COMPILER_LIBCXX} -o gtest=${GTEST_VERSION} -o openssl=${OPENSSL_VERSION}; 121 | fi 122 | fi 123 | - | 124 | if [ -n "$TRAVIS_TAG" ]; then 125 | conan export-pkg ../test/RESTAPICoreTestUtilities/conanfile.py "RESTAPICoreTestUtilities/${TRAVIS_TAG:1}@systelab/stable" -s build_type=${CONAN_BUILD_TYPE} -s compiler.libcxx=${COMPILER_LIBCXX} -o gtest=${GTEST_VERSION} -o openssl=${OPENSSL_VERSION}; 126 | else 127 | conan export-pkg ../test/RESTAPICoreTestUtilities/conanfile.py "RESTAPICoreTestUtilities/0.0.${TRAVIS_BUILD_NUMBER}@systelab/stable" -s build_type=${CONAN_BUILD_TYPE} -s compiler.libcxx=${COMPILER_LIBCXX} -o gtest=${GTEST_VERSION} -o openssl=${OPENSSL_VERSION}; 128 | fi 129 | - | 130 | if [ "$BUILD_TYPE" == "Release" ] || [ "$BUILD_TYPE" == "Debug" ]; then 131 | if [ -n "$TRAVIS_TAG" ]; then 132 | conan test ../test/RESTAPICoreTestUtilities/test_package/conanfile.py RESTAPICoreTestUtilities/${TRAVIS_TAG:1}@systelab/stable -s build_type=${CONAN_BUILD_TYPE} -s compiler.libcxx=${COMPILER_LIBCXX} -o gtest=${GTEST_VERSION} -o openssl=${OPENSSL_VERSION}; 133 | else 134 | conan test ../test/RESTAPICoreTestUtilities/test_package/conanfile.py RESTAPICoreTestUtilities/0.0.${TRAVIS_BUILD_NUMBER}@systelab/stable -s build_type=${CONAN_BUILD_TYPE} -s compiler.libcxx=${COMPILER_LIBCXX} -o gtest=${GTEST_VERSION} -o openssl=${OPENSSL_VERSION}; 135 | fi 136 | fi 137 | - cd .. 138 | 139 | deploy: 140 | - provider: script 141 | script: bash ./ci/deploy.sh -v "${TRAVIS_TAG:1}" 142 | skip_cleanup: true 143 | on: 144 | tags: true 145 | condition: "$BUILD_TYPE != Coverage" 146 | - provider: script 147 | script: bash ./ci/build_docs.sh -o "systelab" -s "cpp-rest-api-core" -t "${TRAVIS_TAG}" -c "Travis" -n "${COMPILER_NAME} ${BUILD_TYPE}" -j "${TRAVIS_JOB_ID}" -p "RESTAPICoreTest" 148 | skip_cleanup: true 149 | on: 150 | tags: true 151 | 152 | after_success: 153 | - if [[ $TRAVIS_OS_NAME == linux && $BUILD_TYPE == Coverage ]]; then bash <(curl -s https://codecov.io/bash); fi 154 | -------------------------------------------------------------------------------- /BUILD.md: -------------------------------------------------------------------------------- 1 | # Build from sources 2 | 3 | ## Prerequisites: 4 | - [Git](https://git-scm.com/) 5 | - [Conan](https://conan.io/) 6 | - [CMake](https://cmake.org/) 7 | - [Visual Studio](https://visualstudio.microsoft.com/) (only on Windows) 8 | - [GCC](https://gcc.gnu.org/) (only on Linux) 9 | 10 | ## Build steps 11 | 12 | Build library with the following steps: 13 | 1. Clone this repository in a local drive 14 | 2. Make a build directory (i.e. `build/`) 15 | 3. Install `conan` dependencies in the build directory 16 | 4. Run `cmake` in the build directory to configure build targets 17 | 5. Use `Visual Studio` (on Windows) or `make` (on Linux) to build the library 18 | 19 | ### Windows 20 | 21 | In order to build the application on Windows for the `Release` configuration, run the following commands ($VSINSTALLPATH is the path where Visual Studio has been installed): 22 | 23 | ``` bash 24 | > git clone https://github.com/systelab/cpp-rest-api-core 25 | > md build && cd build 26 | > conan remote add systelab-public https://systelab.jfrog.io/artifactory/api/conan/cpp-conan-production-local 27 | > conan install . --profile=vs2022.conanprofile -s build_type=Release -s arch=x86_64 --install-folder build/Release_x64 28 | > conan build . --build-folder build/Release_x64 29 | ``` 30 | 31 | However, if you want to `Debug` the source code, you will need these commands: 32 | 33 | ``` bash 34 | > git clone https://github.com/systelab/cpp-rest-api-core 35 | > md build && cd build 36 | > conan remote add systelab-public https://systelab.jfrog.io/artifactory/api/conan/cpp-conan-production-local 37 | > conan install . --profile=vs2022.conanprofile -s build_type=Debug -s arch=x86_64 --install-folder build/Debug_x64 38 | > conan build . --build-folder build/Debug_x64 39 | ``` 40 | 41 | ### Linux 42 | 43 | ``` bash 44 | > git clone https://github.com/systelab/cpp-rest-api-core 45 | > mkdir build && cd build 46 | > conan remote add systelab-public https://systelab.jfrog.io/artifactory/api/conan/cpp-conan-production-local 47 | > conan install .. 48 | > cmake .. -DCMAKE_BUILD_TYPE=[Debug | Coverage | Release] 49 | > make 50 | ``` 51 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2) 2 | 3 | project(RESTAPICore) 4 | 5 | # Configure environment 6 | set(CMAKE_CXX_STANDARD 20) 7 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 8 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 9 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 10 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 11 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 12 | 13 | # Configure include directories 14 | include_directories(${CMAKE_SOURCE_DIR}/src) 15 | include_directories(${CMAKE_SOURCE_DIR}/test) 16 | 17 | # Add subprojects 18 | add_subdirectory(${CMAKE_SOURCE_DIR}/src/RESTAPICore) 19 | add_subdirectory(${CMAKE_SOURCE_DIR}/test/RESTAPICoreTestUtilities) 20 | add_subdirectory(${CMAKE_SOURCE_DIR}/test/RESTAPICoreTest) 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Systelab. A Werfen Company 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/systelab/cpp-rest-api-core.svg?branch=master)](https://travis-ci.org/systelab/cpp-rest-api-core) 2 | [![Build status](https://ci.appveyor.com/api/projects/status/ix9b122obt2f2j9v?svg=true)](https://ci.appveyor.com/project/systelab/cpp-rest-api-core) 3 | [![codecov](https://codecov.io/gh/systelab/cpp-rest-api-core/branch/master/graph/badge.svg)](https://codecov.io/gh/systelab/cpp-rest-api-core) 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/3c7b64648176416eaf66d78203c4a4d4)](https://www.codacy.com/gh/systelab/cpp-rest-api-core/dashboard?utm_source=github.com&utm_medium=referral&utm_content=systelab/cpp-rest-api-core&utm_campaign=Badge_Grade) 5 | 6 | 7 | # C++ REST API framework 8 | 9 | This repository implements a C++ framework to facilitate the creation of REST API. 10 | 11 | ## Supported features 12 | 13 | * Routing mechanism 14 | * Authorization through JWT 15 | * User access validation 16 | * Token expiration validation 17 | * Custom access validation 18 | 19 | 20 | ## Setup 21 | 22 | ### Download using Conan 23 | 24 | This library is designed to be installed by making use of [Conan](https://conan.io/) package manager. So, you just need to add the following requirement into your Conan recipe: 25 | 26 | ```python 27 | def requirements(self): 28 | self.requires("RESTAPICore/1.0.0@systelab/stable") 29 | ``` 30 | 31 | > Version number of this code snipped is set just as an example. Replace it for the desired package version to retrieve. 32 | 33 | As this package is not available on the conan-center, you will also need to configure a remote repository before installing dependencies: 34 | 35 | ```bash 36 | conan remote add systelab-public https://systelab.jfrog.io/artifactory/api/conan/cpp-conan-production-local 37 | ``` 38 | 39 | See Conan [documentation](https://docs.conan.io/en/latest/) for further details on how to integrate this package with your build system. 40 | 41 | ### Build from sources 42 | 43 | See [BUILD.md](BUILD.md) document for details. 44 | 45 | 46 | ## Usage 47 | 48 | ### Configure basic routing 49 | 50 | 1) Implement an endpoint by creating a class that inherits ```systelab::rest_api_core::IEndpoint``` interface: 51 | 52 | ```cpp 53 | #include "RESTAPICore/Endpoint/IEndpoint.h" 54 | 55 | class YourEndpoint : public systelab::rest_api_core::IEndpoint 56 | { 57 | public: 58 | YourEndpoint() = default; 59 | 60 | std::unique_ptr 61 | execute(const systelab::rest_api_core::EndpointRequestData&) override 62 | { 63 | // Process given systelab::rest_api_core::EndpointRequestData 64 | // and generate a systelab::web_server::Reply 65 | return reply; 66 | } 67 | }; 68 | ``` 69 | 70 | 71 | 2) Create a web service that sets up a router with a single route registered: 72 | 73 | ```cpp 74 | #include "RESTAPICore/Router/Router.h" 75 | #include "RESTAPICore/Router/RoutesFactory.h" 76 | 77 | class RESTAPIWebService : public systelab::web_server::IWebService 78 | { 79 | public: 80 | RESTAPIWebService() 81 | { 82 | std::string jwtKey = "HereGoesYourJWTSecretKey"; 83 | auto routesFactory = std::make_unique(jwtKey); 84 | 85 | m_router = std::make_unique(); 86 | m_router->addRoute(routesFactory.buildRoute("GET", "/rest/api/yourendpoint", {}, 87 | []() { return std::make_unique() }) ); 88 | // Register more routes here 89 | } 90 | 91 | std::unique_ptr 92 | process(const systelab::web_server::Request& request) const override 93 | { 94 | return m_router->process(request); 95 | } 96 | 97 | private: 98 | std::unique_ptr m_router; 99 | }; 100 | ``` 101 | 102 | > Thus, when the web service receives a GET HTTP request with "/rest/api/yourendpoint" URI, it redirects this request to the ```YourEndpoint```class implemented previously. 103 | 104 | 105 | 3) Register additional routes to other endpoints: 106 | 107 | ```cpp 108 | router->addRoute(routesFactory.buildRoute("POST", "/rest/api/yourendpoint", {}, 109 | []() { return std::make_unique() }); 110 | router->addRoute(routesFactory.buildRoute("PUT", "/rest/api/yourendpoint", {}, 111 | []() { return std::make_unique() }); 112 | router->addRoute(routesFactory.buildRoute("DELETE", "/rest/api/yourendpoint", {}, 113 | []() { return std::make_unique() }); 114 | router->addRoute(routesFactory.buildRoute("GET", "/rest/api/anotherendpoint", {}, 115 | []() { return std::make_unique() }); 116 | ``` 117 | 118 | 119 | ### Routes with parameters 120 | 121 | Routes with parameters can be registered by using an specific syntax on associated URIs: 122 | 123 | ```cpp 124 | // Route with an string parameter named 'id' 125 | router->addRoute(routesFactory.buildRoute("GET", "/rest/api/yourendpoint/:id", {}, 126 | []() { return std::make_unique() }); 127 | 128 | // Route with a numeric parameter named 'number' 129 | router->addRoute(routesFactory.buildRoute("GET", "/rest/api/anotherendpoint/+number", {}, 130 | []() { return std::make_unique() }); 131 | 132 | // Route with multiple parameters 133 | router->addRoute(routesFactory.buildRoute("GET", "/rest/api/yourendpoint/+id1/:id2", {}, 134 | []() { return std::make_unique() }); 135 | ``` 136 | 137 | When a request matches a registered route with parameters, the associated endpoint is called with a `systelab::rest_api_core::EndpointRequestData` object that contains these parameters properly parsed for easy usage: 138 | 139 | ```cpp 140 | std::unique_ptr 141 | YourGetMultipleParamsEndpoint::execute(const systelab::rest_api_core::EndpointRequestData& requestData) 142 | { 143 | unsigned int id1 = requestData.getParameters().getNumericParameter("id1"); 144 | std::string id2 = requestData.getParameters().getStringParameter("id2"); 145 | ... 146 | } 147 | ``` 148 | 149 | 150 | ### Authorization through JWT 151 | 152 | When working with a [Bearer Authentication](https://swagger.io/docs/specification/authentication/bearer-authentication/) scheme, the library automatically decodes the tokens contained on `Authorization` header of HTTP requests. It uses the key provided on the `RoutesFactory` constructor to verify that token signature is correct. Then, claims contained on decoded [JSON Web Tokens](https://jwt.io/) are provided to the matching endpoint as part of the `systelab::rest_api_core::EndpointRequestData` object: 153 | 154 | ```cpp 155 | std::unique_ptr 156 | YourEndpoint::execute(const systelab::rest_api_core::EndpointRequestData& requestData) 157 | { 158 | auto& authorizationClaims = requestData.getAuthorizationClaims(); 159 | std::vector claimNames = authorizationClaims.getClaimNames(); 160 | for (auto name : claimNames) 161 | { 162 | std::string claimValue = authorizationClaims.getClaim(name); 163 | ... 164 | } 165 | ... 166 | } 167 | ``` 168 | 169 | 170 | ### Routes with user role access validation 171 | 172 | Routes can be configured to allow access only to users that have a certain role. This can be achieved through *route access validators*, a middleware that is executed after matching a request with a route, but before dispatching it to the endpoint. So, user role validation can be set up as follows: 173 | 174 | 1) Create a class that implements `IUserRoleService` interface. The method `getUserRoles()` should return the names of the role(s) associated to the user with the given username according to application bussiness logic. 175 | 176 | ```cpp 177 | #include "RESTAPICore/RouteAccess/IUserRoleService.h" 178 | 179 | class YourUserRoleService : public systelab::rest_api_core::IUserRoleService 180 | { 181 | public: 182 | YourUserRoleService() = default; 183 | 184 | std::vector getUserRoles(const std::string& username) override 185 | { 186 | ... 187 | } 188 | }; 189 | 190 | ``` 191 | 192 | 2) Add an instance of the implemented user role service into the REST API web service: 193 | 194 | ```cpp 195 | class RESTAPIWebService : public systelab::web_server::IWebService 196 | { 197 | public: 198 | ... 199 | 200 | private: 201 | ... 202 | YourUserRoleService m_userRoleService; 203 | }; 204 | ``` 205 | 206 | 3) Use the built-in `UserRoleRouteAccessValidator` to register routes that only allow access to users of a certain role: 207 | 208 | ```cpp 209 | #include "RESTAPICore/RouteAccess/UserRoleRouteAccessValidator.h" 210 | 211 | // Route that only allows access to 'Admin' users 212 | m_router->addRoute(routesFactory.buildRoute("GET", "/rest/api/yourendpoint", 213 | { [this](){ return std::make_unique({"Admin"}, m_userRoleService) } }, 214 | []() { return std::make_unique() }) ); 215 | 216 | // Route that allows access to 'Admin' and 'Basic' users 217 | m_router->addRoute(routesFactory.buildRoute("GET", "/rest/api/anotherendpoint", 218 | { [this](){ return std::make_unique({"Admin", "Basic"}, m_userRoleService) } }, 219 | []() { return std::make_unique() }) ); 220 | ``` 221 | 222 | > When the web service receives an HTTP request that matches any of these routes, before redirecting the request to the endpoint, it will check if the request has an authorization claim for the subject ("sub"). If so, this claim will be used to retrieve the associated user roles and then, these roles will be compared against the allowed ones for the route. If user is allowed, then the request will be dispatched to the endpoint. Otherwise, a forbidden reply will be returned. 223 | 224 | 225 | ### Routes with token expiration validation 226 | 227 | Another built-in route access validator (class `TokenExpirationAccessValidator`) can be used to check if the token contained on the `Authorization` header of the requests has expired or not. This verification is based on the "Issued at" (iat) authorization claim and the current time. Thus, when current time is greater than iat plus a configured expiration time (in seconds), a forbidden reply will be returned. 228 | 229 | ```cpp 230 | #include "RESTAPICore/RouteAccess/TokenExpirationAccessValidator.h" 231 | 232 | // Route that does not allow using tokens generated more than 10 min (600 seconds) ago 233 | m_router->addRoute(routesFactory.buildRoute("GET", "/rest/api/yourendpoint", 234 | { [this](){ return std::make_unique(m_timeAdapter, 600) } }, 235 | []() { return std::make_unique() }) ); 236 | ``` 237 | 238 | > The `m_timeAdapter` member is an instance of the `systelab::time::ITimeAdapter` class, which provides a method to query the current time of the system. See documentation of [cpp-time-adapter](https://github.com/systelab/cpp-time-adapter) repository for further details about it. 239 | 240 | 241 | ### Custom access validation 242 | 243 | This library can be extended by implementing any other kind of route access validation through the `IRouteAccessValidator` interface. The `hasAccess()` method can make use of any data contained on the `EndpointRequestData` argument to determine if the request has access to the route or not and return a boolean accordingly. 244 | 245 | ```cpp 246 | #include "RESTAPICore/RouteAccess/IRouteAccessValidator.h" 247 | 248 | class MyCustomRouteAccessValidator : public systelab::rest_api_core::IRouteAccessValidator 249 | { 250 | MyCustomRouteAccessValidator(... ) // Inject any object required 251 | 252 | bool hasAccess(EndpointRequestData&) const override 253 | { 254 | // Use EndpointRequestData or injected objects to return if request has access to the route 255 | } 256 | }; 257 | ``` 258 | -------------------------------------------------------------------------------- /RELEASENOTES.md: -------------------------------------------------------------------------------- 1 | # Summary of changes 2 | 3 | ## Changes for version 1.1.9 (8 Feb 2022) 4 | 5 | ### Bug Fixes 6 | 7 | - Moved to version 2.0.1 of WebServerAdapter 8 | 9 | 10 | ## Changes for version 1.1.8 (23 Dec 2021) 11 | 12 | ### Bug Fixes 13 | 14 | - Moved to version 2.0.0 of WebServerAdapter 15 | 16 | 17 | ## Changes for version 1.1.7 (30 Sep 2021) 18 | 19 | ### Bug Fixes 20 | 21 | - Moved continuous integration to Jenkins 22 | - Deploy conan package into remote specific for C++ task force 23 | - Added configurations for the Newton and Snow projects into continuous integration 24 | - Fixed use of "export_sources" definition in conan recipes 25 | 26 | 27 | ## Changes for version 1.1.6 (26 Feb 2021) 28 | 29 | ### Bug Fixes 30 | 31 | - Updated continuous integration to: 32 | - Use GitHub Action to generate library documentation 33 | - Upload generated packages to Systelab's public Artifactory 34 | 35 | 36 | ## Changes for version 1.1.5 (7 Sep 2020) 37 | 38 | ### Bug Fixes 39 | 40 | - Moved JWTUtils to version 1.1.4 41 | 42 | 43 | ## Changes for version 1.1.4 (4 Sep 2020) 44 | 45 | ### Bug Fixes 46 | 47 | - Moved to gtest from conan-center (without username/channel) for test utilities 48 | 49 | 50 | ## Changes for version 1.1.3 (4 Sep 2020) 51 | 52 | ### Bug Fixes 53 | 54 | - Moved to version 1.1.7 of WebServerAdapter 55 | 56 | 57 | ## Changes for version 1.1.2 (31 Aug 2020) 58 | 59 | ### Bug Fixes 60 | 61 | - Moved JWTUtils to version 1.1.3 (to fix memory leak on RapidJSONAdapter) 62 | 63 | 64 | ## Changes for version 1.1.1 (4 Jun 2020) 65 | 66 | ### Bug Fixes 67 | 68 | - Moved to gtest from conan-center (without username/channel) 69 | 70 | 71 | ## Changes for version 1.1.0 (23 May 2020) 72 | 73 | ### Enhancements 74 | 75 | - Added support for OpenSSL 1.1.1 76 | 77 | 78 | ## Changes for version 1.0.12 (21 May 2020) 79 | 80 | ### Bug Fixes 81 | 82 | - Moved web server adapter to version 1.1.3 83 | 84 | 85 | ## Changes for version 1.0.11 (18 Apr 2020) 86 | 87 | ### Bug Fixes 88 | 89 | - Updated to version 1.0.8 of JWTUtilts 90 | 91 | 92 | ## Changes for version 1.0.10 (13 Apr 2020) 93 | 94 | ### Bug Fixes 95 | 96 | - Allow configuring data of unauthorized and forbidden replies. 97 | 98 | 99 | ## Changes for version 1.0.9 (9 Apr 2020) 100 | 101 | ### Bug Fixes 102 | 103 | - Used time adapter to check if tokens are expired when validating routes 104 | 105 | 106 | ## Changes for version 1.0.8 (7 Apr 2020) 107 | 108 | ### Bug Fixes 109 | 110 | - Moved JWTUtils to version 1.0.7 111 | 112 | 113 | ## Changes for version 1.0.7 (7 Feb 2020) 114 | 115 | ### Bug Fixes 116 | 117 | - Added support for GTest 1.10.0 118 | - Added support for Visual Studio 2019 119 | 120 | 121 | ## Changes for version 1.0.6 (16 Jan 2020) 122 | 123 | ### Bug Fixes 124 | 125 | - Moved to version 1.1.0 of WebServerAdapter 126 | 127 | 128 | ## Changes for version 1.0.5 (13 Jan 2020) 129 | 130 | ### Bug Fixes 131 | 132 | - Moved to version 1.0.4 of WebServerAdapter 133 | 134 | 135 | ## Changes for version 1.0.4 (9 Dec 2019) 136 | 137 | ### Bug Fixes 138 | 139 | - Fixed Conan recipe to allow building from sources 140 | 141 | 142 | ## Changes for version 1.0.3 (17 Nov 2019) 143 | 144 | ### Bug Fixes 145 | 146 | - Moved version of JWTUtils to version 1.0.4 147 | 148 | 149 | ## Changes for version 1.0.2 (25 Oct 2019) 150 | 151 | ### Bug Fixes 152 | 153 | - Retrieve all external dependencies from Conan instead of compiling them 154 | - Defined used test packages as build requirements 155 | - Created a separated package for test utilities 156 | 157 | 158 | ## Changes for version 1.0.1 (26 Sep 2019) 159 | 160 | ### Bug Fixes 161 | 162 | - Moved JWTUtils dependency to version 1.0.2 163 | 164 | 165 | ## Changes for version 1.0.0 (16 Aug 2019) 166 | 167 | ### Enhancements 168 | 169 | - Initial version in GitHub 170 | -------------------------------------------------------------------------------- /ci/pipelines/Build.jenkinsfile: -------------------------------------------------------------------------------- 1 | def channel = "testing" 2 | def version = "0.0.0" 3 | def profile = "vs2022.conanprofile" 4 | def packageNameInterface = "RESTAPICore" 5 | def packageNameTestUtilities = "RESTAPICoreTestUtilities" 6 | def archs = ['x86', 'x86_64'] 7 | def configs = ['Debug', 'Release'] 8 | 9 | library identifier: "jenkins-pipeline-utils@1.0.11", 10 | retriever: modernSCM([ 11 | $class: "GitSCMSource", 12 | remote: "https://bitbucket.org/Systelab/jenkins-pipeline-utils.git", 13 | credentialsId: "BitbucketWerfen" 14 | ]) 15 | 16 | pipeline 17 | { 18 | agent 19 | { 20 | label 'lib-build' 21 | } 22 | 23 | parameters 24 | { 25 | string( name: 'tag', 26 | description: 'Version to build (must match the version of the tag that will be checked out), leave blank for checkout of current branch', 27 | defaultValue: '', 28 | trim: true) 29 | 30 | booleanParam( name: 'stable', 31 | description: 'Show if generated library should be uploaded as stable or testing', 32 | defaultValue: false ) 33 | 34 | booleanParam( name: 'uploadPackage', 35 | description: 'Whether or not to upload conan package', 36 | defaultValue: false ) 37 | } 38 | 39 | options 40 | { 41 | skipDefaultCheckout(true) 42 | disableConcurrentBuilds() 43 | buildDiscarder(logRotator(numToKeepStr: '5')) 44 | } 45 | 46 | stages 47 | { 48 | stage('Checkout') 49 | { 50 | steps 51 | { 52 | cleanupWorkspace() 53 | script 54 | { 55 | if (params.tag == '') 56 | { 57 | checkoutFromGIT() 58 | } 59 | else 60 | { 61 | version = params.tag 62 | checkoutFromGIT(scm.userRemoteConfigs[0].url, "v${version}", true) 63 | } 64 | channel = params.stable ? "stable" : "testing" 65 | } 66 | } 67 | } 68 | 69 | stage('Build') 70 | { 71 | steps 72 | { 73 | script 74 | { 75 | archs.each 76 | { arch -> 77 | configs.each 78 | { config -> 79 | stage("Build ${config}|${arch}") 80 | { 81 | def buildFolder = "build/${config}-${arch}" 82 | bat script: "conan install . --install-folder ${buildFolder} --profile=${profile} -s arch=${arch} -s build_type=${config}", label: 'Installing dependencies' 83 | bat script: "conan build . --build-folder ${buildFolder}", label: 'Building' 84 | bat script: "conan export-pkg . ${packageNameInterface}/${version}@systelab/${channel} --build-folder ${buildFolder} --force", label: "Exporting package ${packageNameInterface}/${version}@systelab/${channel}" 85 | bat script: "conan export-pkg ./test/${packageNameTestUtilities}/conanfile.py ${packageNameTestUtilities}/${version}@systelab/${channel} --build-folder ${buildFolder} --force", label: "Exporting package ${packageNameTestUtilities}/${version}@systelab/${channel}" 86 | dir("${buildFolder}/bin/${config}") 87 | { 88 | bat "RESTAPICoreTest.exe --gtest_output=xml:RESTAPICoreTest.xml" 89 | } 90 | } 91 | } 92 | } 93 | } 94 | } 95 | } 96 | 97 | stage('Test Packages') 98 | { 99 | steps 100 | { 101 | script 102 | { 103 | archs.each 104 | { arch -> 105 | configs.each 106 | { config -> 107 | bat script: "conan test test_package/conanfile.py ${packageNameInterface}/${version}@systelab/${channel} --profile=${profile} -s arch=${arch} -s build_type=${config}", label: "Testing test_package for ${packageNameInterface}" 108 | bat script: "conan test test/${packageNameTestUtilities}/test_package/conanfile.py ${packageNameTestUtilities}/${version}@systelab/${channel} --profile=${profile} -s arch=${arch} -s build_type=${config}", label: "Testing test_package for ${packageNameTestUtilities}" 109 | } 110 | } 111 | } 112 | } 113 | } 114 | 115 | stage('Deploy') 116 | { 117 | when 118 | { 119 | expression { params.uploadPackage } 120 | } 121 | steps 122 | { 123 | 124 | bat script: "conan upload ${packageNameInterface}/${version}@systelab/${channel} --all -r systelab-conan-local --force", label: "Uploading ${packageNameInterface} package to Artifactory" 125 | bat script: "conan upload ${packageNameTestUtilities}/${version}@systelab/${channel} --all -r systelab-conan-local --force", label: "Uploading ${packageNameTestUtilities} package to Artifactory" 126 | } 127 | } 128 | } 129 | 130 | post 131 | { 132 | always 133 | { 134 | junit allowEmptyResults: true, testResults: "build/**/RESTAPICoreTest.xml" 135 | } 136 | } 137 | } -------------------------------------------------------------------------------- /conanfile.py: -------------------------------------------------------------------------------- 1 | from conans import ConanFile, CMake, tools 2 | 3 | class RESTAPICoreConan(ConanFile): 4 | name = "RESTAPICore" 5 | description = "C++ REST API framework" 6 | url = "https://github.com/systelab/cpp-rest-api-core" 7 | homepage = "https://github.com/systelab/cpp-rest-api-core" 8 | author = "CSW " 9 | topics = ("conan", "rest", "api", "framework") 10 | license = "MIT" 11 | generators = "cmake_find_package" 12 | settings = "os", "compiler", "build_type", "arch" 13 | exports_sources = "*", "!test/RESTAPICoreTestUtilities", "!build*", "!*.yml", "!*.md", "!*.in", "!ci", "!.gitattributes", "!.gitignore", "!LICENSE" 14 | 15 | def requirements(self): 16 | self.requires("WebServerAdapterInterface/2.0.2@systelab/stable") 17 | self.requires("JWTUtils/1.2.1@systelab/stable") 18 | self.requires("TimeAdapter/1.0.6@systelab/stable") 19 | 20 | self.requires("gtest/1.14.0#4372c5aed2b4018ed9f9da3e218d18b3", override=True) # Hack 21 | self.requires("RapidJSONAdapter/1.1.7@systelab/stable", override=True) 22 | self.requires("TestUtilitiesInterface/1.0.8@systelab/stable", private=True) 23 | self.requires("WebServerAdapterTestUtilities/2.0.2@systelab/stable", private=True) 24 | self.requires("JSONAdapterTestUtilities/1.1.6@systelab/stable", private=True) 25 | self.requires("TimeAdapterTestUtilities/1.0.6@systelab/stable", private=True) 26 | 27 | def build(self): 28 | cmake = CMake(self) 29 | cmake.configure() 30 | cmake.build() 31 | 32 | def imports(self): 33 | self.copy("*.dll", dst=("bin/%s" % self.settings.build_type), src="bin") 34 | self.copy("*.dylib*", dst=("bin/%s" % self.settings.build_type), src="lib") 35 | self.copy("*.so*", dst=("bin/%s" % self.settings.build_type), src="lib") 36 | 37 | def package(self): 38 | self.copy("*.h", dst="include/RESTAPICore", src="src/RESTAPICore") 39 | self.copy("*RESTAPICore.lib", dst="lib", keep_path=False) 40 | self.copy("*RESTAPICore.pdb", dst="lib", keep_path=False) 41 | self.copy("*RESTAPICore.a", dst="lib", keep_path=False) 42 | 43 | def package_info(self): 44 | self.cpp_info.libs = tools.collect_libs(self) 45 | -------------------------------------------------------------------------------- /src/RESTAPICore/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2) 2 | 3 | # Find external dependencides 4 | find_package(WebServerAdapterInterface) 5 | find_package(JWTUtils) 6 | find_package(TimeAdapter) 7 | 8 | # Add project folder into includes 9 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 10 | 11 | # Configure RESTAPICore static library 12 | set(REST_API_CORE RESTAPICore) 13 | file(GLOB_RECURSE REST_API_CORE_SRC "*.cpp") 14 | file(GLOB_RECURSE REST_API_CORE_HDR "*.h") 15 | add_library(${REST_API_CORE} STATIC ${REST_API_CORE_SRC} ${REST_API_CORE_HDR}) 16 | target_link_libraries(${REST_API_CORE} WebServerAdapterInterface::WebServerAdapterInterface 17 | JWTUtils::JWTUtils 18 | TimeAdapter::TimeAdapter) 19 | 20 | #Configure source groups 21 | foreach(FILE ${REST_API_CORE_SRC} ${REST_API_CORE_HDR}) 22 | get_filename_component(PARENT_DIR "${FILE}" DIRECTORY) 23 | string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}" "" GROUP "${PARENT_DIR}") 24 | string(REPLACE "/" "\\" GROUP "${GROUP}") 25 | 26 | if ("${FILE}" MATCHES ".*\\.cpp") 27 | set(GROUP "Source Files${GROUP}") 28 | elseif("${FILE}" MATCHES ".*\\.h") 29 | set(GROUP "Header Files${GROUP}") 30 | endif() 31 | 32 | source_group("${GROUP}" FILES "${FILE}") 33 | endforeach() 34 | 35 | -------------------------------------------------------------------------------- /src/RESTAPICore/Endpoint/ClaimConstants.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | namespace systelab { namespace rest_api_core { namespace claim { 5 | 6 | static const std::string SUBJECT = "sub"; 7 | static const std::string ISSUED_AT = "iat"; 8 | 9 | }}} 10 | 11 | -------------------------------------------------------------------------------- /src/RESTAPICore/Endpoint/EndpointRequestAuthorizationClaims.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "EndpointRequestAuthorizationClaims.h" 3 | 4 | 5 | namespace systelab { namespace rest_api_core { 6 | 7 | EndpointRequestAuthorizationClaims::EndpointRequestAuthorizationClaims() 8 | :m_claims() 9 | { 10 | } 11 | 12 | EndpointRequestAuthorizationClaims::EndpointRequestAuthorizationClaims(const std::vector< std::pair >& claims) 13 | :m_claims() 14 | { 15 | for (auto claim : claims) 16 | { 17 | m_claims.insert(claim); 18 | } 19 | } 20 | 21 | EndpointRequestAuthorizationClaims::EndpointRequestAuthorizationClaims(const EndpointRequestAuthorizationClaims& other) 22 | :m_claims(other.m_claims) 23 | { 24 | } 25 | 26 | EndpointRequestAuthorizationClaims::~EndpointRequestAuthorizationClaims() = default; 27 | 28 | unsigned int EndpointRequestAuthorizationClaims::getClaimCount() const 29 | { 30 | return (unsigned int) m_claims.size(); 31 | } 32 | 33 | std::vector EndpointRequestAuthorizationClaims::getClaimNames() const 34 | { 35 | std::vector names; 36 | for (auto claim : m_claims) 37 | { 38 | names.push_back(claim.first); 39 | } 40 | 41 | return names; 42 | } 43 | 44 | bool EndpointRequestAuthorizationClaims::hasClaim(const std::string& name) const 45 | { 46 | return (m_claims.find(name) != m_claims.end()); 47 | } 48 | 49 | std::string EndpointRequestAuthorizationClaims::getClaim(const std::string& name) const 50 | { 51 | auto it = m_claims.find(name); 52 | if (it != m_claims.end()) 53 | { 54 | return it->second; 55 | } 56 | else 57 | { 58 | throw std::runtime_error("Claim '" + name + "' not found"); 59 | } 60 | } 61 | 62 | void EndpointRequestAuthorizationClaims::addClaim(const std::string& name, const std::string& value) 63 | { 64 | m_claims.insert(std::make_pair(name, value)); 65 | } 66 | 67 | EndpointRequestAuthorizationClaims& EndpointRequestAuthorizationClaims::operator=(const EndpointRequestAuthorizationClaims& other) 68 | { 69 | m_claims = other.m_claims; 70 | return *this; 71 | } 72 | 73 | bool operator== (const EndpointRequestAuthorizationClaims& lhs, const EndpointRequestAuthorizationClaims& rhs) 74 | { 75 | if (lhs.m_claims != rhs.m_claims) 76 | { 77 | return false; 78 | } 79 | 80 | return true; 81 | } 82 | 83 | bool operator!= (const EndpointRequestAuthorizationClaims& lhs, const EndpointRequestAuthorizationClaims& rhs) 84 | { 85 | return !(lhs == rhs); 86 | } 87 | 88 | }} 89 | 90 | -------------------------------------------------------------------------------- /src/RESTAPICore/Endpoint/EndpointRequestAuthorizationClaims.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | namespace systelab { namespace rest_api_core { 5 | 6 | class EndpointRequestAuthorizationClaims 7 | { 8 | public: 9 | EndpointRequestAuthorizationClaims(); 10 | EndpointRequestAuthorizationClaims(const EndpointRequestAuthorizationClaims&); 11 | EndpointRequestAuthorizationClaims(const std::vector< std::pair >&); 12 | virtual ~EndpointRequestAuthorizationClaims(); 13 | 14 | unsigned int getClaimCount() const; 15 | std::vector getClaimNames() const; 16 | bool hasClaim(const std::string& name) const; 17 | std::string getClaim(const std::string& name) const; 18 | 19 | void addClaim(const std::string& name, const std::string& value); 20 | 21 | EndpointRequestAuthorizationClaims& operator= (const EndpointRequestAuthorizationClaims&); 22 | friend bool operator== (const EndpointRequestAuthorizationClaims&, const EndpointRequestAuthorizationClaims&); 23 | friend bool operator!= (const EndpointRequestAuthorizationClaims&, const EndpointRequestAuthorizationClaims&); 24 | 25 | private: 26 | std::map m_claims; 27 | }; 28 | 29 | }} 30 | 31 | -------------------------------------------------------------------------------- /src/RESTAPICore/Endpoint/EndpointRequestData.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "EndpointRequestData.h" 3 | 4 | 5 | namespace systelab { namespace rest_api_core { 6 | 7 | EndpointRequestData::EndpointRequestData() 8 | :m_parameters() 9 | ,m_content("") 10 | ,m_headers() 11 | ,m_queryStrings() 12 | ,m_authorizationClaims() 13 | { 14 | } 15 | 16 | EndpointRequestData::EndpointRequestData(const EndpointRequestParams& parameters, 17 | const std::string& content, 18 | const systelab::web_server::RequestHeaders& headers, 19 | const systelab::web_server::RequestQueryStrings& queryStrings, 20 | const EndpointRequestAuthorizationClaims& authorizationClaims) 21 | :m_parameters(parameters) 22 | ,m_content(content) 23 | ,m_headers(headers) 24 | ,m_queryStrings(queryStrings) 25 | ,m_authorizationClaims(authorizationClaims) 26 | { 27 | } 28 | 29 | EndpointRequestData::EndpointRequestData(const EndpointRequestData& other) 30 | :m_parameters(other.m_parameters) 31 | ,m_content(other.m_content) 32 | ,m_headers(other.m_headers) 33 | ,m_queryStrings(other.m_queryStrings) 34 | ,m_authorizationClaims(other.m_authorizationClaims) 35 | { 36 | } 37 | 38 | EndpointRequestData::~EndpointRequestData() = default; 39 | 40 | const EndpointRequestParams& EndpointRequestData::getParameters() const 41 | { 42 | return m_parameters; 43 | } 44 | 45 | const std::string& EndpointRequestData::getContent() const 46 | { 47 | return m_content; 48 | } 49 | 50 | const systelab::web_server::RequestHeaders& EndpointRequestData::getHeaders() const 51 | { 52 | return m_headers; 53 | } 54 | 55 | const systelab::web_server::RequestQueryStrings& EndpointRequestData::getQueryStrings() const 56 | { 57 | return m_queryStrings; 58 | } 59 | 60 | const EndpointRequestAuthorizationClaims& EndpointRequestData::getAuthorizationClaims() const 61 | { 62 | return m_authorizationClaims; 63 | } 64 | 65 | void EndpointRequestData::setParameters(const EndpointRequestParams& parameters) 66 | { 67 | m_parameters = parameters; 68 | } 69 | 70 | void EndpointRequestData::setContent(const std::string& content) 71 | { 72 | m_content = content; 73 | } 74 | 75 | void EndpointRequestData::setHeaders(const systelab::web_server::RequestHeaders& headers) 76 | { 77 | m_headers = headers; 78 | } 79 | 80 | void EndpointRequestData::setQueryStrings(const systelab::web_server::RequestQueryStrings& queryStrings) 81 | { 82 | m_queryStrings = queryStrings; 83 | } 84 | 85 | void EndpointRequestData::setAuthorizationClaims(const EndpointRequestAuthorizationClaims& authorizationClaims) 86 | { 87 | m_authorizationClaims = authorizationClaims; 88 | } 89 | 90 | }} 91 | 92 | -------------------------------------------------------------------------------- /src/RESTAPICore/Endpoint/EndpointRequestData.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RESTAPICore/Endpoint/EndpointRequestAuthorizationClaims.h" 4 | #include "RESTAPICore/Endpoint/EndpointRequestParams.h" 5 | 6 | #include "WebServerAdapterInterface/Model/RequestHeaders.h" 7 | #include "WebServerAdapterInterface/Model/RequestQueryStrings.h" 8 | 9 | 10 | namespace systelab { namespace rest_api_core { 11 | 12 | class EndpointRequestData 13 | { 14 | public: 15 | EndpointRequestData(); 16 | EndpointRequestData(const EndpointRequestParams&, 17 | const std::string&, 18 | const systelab::web_server::RequestHeaders&, 19 | const systelab::web_server::RequestQueryStrings&, 20 | const EndpointRequestAuthorizationClaims&); 21 | EndpointRequestData(const EndpointRequestData&); 22 | virtual ~EndpointRequestData(); 23 | 24 | const EndpointRequestParams& getParameters() const; 25 | const std::string& getContent() const; 26 | const systelab::web_server::RequestHeaders& getHeaders() const; 27 | const systelab::web_server::RequestQueryStrings& getQueryStrings() const; 28 | const EndpointRequestAuthorizationClaims& getAuthorizationClaims() const; 29 | 30 | void setParameters(const EndpointRequestParams&); 31 | void setContent(const std::string&); 32 | void setHeaders(const systelab::web_server::RequestHeaders&); 33 | void setQueryStrings(const systelab::web_server::RequestQueryStrings&); 34 | void setAuthorizationClaims(const EndpointRequestAuthorizationClaims&); 35 | 36 | private: 37 | EndpointRequestParams m_parameters; 38 | std::string m_content; 39 | systelab::web_server::RequestHeaders m_headers; 40 | systelab::web_server::RequestQueryStrings m_queryStrings; 41 | EndpointRequestAuthorizationClaims m_authorizationClaims; 42 | }; 43 | 44 | }} 45 | 46 | -------------------------------------------------------------------------------- /src/RESTAPICore/Endpoint/EndpointRequestParams.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "EndpointRequestParams.h" 3 | 4 | 5 | namespace systelab { namespace rest_api_core { 6 | 7 | EndpointRequestParams::EndpointRequestParams() 8 | :m_stringParameters() 9 | ,m_numericParameters() 10 | { 11 | } 12 | 13 | EndpointRequestParams::EndpointRequestParams(const EndpointRequestParams& other) 14 | :m_stringParameters(other.m_stringParameters) 15 | ,m_numericParameters(other.m_numericParameters) 16 | { 17 | } 18 | 19 | EndpointRequestParams::EndpointRequestParams(const std::map& stringParameters, 20 | const std::map& numericParameters) 21 | :m_stringParameters(stringParameters) 22 | ,m_numericParameters(numericParameters) 23 | { 24 | } 25 | 26 | EndpointRequestParams::~EndpointRequestParams() = default; 27 | 28 | 29 | // String parameters 30 | unsigned int EndpointRequestParams::getStringParameterCount() const 31 | { 32 | return (unsigned int) m_stringParameters.size(); 33 | } 34 | 35 | std::vector EndpointRequestParams::getStringParameterNames() const 36 | { 37 | std::vector names; 38 | for (auto parameter : m_stringParameters) 39 | { 40 | names.push_back(parameter.first); 41 | } 42 | 43 | return names; 44 | } 45 | 46 | bool EndpointRequestParams::hasStringParameter(const std::string& name) const 47 | { 48 | return (m_stringParameters.find(name) != m_stringParameters.end()); 49 | } 50 | 51 | std::string EndpointRequestParams::getStringParameter(const std::string& name) const 52 | { 53 | auto it = m_stringParameters.find(name); 54 | if (it != m_stringParameters.end()) 55 | { 56 | return it->second; 57 | } 58 | else 59 | { 60 | throw std::runtime_error("String parameter '" + name + "' not found"); 61 | } 62 | } 63 | 64 | void EndpointRequestParams::addStringParameter(const std::string& name, const std::string& value) 65 | { 66 | m_stringParameters.insert({ name, value }); 67 | } 68 | 69 | 70 | // Numeric parameters 71 | unsigned int EndpointRequestParams::getNumericParameterCount() const 72 | { 73 | return (unsigned int) m_numericParameters.size(); 74 | } 75 | 76 | std::vector EndpointRequestParams::getNumericParameterNames() const 77 | { 78 | std::vector names; 79 | for (auto parameter : m_numericParameters) 80 | { 81 | names.push_back(parameter.first); 82 | } 83 | 84 | return names; 85 | } 86 | 87 | bool EndpointRequestParams::hasNumericParameter(const std::string& name) const 88 | { 89 | return (m_numericParameters.find(name) != m_numericParameters.end()); 90 | } 91 | 92 | unsigned int EndpointRequestParams::getNumericParameter(const std::string& name) const 93 | { 94 | auto it = m_numericParameters.find(name); 95 | if (it != m_numericParameters.end()) 96 | { 97 | return it->second; 98 | } 99 | else 100 | { 101 | throw std::runtime_error("Numeric parameter '" + name + "' not found"); 102 | } 103 | } 104 | 105 | void EndpointRequestParams::addNumericParameter(const std::string& name, unsigned int value) 106 | { 107 | m_numericParameters.insert({ name, value }); 108 | } 109 | 110 | EndpointRequestParams& EndpointRequestParams::operator=(const EndpointRequestParams& other) 111 | { 112 | m_stringParameters = other.m_stringParameters; 113 | m_numericParameters = other.m_numericParameters; 114 | 115 | return *this; 116 | } 117 | 118 | bool operator== (const EndpointRequestParams& lhs, const EndpointRequestParams& rhs) 119 | { 120 | if ((lhs.m_stringParameters != rhs.m_stringParameters) || 121 | (lhs.m_numericParameters != rhs.m_numericParameters)) 122 | { 123 | return false; 124 | } 125 | 126 | return true; 127 | } 128 | 129 | bool operator!= (const EndpointRequestParams& lhs, const EndpointRequestParams& rhs) 130 | { 131 | return !(lhs == rhs); 132 | } 133 | 134 | }} 135 | 136 | -------------------------------------------------------------------------------- /src/RESTAPICore/Endpoint/EndpointRequestParams.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | namespace systelab { namespace rest_api_core { 5 | 6 | class EndpointRequestParams 7 | { 8 | public: 9 | EndpointRequestParams(); 10 | EndpointRequestParams(const EndpointRequestParams&); 11 | EndpointRequestParams(const std::map&, 12 | const std::map&); 13 | virtual ~EndpointRequestParams(); 14 | 15 | // String parameters 16 | unsigned int getStringParameterCount() const; 17 | std::vector getStringParameterNames() const; 18 | bool hasStringParameter(const std::string& name) const; 19 | std::string getStringParameter(const std::string& name) const; 20 | void addStringParameter(const std::string& name, const std::string& value); 21 | 22 | // Numeric parameters 23 | unsigned int getNumericParameterCount() const; 24 | std::vector getNumericParameterNames() const; 25 | bool hasNumericParameter(const std::string& name) const; 26 | unsigned int getNumericParameter(const std::string& name) const; 27 | void addNumericParameter(const std::string& name, unsigned int value); 28 | 29 | EndpointRequestParams& operator= (const EndpointRequestParams&); 30 | friend bool operator== (const EndpointRequestParams&, const EndpointRequestParams&); 31 | friend bool operator!= (const EndpointRequestParams&, const EndpointRequestParams&); 32 | 33 | private: 34 | std::map m_stringParameters; 35 | std::map m_numericParameters; 36 | }; 37 | 38 | }} 39 | 40 | -------------------------------------------------------------------------------- /src/RESTAPICore/Endpoint/IEndpoint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace systelab { namespace web_server { 4 | class Reply; 5 | }} 6 | 7 | namespace systelab { namespace rest_api_core { 8 | 9 | class EndpointRequestData; 10 | 11 | class IEndpoint 12 | { 13 | public: 14 | virtual ~IEndpoint() {}; 15 | 16 | virtual std::unique_ptr execute(const EndpointRequestData&) = 0; 17 | }; 18 | 19 | }} 20 | 21 | -------------------------------------------------------------------------------- /src/RESTAPICore/RouteAccess/EpochTimeService.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "EpochTimeService.h" 3 | 4 | #include "TimeAdapter/ITimeAdapter.h" 5 | #include 6 | 7 | 8 | namespace systelab { namespace rest_api_core { 9 | 10 | EpochTimeService::EpochTimeService(const systelab::time::ITimeAdapter& timeAdapter) 11 | :m_timeAdapter(timeAdapter) 12 | { 13 | } 14 | 15 | EpochTimeService::~EpochTimeService() = default; 16 | 17 | long long EpochTimeService::getCurrentEpochTime() const 18 | { 19 | return std::chrono::system_clock::to_time_t(m_timeAdapter.getCurrentUTCTime()); 20 | } 21 | 22 | }} 23 | -------------------------------------------------------------------------------- /src/RESTAPICore/RouteAccess/EpochTimeService.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "IEpochTimeService.h" 4 | 5 | 6 | namespace systelab { namespace time { 7 | class ITimeAdapter; 8 | }} 9 | 10 | namespace systelab { namespace rest_api_core { 11 | 12 | class EpochTimeService : public IEpochTimeService 13 | { 14 | public: 15 | EpochTimeService(const systelab::time::ITimeAdapter&); 16 | virtual ~EpochTimeService(); 17 | 18 | long long getCurrentEpochTime() const override; 19 | 20 | private: 21 | const systelab::time::ITimeAdapter& m_timeAdapter; 22 | }; 23 | 24 | }} 25 | -------------------------------------------------------------------------------- /src/RESTAPICore/RouteAccess/IEpochTimeService.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | namespace systelab { namespace rest_api_core { 5 | 6 | class IEpochTimeService 7 | { 8 | public: 9 | virtual ~IEpochTimeService() = default; 10 | 11 | virtual long long getCurrentEpochTime() const = 0; 12 | }; 13 | 14 | }} 15 | -------------------------------------------------------------------------------- /src/RESTAPICore/RouteAccess/IRouteAccessValidator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | 4 | namespace systelab { namespace rest_api_core { 5 | 6 | class EndpointRequestData; 7 | 8 | class IRouteAccessValidator 9 | { 10 | public: 11 | virtual ~IRouteAccessValidator() {} 12 | 13 | virtual bool hasAccess(EndpointRequestData&) const = 0; 14 | }; 15 | 16 | }} 17 | -------------------------------------------------------------------------------- /src/RESTAPICore/RouteAccess/IUserRoleService.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | 6 | 7 | namespace systelab { namespace rest_api_core { 8 | 9 | class IUserRoleService 10 | { 11 | public: 12 | virtual ~IUserRoleService() = default; 13 | 14 | virtual std::vector getUserRoles(const std::string& username) const = 0; 15 | }; 16 | 17 | }} 18 | -------------------------------------------------------------------------------- /src/RESTAPICore/RouteAccess/TokenExpirationAccessValidator.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "TokenExpirationAccessValidator.h" 3 | 4 | #include "Endpoint/ClaimConstants.h" 5 | #include "Endpoint/EndpointRequestData.h" 6 | #include "RouteAccess/EpochTimeService.h" 7 | 8 | #include "TimeAdapter/ITimeAdapter.h" 9 | 10 | 11 | namespace systelab { namespace rest_api_core { 12 | 13 | TokenExpirationAccessValidator::TokenExpirationAccessValidator(const systelab::time::ITimeAdapter& timeAdapter, 14 | long expirationSeconds) 15 | :m_epochTimeService(std::make_unique(timeAdapter)) 16 | ,m_expirationSeconds(expirationSeconds) 17 | { 18 | } 19 | 20 | TokenExpirationAccessValidator::~TokenExpirationAccessValidator() = default; 21 | 22 | bool TokenExpirationAccessValidator::hasAccess(EndpointRequestData& endpointRequestData) const 23 | { 24 | try 25 | { 26 | std::string iat = endpointRequestData.getAuthorizationClaims().getClaim(claim::ISSUED_AT); 27 | long long tokenEpoch = std::atoll(iat.c_str()); 28 | long long expirationEpoch = tokenEpoch + m_expirationSeconds; 29 | long long currentEpoch = m_epochTimeService->getCurrentEpochTime(); 30 | 31 | return (currentEpoch <= expirationEpoch); 32 | } 33 | catch (std::exception&) 34 | { 35 | return false; 36 | } 37 | } 38 | 39 | }} 40 | -------------------------------------------------------------------------------- /src/RESTAPICore/RouteAccess/TokenExpirationAccessValidator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "IRouteAccessValidator.h" 4 | 5 | 6 | namespace systelab { namespace time { 7 | class ITimeAdapter; 8 | }} 9 | 10 | namespace systelab { namespace rest_api_core { 11 | 12 | class IEpochTimeService; 13 | 14 | class TokenExpirationAccessValidator : public IRouteAccessValidator 15 | { 16 | public: 17 | TokenExpirationAccessValidator(const systelab::time::ITimeAdapter&, long expirationSeconds); 18 | virtual ~TokenExpirationAccessValidator(); 19 | 20 | bool hasAccess(EndpointRequestData&) const override; 21 | 22 | private: 23 | std::unique_ptr m_epochTimeService; 24 | long m_expirationSeconds; 25 | }; 26 | 27 | }} 28 | -------------------------------------------------------------------------------- /src/RESTAPICore/RouteAccess/UserRoleRouteAccessValidator.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "UserRoleRouteAccessValidator.h" 3 | 4 | #include "Endpoint/ClaimConstants.h" 5 | #include "Endpoint/EndpointRequestData.h" 6 | #include "RouteAccess/IUserRoleService.h" 7 | 8 | 9 | namespace systelab { namespace rest_api_core { 10 | 11 | UserRoleRouteAccessValidator::UserRoleRouteAccessValidator(const std::vector& allowedRoles, 12 | const IUserRoleService& userRoleService) 13 | :m_allowedRoles(allowedRoles) 14 | ,m_userRoleService(userRoleService) 15 | { 16 | } 17 | 18 | UserRoleRouteAccessValidator::~UserRoleRouteAccessValidator() = default; 19 | 20 | bool UserRoleRouteAccessValidator::hasAccess(EndpointRequestData& endpointRequestData) const 21 | { 22 | try 23 | { 24 | std::string sub = endpointRequestData.getAuthorizationClaims().getClaim(claim::SUBJECT); 25 | std::vector roles = m_userRoleService.getUserRoles(sub); 26 | for (auto role : roles) 27 | { 28 | for (auto allowedRole : m_allowedRoles) 29 | { 30 | if (role == allowedRole) 31 | { 32 | return true; 33 | } 34 | } 35 | } 36 | 37 | return false; 38 | } 39 | catch (std::exception&) 40 | { 41 | return false; 42 | } 43 | } 44 | 45 | }} 46 | -------------------------------------------------------------------------------- /src/RESTAPICore/RouteAccess/UserRoleRouteAccessValidator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "IRouteAccessValidator.h" 4 | 5 | #include "RESTAPICore/Endpoint/IEndpoint.h" 6 | #include "RESTAPICore/Router/RouteFragment.h" 7 | 8 | namespace systelab { namespace jwt { 9 | class ITokenParserService; 10 | }} 11 | 12 | namespace systelab { namespace rest_api_core { 13 | 14 | class IUserRoleService; 15 | 16 | class UserRoleRouteAccessValidator : public IRouteAccessValidator 17 | { 18 | public: 19 | UserRoleRouteAccessValidator(const std::vector& allowedRoles, 20 | const IUserRoleService&); 21 | virtual ~UserRoleRouteAccessValidator(); 22 | 23 | bool hasAccess(EndpointRequestData&) const override; 24 | 25 | private: 26 | std::vector m_allowedRoles; 27 | const IUserRoleService& m_userRoleService; 28 | }; 29 | 30 | }} 31 | -------------------------------------------------------------------------------- /src/RESTAPICore/Router/AuthorizationDataBuilder.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "AuthorizationDataBuilder.h" 3 | 4 | #include "Endpoint/EndpointRequestAuthorizationClaims.h" 5 | #include "RouteAccess/IRouteAccessValidator.h" 6 | #include "RouteAccess/IUserRoleService.h" 7 | 8 | #include "WebServerAdapterInterface/Model/Headers.h" 9 | #include "WebServerAdapterInterface/Model/RequestHeaders.h" 10 | 11 | #include "JWTUtils/Services/ITokenParserService.h" 12 | 13 | 14 | namespace systelab { namespace rest_api_core { 15 | 16 | AuthorizationDataBuilder::AuthorizationDataBuilder(const std::string& jwtKey, 17 | const jwt::ITokenParserService& jwtParserService) 18 | :m_jwtKey(jwtKey) 19 | ,m_jwtParserService(jwtParserService) 20 | { 21 | } 22 | 23 | AuthorizationDataBuilder::~AuthorizationDataBuilder() = default; 24 | 25 | std::unique_ptr 26 | AuthorizationDataBuilder::buildAuthorizationData(const systelab::web_server::RequestHeaders& headers) const 27 | { 28 | if (!headers.hasHeader(systelab::web_server::AUTHORIZATION)) 29 | { 30 | return std::make_unique(); 31 | } 32 | 33 | std::string authorizationHeader = headers.getHeader(systelab::web_server::AUTHORIZATION); 34 | if (authorizationHeader.substr(0, 7) != "Bearer ") 35 | { 36 | return std::make_unique(); 37 | } 38 | 39 | std::string token = authorizationHeader.substr(7); 40 | std::vector< std::pair > claims; 41 | if (!m_jwtParserService.validateJWT(token, m_jwtKey, claims)) 42 | { 43 | return std::make_unique(); 44 | } 45 | 46 | return std::make_unique(claims); 47 | } 48 | 49 | }} 50 | -------------------------------------------------------------------------------- /src/RESTAPICore/Router/AuthorizationDataBuilder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "IAuthorizationDataBuilder.h" 4 | 5 | #include "RESTAPICore/Endpoint/IEndpoint.h" 6 | #include "RESTAPICore/Router/RouteFragment.h" 7 | 8 | 9 | namespace systelab { namespace jwt { 10 | class ITokenParserService; 11 | }} 12 | 13 | namespace systelab { namespace rest_api_core { 14 | 15 | class AuthorizationDataBuilder : public IAuthorizationDataBuilder 16 | { 17 | public: 18 | AuthorizationDataBuilder(const std::string& jwtKey, 19 | const jwt::ITokenParserService&); 20 | virtual ~AuthorizationDataBuilder(); 21 | 22 | std::unique_ptr 23 | buildAuthorizationData(const systelab::web_server::RequestHeaders&) const override; 24 | 25 | private: 26 | std::string m_jwtKey; 27 | const jwt::ITokenParserService& m_jwtParserService; 28 | }; 29 | 30 | }} 31 | -------------------------------------------------------------------------------- /src/RESTAPICore/Router/IAuthorizationDataBuilder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace systelab { namespace web_server { 4 | class RequestHeaders; 5 | }} 6 | 7 | namespace systelab { namespace rest_api_core { 8 | 9 | class EndpointRequestAuthorizationClaims; 10 | 11 | class IAuthorizationDataBuilder 12 | { 13 | public: 14 | virtual ~IAuthorizationDataBuilder() = default; 15 | 16 | virtual std::unique_ptr 17 | buildAuthorizationData(const systelab::web_server::RequestHeaders&) const = 0; 18 | }; 19 | 20 | }} 21 | -------------------------------------------------------------------------------- /src/RESTAPICore/Router/IRoute.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | 6 | namespace systelab { namespace web_server { 7 | class Reply; 8 | class Request; 9 | }} 10 | 11 | namespace systelab { namespace rest_api_core { 12 | 13 | class IRoute 14 | { 15 | public: 16 | virtual ~IRoute() = default; 17 | 18 | virtual std::unique_ptr execute(const systelab::web_server::Request&) const = 0; 19 | }; 20 | 21 | }} 22 | -------------------------------------------------------------------------------- /src/RESTAPICore/Router/IRoutesFactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace systelab { namespace rest_api_core { 4 | 5 | class IEndpoint; 6 | class IRoute; 7 | class IRouteAccessValidator; 8 | 9 | class IRoutesFactory 10 | { 11 | public: 12 | typedef std::function< std::unique_ptr() > RouteAccessValidatorFactoryMethod; 13 | typedef std::function< std::unique_ptr() > EndpointFactoryMethod; 14 | 15 | public: 16 | virtual ~IRoutesFactory() = default; 17 | 18 | virtual std::unique_ptr 19 | buildRoute(const std::string& method, 20 | const std::string& uri, 21 | const std::vector&, 22 | EndpointFactoryMethod) const = 0; 23 | }; 24 | 25 | }} 26 | -------------------------------------------------------------------------------- /src/RESTAPICore/Router/Route.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "Route.h" 3 | 4 | #include "Endpoint/EndpointRequestAuthorizationClaims.h" 5 | #include "Endpoint/EndpointRequestData.h" 6 | #include "Endpoint/EndpointRequestParams.h" 7 | #include "RouteAccess/IRouteAccessValidator.h" 8 | #include "Router/IAuthorizationDataBuilder.h" 9 | 10 | #include "WebServerAdapterInterface/Model/Reply.h" 11 | #include "WebServerAdapterInterface/Model/Request.h" 12 | #include "WebServerAdapterInterface/Model/Headers.h" 13 | 14 | #include 15 | 16 | 17 | namespace systelab { namespace rest_api_core { 18 | 19 | Route::Route(const std::string& method, 20 | const std::string& uri, 21 | const IAuthorizationDataBuilder& authorizationDataBuilder, 22 | const std::vector& accessValidatorFactoryMethods, 23 | EndpointFactoryMethod endpointFactoryMethod, 24 | const systelab::web_server::Reply& unauthorizedReply, 25 | const systelab::web_server::Reply& forbiddenReply) 26 | :m_method(method) 27 | ,m_fragments(buildFragmentsFromURI(uri)) 28 | ,m_authorizationDataBuilder(authorizationDataBuilder) 29 | ,m_accessValidatorFactoryMethods(accessValidatorFactoryMethods) 30 | ,m_endpointFactoryMethod(endpointFactoryMethod) 31 | ,m_unauthorizedReply(unauthorizedReply) 32 | ,m_forbiddenReply(forbiddenReply) 33 | { 34 | } 35 | 36 | std::unique_ptr Route::execute(const systelab::web_server::Request& request) const 37 | { 38 | if (!validateMethod(request)) 39 | { 40 | return nullptr; 41 | } 42 | 43 | auto requestData = parseRequestData(request); 44 | if (!requestData) 45 | { 46 | return nullptr; 47 | } 48 | 49 | if ((m_accessValidatorFactoryMethods.size() > 0) && !requestData->getHeaders().hasHeader(systelab::web_server::AUTHORIZATION)) 50 | { 51 | return std::make_unique(systelab::web_server::Reply::UNAUTHORIZED, 52 | m_unauthorizedReply.getHeaders(), m_unauthorizedReply.getContent()); 53 | } 54 | 55 | for (auto& accessValidatorFactoryMethod : m_accessValidatorFactoryMethods) 56 | { 57 | auto accessValidator = accessValidatorFactoryMethod(); 58 | if (!accessValidator->hasAccess(*requestData)) 59 | { 60 | return std::make_unique(systelab::web_server::Reply::FORBIDDEN, 61 | m_forbiddenReply.getHeaders(), m_forbiddenReply.getContent()); 62 | } 63 | } 64 | 65 | try 66 | { 67 | std::unique_ptr endpoint = m_endpointFactoryMethod(); 68 | if (endpoint) 69 | { 70 | return endpoint->execute(*requestData); 71 | } 72 | else 73 | { 74 | std::string content = "{ \"reason\": \"Unable to build endpoint for " + 75 | request.getMethod() + " " + request.getURI() + "\"}"; 76 | std::map headers = { {std::string("Content-Type"), std::string("application/json")} }; 77 | return std::make_unique(systelab::web_server::Reply::INTERNAL_SERVER_ERROR, headers, content); 78 | } 79 | } 80 | catch (std::exception& exc) 81 | { 82 | std::string content = "{ \"reason\": \"Unknown error while processing endpoint " + 83 | request.getMethod() + " " + request.getURI() + ": " + exc.what() + "\"}"; 84 | std::map headers = { {std::string("Content-Type"), std::string("application/json")} }; 85 | return std::make_unique(systelab::web_server::Reply::INTERNAL_SERVER_ERROR, headers, content); 86 | } 87 | } 88 | 89 | bool Route::validateMethod(const systelab::web_server::Request& request) const 90 | { 91 | return (request.getMethod() == m_method); 92 | } 93 | 94 | std::unique_ptr Route::parseRequestData(const systelab::web_server::Request& request) const 95 | { 96 | std::vector requestFragments = buildFragmentsFromURI(request.getURI()); 97 | if (requestFragments.size() != m_fragments.size()) 98 | { 99 | return nullptr; 100 | } 101 | 102 | EndpointRequestParams requestParams; 103 | size_t nFragments = m_fragments.size(); 104 | for (size_t i = 0; i < nFragments; i++) 105 | { 106 | const auto& requestFragment = requestFragments[i]; 107 | if (requestFragment.isNumericParameter() || requestFragment.isStringParameter()) 108 | { 109 | return nullptr; 110 | } 111 | 112 | const auto& fragment = m_fragments[i]; 113 | if (!fragment.match(requestFragment.getValue())) 114 | { 115 | return nullptr; 116 | } 117 | 118 | if (fragment.isNumericParameter()) 119 | { 120 | unsigned int numericValue = static_cast(std::atoi(requestFragment.getValue().c_str())); 121 | requestParams.addNumericParameter(fragment.getValue(), numericValue); 122 | } 123 | else if (fragment.isStringParameter()) 124 | { 125 | requestParams.addStringParameter(fragment.getValue(), requestFragment.getValue()); 126 | } 127 | } 128 | 129 | auto authorizationData = m_authorizationDataBuilder.buildAuthorizationData(request.getHeaders()); 130 | 131 | return std::make_unique(requestParams, request.getContent(), 132 | request.getHeaders(), request.getQueryStrings(), 133 | *authorizationData); 134 | } 135 | 136 | std::vector Route::buildFragmentsFromURI(const std::string& uri) const 137 | { 138 | std::vector fragments; 139 | 140 | std::istringstream iss(uri); 141 | std::string item; 142 | 143 | while (std::getline(iss, item, '/')) 144 | { 145 | if (item.empty()) 146 | { 147 | continue; 148 | } 149 | 150 | fragments.push_back(RouteFragment(item)); 151 | } 152 | 153 | return fragments; 154 | } 155 | 156 | }} -------------------------------------------------------------------------------- /src/RESTAPICore/Router/Route.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "IRoute.h" 4 | 5 | #include "RESTAPICore/Endpoint/IEndpoint.h" 6 | #include "RESTAPICore/Router/RouteFragment.h" 7 | 8 | 9 | namespace systelab { namespace web_server { 10 | class Reply; 11 | }} 12 | 13 | namespace systelab { namespace rest_api_core { 14 | 15 | class IAuthorizationDataBuilder; 16 | class IRouteAccessValidator; 17 | 18 | class Route : public IRoute 19 | { 20 | public: 21 | typedef std::function< std::unique_ptr() > RouteAccessValidatorFactoryMethod; 22 | typedef std::function< std::unique_ptr() > EndpointFactoryMethod; 23 | 24 | public: 25 | Route(const std::string& method, 26 | const std::string& uri, 27 | const IAuthorizationDataBuilder&, 28 | const std::vector&, 29 | EndpointFactoryMethod factoryMethod, 30 | const systelab::web_server::Reply& unauthorizedReply, 31 | const systelab::web_server::Reply& forbiddenReply); 32 | 33 | std::unique_ptr execute(const systelab::web_server::Request&) const; 34 | 35 | protected: 36 | std::string m_method; 37 | std::vector m_fragments; 38 | const IAuthorizationDataBuilder& m_authorizationDataBuilder; 39 | std::vector m_accessValidatorFactoryMethods; 40 | EndpointFactoryMethod m_endpointFactoryMethod; 41 | const systelab::web_server::Reply& m_unauthorizedReply; 42 | const systelab::web_server::Reply& m_forbiddenReply; 43 | 44 | protected: 45 | bool validateMethod(const systelab::web_server::Request&) const; 46 | std::unique_ptr parseRequestData(const systelab::web_server::Request&) const; 47 | std::vector buildFragmentsFromURI(const std::string& uri) const; 48 | }; 49 | 50 | }} 51 | -------------------------------------------------------------------------------- /src/RESTAPICore/Router/RouteFragment.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "RouteFragment.h" 3 | 4 | #include 5 | 6 | 7 | namespace systelab { namespace rest_api_core { 8 | 9 | RouteFragment::RouteFragment(const std::string& fragment) 10 | { 11 | m_isStringParameter = (fragment[0] == ':'); 12 | m_isNumericParameter = (fragment[0] == '+'); 13 | m_value = (m_isStringParameter || m_isNumericParameter) ? fragment.substr(1) : fragment; 14 | } 15 | 16 | RouteFragment::~RouteFragment() = default; 17 | 18 | bool RouteFragment::isStringParameter() const 19 | { 20 | return m_isStringParameter; 21 | } 22 | 23 | bool RouteFragment::isNumericParameter() const 24 | { 25 | return m_isNumericParameter; 26 | } 27 | 28 | std::string RouteFragment::getValue() const 29 | { 30 | return m_value; 31 | } 32 | 33 | bool RouteFragment::match(const std::string& uriFragment) const 34 | { 35 | if (m_isStringParameter) 36 | { 37 | return true; 38 | } 39 | else if (m_isNumericParameter) 40 | { 41 | std::string::const_iterator it = uriFragment.begin(); 42 | while (it != uriFragment.end() && std::isdigit(*it)) ++it; 43 | return !uriFragment.empty() && it == uriFragment.end(); 44 | } 45 | else 46 | { 47 | return (uriFragment == m_value); 48 | } 49 | } 50 | 51 | }} 52 | -------------------------------------------------------------------------------- /src/RESTAPICore/Router/RouteFragment.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | 6 | namespace systelab { namespace rest_api_core { 7 | 8 | class RouteFragment 9 | { 10 | public: 11 | RouteFragment(const std::string& item); 12 | virtual ~RouteFragment(); 13 | 14 | bool isStringParameter() const; 15 | bool isNumericParameter() const; 16 | std::string getValue() const; 17 | 18 | bool match(const std::string& uriFragment) const; 19 | 20 | private: 21 | bool m_isStringParameter; 22 | bool m_isNumericParameter; 23 | std::string m_value; 24 | }; 25 | 26 | }} 27 | -------------------------------------------------------------------------------- /src/RESTAPICore/Router/Router.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "Router.h" 3 | 4 | #include "Router/Route.h" 5 | 6 | #include "WebServerAdapterInterface/Model/Reply.h" 7 | #include "WebServerAdapterInterface/Model/Request.h" 8 | 9 | 10 | namespace systelab { namespace rest_api_core { 11 | 12 | Router::Router() 13 | { 14 | } 15 | 16 | Router::~Router() = default; 17 | 18 | void Router::addRoute(std::unique_ptr route) 19 | { 20 | m_routes.push_back(std::move(route)); 21 | } 22 | 23 | std::unique_ptr Router::process(const systelab::web_server::Request& request) const 24 | { 25 | for (const auto& route : m_routes) 26 | { 27 | std::unique_ptr reply = route->execute(request); 28 | if (reply) 29 | { 30 | return std::move(reply); 31 | } 32 | } 33 | 34 | return std::unique_ptr(); 35 | } 36 | 37 | }} 38 | -------------------------------------------------------------------------------- /src/RESTAPICore/Router/Router.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | 9 | namespace systelab { namespace web_server { 10 | class Reply; 11 | class Request; 12 | }} 13 | 14 | namespace systelab { namespace rest_api_core { 15 | 16 | class IRoute; 17 | 18 | class Router 19 | { 20 | public: 21 | Router(); 22 | virtual ~Router(); 23 | 24 | void addRoute(std::unique_ptr); 25 | 26 | std::unique_ptr process(const systelab::web_server::Request& request) const; 27 | 28 | private: 29 | std::vector< std::unique_ptr > m_routes; 30 | }; 31 | 32 | }} 33 | -------------------------------------------------------------------------------- /src/RESTAPICore/Router/RoutesFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "RoutesFactory.h" 3 | 4 | #include "Router/AuthorizationDataBuilder.h" 5 | #include "Router/Route.h" 6 | 7 | #include "JWTUtils/Services/TokenParserService.h" 8 | #include "RapidJSONAdapter/JSONAdapter.h" 9 | #include "WebServerAdapterInterface/Model/Reply.h" 10 | 11 | 12 | namespace systelab { namespace rest_api_core { 13 | 14 | RoutesFactory::RoutesFactory(const std::string& jwtKey) 15 | :m_jsonAdapter(std::make_unique()) 16 | ,m_jwtParserService(std::make_unique(*m_jsonAdapter)) 17 | ,m_authorizationDataBuilder(std::make_unique(jwtKey, *m_jwtParserService)) 18 | { 19 | std::map headers = { {std::string("Content-Type"), std::string("application/json")} }; 20 | m_unauthorizedReply = std::make_unique(systelab::web_server::Reply::UNAUTHORIZED, headers, "{}"); 21 | m_forbiddenReply = std::make_unique(systelab::web_server::Reply::FORBIDDEN, headers, "{}"); 22 | } 23 | 24 | RoutesFactory::~RoutesFactory() 25 | { 26 | m_authorizationDataBuilder.reset(); 27 | m_jwtParserService.reset(); 28 | m_jsonAdapter.reset(); 29 | 30 | m_unauthorizedReply.reset(); 31 | m_forbiddenReply.reset(); 32 | } 33 | 34 | void RoutesFactory::setUnauthorizedReply(const systelab::web_server::Reply& unauthorizedReply) 35 | { 36 | m_unauthorizedReply = std::make_unique(unauthorizedReply.getStatus(), unauthorizedReply.getHeaders(), unauthorizedReply.getContent()); 37 | } 38 | 39 | void RoutesFactory::setForbiddenReply(const systelab::web_server::Reply& forbiddenReply) 40 | { 41 | m_forbiddenReply = std::make_unique(forbiddenReply.getStatus(), forbiddenReply.getHeaders(), forbiddenReply.getContent()); 42 | } 43 | 44 | std::unique_ptr 45 | RoutesFactory::buildRoute(const std::string& method, 46 | const std::string& uri, 47 | const std::vector& accessValidatorFactoryMethods, 48 | EndpointFactoryMethod endpointFactoryMethod) const 49 | { 50 | return std::make_unique(method, uri, *m_authorizationDataBuilder, 51 | accessValidatorFactoryMethods, endpointFactoryMethod, 52 | *m_unauthorizedReply, *m_forbiddenReply); 53 | } 54 | 55 | }} -------------------------------------------------------------------------------- /src/RESTAPICore/Router/RoutesFactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "IRoutesFactory.h" 4 | 5 | 6 | namespace systelab { 7 | namespace json { 8 | class IJSONAdapter; 9 | } 10 | namespace jwt { 11 | class ITokenParserService; 12 | } 13 | namespace web_server { 14 | class Reply; 15 | } 16 | } 17 | 18 | namespace systelab { namespace rest_api_core { 19 | 20 | class IAuthorizationDataBuilder; 21 | class IRouteAccessValidator; 22 | 23 | class RoutesFactory : public IRoutesFactory 24 | { 25 | public: 26 | RoutesFactory(const std::string& jwtKey); 27 | virtual ~RoutesFactory(); 28 | 29 | void setUnauthorizedReply(const systelab::web_server::Reply&); 30 | void setForbiddenReply(const systelab::web_server::Reply&); 31 | 32 | std::unique_ptr 33 | buildRoute(const std::string& method, 34 | const std::string& uri, 35 | const std::vector&, 36 | EndpointFactoryMethod) const override; 37 | 38 | protected: 39 | std::unique_ptr m_jsonAdapter; 40 | std::unique_ptr m_jwtParserService; 41 | std::unique_ptr m_authorizationDataBuilder; 42 | 43 | std::unique_ptr m_unauthorizedReply; 44 | std::unique_ptr m_forbiddenReply; 45 | }; 46 | 47 | }} 48 | -------------------------------------------------------------------------------- /src/RESTAPICore/stdafx.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | // STL 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | -------------------------------------------------------------------------------- /test/RESTAPICoreTest/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2) 2 | 3 | # Enable testing module 4 | enable_testing() 5 | 6 | # Find external dependencides 7 | set(GTEST_PACKAGE_NAME gtest) 8 | find_package(${GTEST_PACKAGE_NAME}) 9 | if (NOT TARGET ${GTEST_PACKAGE_NAME}::${GTEST_PACKAGE_NAME}) 10 | set(GTEST_PACKAGE_NAME GTest) 11 | find_package(${GTEST_PACKAGE_NAME}) 12 | endif() 13 | 14 | find_package(WebServerAdapterTestUtilities) 15 | find_package(JSONAdapterTestUtilities) 16 | find_package(TimeAdapterTestUtilities) 17 | 18 | # Add project folder into includes 19 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 20 | 21 | # Configure test project 22 | set(REST_API_CORE_TEST_PROJECT RESTAPICoreTest) 23 | file(GLOB_RECURSE REST_API_CORE_TEST_PROJECT_SRC "*.cpp") 24 | file(GLOB_RECURSE REST_API_CORE_TEST_PROJECT_HDR "*.h") 25 | add_executable(${REST_API_CORE_TEST_PROJECT} ${REST_API_CORE_TEST_PROJECT_SRC} ${REST_API_CORE_TEST_PROJECT_HDR}) 26 | target_link_libraries(${REST_API_CORE_TEST_PROJECT} RESTAPICore RESTAPICoreTestUtilities 27 | ${GTEST_PACKAGE_NAME}::${GTEST_PACKAGE_NAME} 28 | WebServerAdapterTestUtilities::WebServerAdapterTestUtilities 29 | JSONAdapterTestUtilities::JSONAdapterTestUtilities 30 | TimeAdapterTestUtilities::TimeAdapterTestUtilities) 31 | 32 | target_include_directories(${REST_API_CORE_TEST_PROJECT} PRIVATE ${JSONAdapterTestUtilities_INCLUDE_DIRS}) 33 | #Configure source groups 34 | foreach(FILE ${REST_API_CORE_TEST_PROJECT_SRC} ${REST_API_CORE_TEST_PROJECT_HDR}) 35 | get_filename_component(PARENT_DIR "${FILE}" DIRECTORY) 36 | string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}" "" GROUP "${PARENT_DIR}") 37 | string(REPLACE "/" "\\" GROUP "${GROUP}") 38 | 39 | if ("${FILE}" MATCHES ".*\\.cpp") 40 | set(GROUP "Source Files${GROUP}") 41 | elseif("${FILE}" MATCHES ".*\\.h") 42 | set(GROUP "Header Files${GROUP}") 43 | endif() 44 | 45 | source_group("${GROUP}" FILES "${FILE}") 46 | endforeach() 47 | 48 | # Register tests 49 | add_test(NAME RESTAPICoreTest COMMAND ${REST_API_CORE_TEST_PROJECT}) 50 | 51 | # Additional target for code coverage analysis 52 | if (CMAKE_BUILD_TYPE STREQUAL "Coverage") 53 | include(CodeCoverage) 54 | setup_target_for_coverage(RESTAPICoreTestCoverage RESTAPICoreTest coverage) 55 | endif() 56 | -------------------------------------------------------------------------------- /test/RESTAPICoreTest/RESTAPICoreTest.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | 3 | 4 | int main(int argc, char* argv[]) 5 | { 6 | ::testing::FLAGS_gmock_verbose = "error"; 7 | ::testing::InitGoogleTest(&argc, argv); 8 | 9 | int res = RUN_ALL_TESTS(); 10 | 11 | return res; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /test/RESTAPICoreTest/Tests/RouteAccess/EpochTimeServiceTest.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "RESTAPICore/RouteAccess/EpochTimeService.h" 3 | 4 | #include "TimeAdapterTestUtilities/Stubs/StubTimeAdapter.h" 5 | 6 | 7 | using namespace testing; 8 | using namespace systelab::time::test_utility; 9 | 10 | namespace systelab { namespace rest_api_core { namespace unit_test { 11 | 12 | class EpochTimeServiceTest : public Test 13 | { 14 | void SetUp() 15 | { 16 | setUpTimeAdapter(); 17 | m_service = std::make_unique(m_timeAdapter); 18 | } 19 | 20 | void setUpTimeAdapter() 21 | { 22 | m_currentEpoch = 1586272783; 23 | auto currentTimePoint = std::chrono::system_clock::time_point{ std::chrono::seconds{m_currentEpoch} }; 24 | m_timeAdapter.setCurrentTime(currentTimePoint); 25 | } 26 | 27 | protected: 28 | std::unique_ptr m_service; 29 | 30 | StubTimeAdapter m_timeAdapter; 31 | long long m_currentEpoch; 32 | }; 33 | 34 | 35 | TEST_F(EpochTimeServiceTest, testGetCurrentEpochTimeReturnsValueConfiguredOnTimeAdapter) 36 | { 37 | ASSERT_EQ(m_currentEpoch, m_service->getCurrentEpochTime()); 38 | } 39 | 40 | TEST_F(EpochTimeServiceTest, testGetCurrentEpochTimeReturnsSameValueOnConsecutiveCalls) 41 | { 42 | time_t currentTime1 = m_service->getCurrentEpochTime(); 43 | time_t currentTime2 = m_service->getCurrentEpochTime(); 44 | ASSERT_EQ(currentTime2, currentTime1); 45 | } 46 | 47 | TEST_F(EpochTimeServiceTest, testGetCurrentEpochTimeReturnsValueConfiguredOnTimeAdapterAfter1Millisecond) 48 | { 49 | m_timeAdapter.addMilliseconds(1); 50 | ASSERT_EQ(m_currentEpoch, m_service->getCurrentEpochTime()); 51 | } 52 | 53 | TEST_F(EpochTimeServiceTest, testGetCurrentEpochTimeReturnsValueConfiguredOnTimeAdapterAfter999Milliseconds) 54 | { 55 | m_timeAdapter.addMilliseconds(999); 56 | ASSERT_EQ(m_currentEpoch, m_service->getCurrentEpochTime()); 57 | } 58 | 59 | TEST_F(EpochTimeServiceTest, testGetCurrentEpochTimeReturnsNextEpochAfter1Second) 60 | { 61 | m_timeAdapter.addSeconds(1); 62 | ASSERT_EQ(m_currentEpoch + 1, m_service->getCurrentEpochTime()); 63 | } 64 | 65 | TEST_F(EpochTimeServiceTest, testGetCurrentEpochTimeReturnsNextEpochAfter1SecondAndAHalf) 66 | { 67 | m_timeAdapter.addMilliseconds(1500); 68 | ASSERT_EQ(m_currentEpoch + 1, m_service->getCurrentEpochTime()); 69 | } 70 | 71 | TEST_F(EpochTimeServiceTest, testGetCurrentTimeEpochReturnsTime25SecondsGreaterAfter25Seconds) 72 | { 73 | time_t currentTime1 = m_service->getCurrentEpochTime(); 74 | m_timeAdapter.addSeconds(25); 75 | time_t currentTime2 = m_service->getCurrentEpochTime(); 76 | ASSERT_EQ(difftime(currentTime2, currentTime1), 25); 77 | } 78 | 79 | }}} 80 | -------------------------------------------------------------------------------- /test/RESTAPICoreTest/Tests/RouteAccess/TokenExpirationAccessValidatorTest.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "RESTAPICore/RouteAccess/TokenExpirationAccessValidator.h" 3 | 4 | #include "RESTAPICore/Endpoint/ClaimConstants.h" 5 | #include "RESTAPICore/Endpoint/EndpointRequestData.h" 6 | 7 | #include "TimeAdapterTestUtilities/Stubs/StubTimeAdapter.h" 8 | 9 | #include 10 | 11 | 12 | using namespace testing; 13 | using namespace systelab::time::test_utility; 14 | 15 | namespace systelab { namespace rest_api_core { namespace unit_test { 16 | 17 | namespace { 18 | struct TokenExpirationAccessValidatorTestData 19 | { 20 | unsigned int expirationSeconds; 21 | unsigned int elapsedSeconds; 22 | bool expectedAccess; 23 | }; 24 | 25 | std::vector< TokenExpirationAccessValidatorTestData> testData = 26 | { 27 | // Expiration 30 minutes => 1800 seconds 28 | {1800, 0, true}, 29 | {1800, 1, true}, 30 | {1800, 10, true}, 31 | {1800, 100, true}, 32 | {1800, 1000, true}, 33 | {1800, 1799, true}, 34 | {1800, 1800, true}, 35 | {1800, 1801, false}, 36 | {1800, 10000, false}, 37 | {1800, 100000, false}, 38 | 39 | // Expiration 2 hours => 7200 seconds 40 | {7200, 0, true}, 41 | {7200, 1, true}, 42 | {7200, 10, true}, 43 | {7200, 100, true}, 44 | {7200, 1000, true}, 45 | {7200, 5000, true}, 46 | {7200, 7199, true}, 47 | {7200, 7200, true}, 48 | {7200, 7201, false}, 49 | {7200, 10000, false}, 50 | {7200, 100000, false}, 51 | }; 52 | } 53 | 54 | class TokenExpirationAccessValidatorTest : public TestWithParam 55 | { 56 | public: 57 | void SetUp() 58 | { 59 | unsigned int expirationSeconds = 1800; 60 | setUpTimeAdapter(); 61 | 62 | m_validator = std::make_unique(m_timeAdapter, expirationSeconds); 63 | } 64 | 65 | void setUpTimeAdapter() 66 | { 67 | long long currentEpoch = 1586432000; 68 | auto currentTimePoint = std::chrono::system_clock::time_point{ std::chrono::seconds{currentEpoch} }; 69 | m_timeAdapter.setCurrentTime(currentTimePoint); 70 | } 71 | 72 | EndpointRequestData buildEndpointRequestDataWithIATOfCurrentTime() 73 | { 74 | std::map claims; 75 | long long currentEpoch = std::chrono::system_clock::to_time_t(m_timeAdapter.getCurrentUTCTime()); 76 | claims.insert( std::make_pair(claim::ISSUED_AT, std::to_string(currentEpoch)) ); 77 | 78 | return buildEndpointRequestData(claims); 79 | } 80 | 81 | EndpointRequestData buildEndpointRequestData(std::map claims) 82 | { 83 | EndpointRequestAuthorizationClaims authorizationClaims; 84 | for (auto claim : claims) 85 | { 86 | authorizationClaims.addClaim(claim.first, claim.second); 87 | } 88 | 89 | EndpointRequestData endpointRequestData; 90 | endpointRequestData.setAuthorizationClaims(authorizationClaims); 91 | 92 | return endpointRequestData; 93 | } 94 | 95 | protected: 96 | std::unique_ptr m_validator; 97 | 98 | StubTimeAdapter m_timeAdapter; 99 | long m_expirationSeconds; 100 | }; 101 | 102 | 103 | TEST_P(TokenExpirationAccessValidatorTest, testHasAccessReturnsExpectedValue) 104 | { 105 | m_validator = std::make_unique(m_timeAdapter, GetParam().expirationSeconds); 106 | EndpointRequestData endpointRequestData = buildEndpointRequestDataWithIATOfCurrentTime(); 107 | 108 | m_timeAdapter.addSeconds(GetParam().elapsedSeconds); 109 | 110 | ASSERT_EQ(GetParam().expectedAccess, m_validator->hasAccess(endpointRequestData)); 111 | } 112 | 113 | TEST_F(TokenExpirationAccessValidatorTest, testHasAccessReturnsFalseWhenIATClaimNotFound) 114 | { 115 | EndpointRequestData endpointRequestData = buildEndpointRequestData({}); 116 | ASSERT_FALSE(m_validator->hasAccess(endpointRequestData)); 117 | } 118 | 119 | TEST_F(TokenExpirationAccessValidatorTest, testHasAccessReturnsFalseWhenIATClaimIsInvalid) 120 | { 121 | EndpointRequestData endpointRequestData = buildEndpointRequestData({ {claim::ISSUED_AT, "InvalidValue"} }); 122 | ASSERT_FALSE(m_validator->hasAccess(endpointRequestData)); 123 | } 124 | 125 | INSTANTIATE_TEST_SUITE_P(RESTAPICore, TokenExpirationAccessValidatorTest, testing::ValuesIn(testData)); 126 | 127 | }}} 128 | -------------------------------------------------------------------------------- /test/RESTAPICoreTest/Tests/RouteAccess/UserRoleRouteAccessValidatorTest.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "RESTAPICore/RouteAccess/UserRoleRouteAccessValidator.h" 3 | 4 | #include "RESTAPICore/Endpoint/ClaimConstants.h" 5 | #include "RESTAPICore/Endpoint/EndpointRequestData.h" 6 | 7 | #include "RESTAPICoreTestUtilities/Mocks/RouteAccess/MockUserRoleService.h" 8 | 9 | 10 | using namespace testing; 11 | using namespace systelab::rest_api_core::test_utility; 12 | 13 | namespace systelab { namespace rest_api_core { namespace unit_test { 14 | 15 | namespace { 16 | struct UserRoleRouteAccessValidatorTestData 17 | { 18 | std::vector allowedRoles; 19 | std::vector userRoles; 20 | bool expectedAccess; 21 | }; 22 | 23 | std::vector testData = 24 | { 25 | // Super, manager and basic allowed 26 | { {"SUPER", "MANAGER", "BASIC"}, {"SUPER"}, true}, 27 | { {"SUPER", "MANAGER", "BASIC"}, {"MANAGER"}, true}, 28 | { {"SUPER", "MANAGER", "BASIC"}, {"BASIC"}, true}, 29 | { {"SUPER", "MANAGER", "BASIC"}, {"BASIC", "EXTERNAL"}, true}, 30 | { {"SUPER", "MANAGER", "BASIC"}, {"EXTERNAL"}, false}, 31 | { {"SUPER", "MANAGER", "BASIC"}, {"ANOTHER", "EXTERNAL"}, false}, 32 | { {"SUPER", "MANAGER", "BASIC"}, {}, false}, 33 | 34 | // Only super allowed 35 | { {"SUPER"}, {"SUPER"}, true}, 36 | { {"SUPER"}, {"MANAGER"}, false}, 37 | { {"SUPER"}, {"BASIC"}, false}, 38 | { {"SUPER"}, {"SUPER", "MANAGER", "BASIC"}, true}, 39 | { {"SUPER"}, {"SUPER", "MANAGER"}, true}, 40 | { {"SUPER"}, {"MANAGER", "BASIC"}, false}, 41 | { {"SUPER"}, {}, false}, 42 | 43 | // No one allowed 44 | { {}, {"SUPER"}, false}, 45 | { {}, {"MANAGER"}, false}, 46 | { {}, {"BASIC"}, false}, 47 | { {}, {"BASIC", "EXTERNAL"}, false}, 48 | { {}, {"EXTERNAL"}, false}, 49 | { {}, {"ANOTHER", "EXTERNAL"}, false}, 50 | { {}, {}, false}, 51 | }; 52 | } 53 | 54 | class UserRoleRouteAccessValidatorTest : public TestWithParam 55 | { 56 | public: 57 | void SetUp() 58 | { 59 | m_username = "jsmith"; 60 | ON_CALL(m_userRoleService, getUserRoles(_)).WillByDefault(Return(std::vector())); 61 | ON_CALL(m_userRoleService, getUserRoles(m_username)).WillByDefault(Invoke( 62 | [this](const std::string&) -> std::vector 63 | { 64 | return m_userRoles; 65 | } 66 | )); 67 | 68 | m_validator = std::make_unique(std::vector(), m_userRoleService); 69 | } 70 | 71 | EndpointRequestData buildEndpointRequestDataWithValidSUBJECT() 72 | { 73 | std::map claims; 74 | claims.insert(std::make_pair(claim::SUBJECT, m_username)); 75 | 76 | return buildEndpointRequestData(claims); 77 | } 78 | 79 | EndpointRequestData buildEndpointRequestData(std::map claims) 80 | { 81 | EndpointRequestAuthorizationClaims authorizationClaims; 82 | for (auto claim : claims) 83 | { 84 | authorizationClaims.addClaim(claim.first, claim.second); 85 | } 86 | 87 | EndpointRequestData endpointRequestData; 88 | endpointRequestData.setAuthorizationClaims(authorizationClaims); 89 | 90 | return endpointRequestData; 91 | } 92 | 93 | protected: 94 | std::unique_ptr m_validator; 95 | MockUserRoleService m_userRoleService; 96 | 97 | std::string m_username; 98 | std::vector m_userRoles; 99 | }; 100 | 101 | 102 | TEST_P(UserRoleRouteAccessValidatorTest, testHasAccessReturnsExpectedValue) 103 | { 104 | m_userRoles = GetParam().userRoles; 105 | m_validator = std::make_unique(GetParam().allowedRoles, m_userRoleService); 106 | EndpointRequestData endpointRequestData = buildEndpointRequestDataWithValidSUBJECT(); 107 | 108 | ASSERT_EQ(GetParam().expectedAccess, m_validator->hasAccess(endpointRequestData)); 109 | } 110 | 111 | TEST_F(UserRoleRouteAccessValidatorTest, testHasAccessReturnsFalseWhenSUBJECTClaimNotFound) 112 | { 113 | EndpointRequestData endpointRequestData = buildEndpointRequestData({}); 114 | ASSERT_FALSE(m_validator->hasAccess(endpointRequestData)); 115 | } 116 | 117 | TEST_F(UserRoleRouteAccessValidatorTest, testHasAccessReturnsFalseWhenSUBJECTClaimIsInvalid) 118 | { 119 | EndpointRequestData endpointRequestData = buildEndpointRequestData({ {claim::SUBJECT, "InvalidUsername"} }); 120 | ASSERT_FALSE(m_validator->hasAccess(endpointRequestData)); 121 | } 122 | 123 | INSTANTIATE_TEST_SUITE_P(RESTAPICore, UserRoleRouteAccessValidatorTest, testing::ValuesIn(testData)); 124 | 125 | }}} 126 | -------------------------------------------------------------------------------- /test/RESTAPICoreTest/Tests/Router/AuthorizationDataBuilderTest.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "RESTAPICore/Router/AuthorizationDataBuilder.h" 3 | 4 | #include "JWTUtils/Services/TokenParserService.h" 5 | #include "RapidJSONAdapter/JSONAdapter.h" 6 | #include "RESTAPICore/Endpoint/EndpointRequestAuthorizationClaims.h" 7 | 8 | #include "WebServerAdapterInterface/Model/Headers.h" 9 | #include "WebServerAdapterInterface/Model/RequestHeaders.h" 10 | 11 | #include "TestUtilitiesInterface/EntityComparator.h" 12 | 13 | 14 | using namespace testing; 15 | using namespace systelab::test_utility; 16 | 17 | namespace systelab { namespace rest_api_core { namespace unit_test { 18 | 19 | namespace { 20 | 21 | struct AuthorizationDataBuilderTestData 22 | { 23 | std::map headers; 24 | std::map expectedClaims; 25 | }; 26 | 27 | std::vector< AuthorizationDataBuilderTestData> testData = 28 | { 29 | { 30 | { {web_server::CONTENT_TYPE, "application/json"} }, 31 | { } 32 | }, 33 | { 34 | { {web_server::AUTHORIZATION, "InvalidValue"} }, 35 | { } 36 | }, 37 | { 38 | { {web_server::AUTHORIZATION, "Basic anNtaXRoOm15cGFzc3dvcmQ="} }, 39 | { } 40 | }, 41 | { 42 | { {web_server::AUTHORIZATION, "Bearer anNtaXRoOm15cGFzc3dvcmQ="} }, 43 | { } 44 | }, 45 | { 46 | { {web_server::AUTHORIZATION, "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." 47 | "eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoiMTUxNjIzOTAyMiJ9." 48 | "SQ8-pQRMliyILSz_9n8f-Q2-viYWPMdPVmy5a6UlyWU"} }, 49 | { {"sub", "1234567890"}, {"name", "John Doe"}, {"iat", "1516239022"} } 50 | }, 51 | { 52 | { {web_server::CONTENT_TYPE, "application/json"}, 53 | {web_server::AUTHORIZATION, "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." 54 | "eyJuYW1lIjoiUGV0ZXIgUGFya2VyIn0." 55 | "7grTTYGH6KLzV9uOOEawERIFTKlF3NgdmA3sptiSjD0"} }, 56 | { {"name", "Peter Parker"} } 57 | } 58 | }; 59 | } 60 | 61 | class AuthorizationDataBuilderTest : public TestWithParam 62 | { 63 | public: 64 | void SetUp() 65 | { 66 | m_jwtKey = "my-super-secret-key"; 67 | m_tokenParserService = std::make_unique(m_jsonAdapter); 68 | m_builder = std::make_unique(m_jwtKey, *m_tokenParserService); 69 | } 70 | 71 | protected: 72 | std::unique_ptr m_builder; 73 | 74 | std::string m_jwtKey; 75 | std::unique_ptr m_tokenParserService; 76 | json::rapidjson::JSONAdapter m_jsonAdapter; 77 | }; 78 | 79 | 80 | TEST_P(AuthorizationDataBuilderTest, testBuildAuthorizationData) 81 | { 82 | web_server::RequestHeaders requestHeaders; 83 | for (auto header : GetParam().headers) 84 | { 85 | requestHeaders.addHeader(header.first, header.second); 86 | } 87 | 88 | EndpointRequestAuthorizationClaims expectedClaims; 89 | for (auto claim : GetParam().expectedClaims) 90 | { 91 | expectedClaims.addClaim(claim.first, claim.second); 92 | } 93 | 94 | auto claims = m_builder->buildAuthorizationData(requestHeaders); 95 | ASSERT_TRUE(claims != NULL); 96 | ASSERT_TRUE(EntityComparator()(expectedClaims, *claims)); 97 | } 98 | 99 | INSTANTIATE_TEST_SUITE_P(RESTAPICore, AuthorizationDataBuilderTest, testing::ValuesIn(testData)); 100 | 101 | }}} 102 | -------------------------------------------------------------------------------- /test/RESTAPICoreTest/Tests/Router/RouteTest.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "RESTAPICore/Router/RoutesFactory.h" 3 | #include "RESTAPICore/Router/IRoute.h" 4 | 5 | #include "WebServerAdapterInterface/Model/Headers.h" 6 | #include "WebServerAdapterInterface/Model/Request.h" 7 | 8 | #include "RESTAPICoreTestUtilities/Mocks/Endpoint/MockEndpoint.h" 9 | #include "RESTAPICoreTestUtilities/Mocks/RouteAccess/MockRouteAccessValidator.h" 10 | 11 | #include "TestUtilitiesInterface/EntityComparator.h" 12 | 13 | 14 | using namespace testing; 15 | using namespace systelab::rest_api_core::test_utility; 16 | 17 | namespace systelab { namespace rest_api_core { namespace unit_test { 18 | 19 | class RouteTest : public Test 20 | { 21 | public: 22 | void SetUp() 23 | { 24 | setUpRoutesFactory(); 25 | setUpEndpointFactoryMethod(); 26 | setUpAccessValidatorFactoryMethods(); 27 | } 28 | 29 | void setUpRoutesFactory() 30 | { 31 | m_jwtKey = "ThisIsMyKey"; 32 | m_routesFactory = std::make_unique(m_jwtKey); 33 | } 34 | 35 | void setUpEndpointFactoryMethod() 36 | { 37 | ON_CALL(m_endpoint, executeProxy(_)).WillByDefault(ReturnNull()); 38 | 39 | m_endpointFactoryMethod = 40 | [this]() -> std::unique_ptr 41 | { 42 | auto endpoint = std::make_unique(); 43 | ON_CALL(*endpoint, executeProxy(_)).WillByDefault(Invoke( 44 | [this](const EndpointRequestData& endpointRequestData) -> web_server::Reply* 45 | { 46 | auto reply = std::make_unique(); 47 | reply->setStatus(web_server::Reply::OK); 48 | reply->setContent("{}"); 49 | 50 | m_endpoint.executeProxy(endpointRequestData); 51 | 52 | return reply.release(); 53 | } 54 | )); 55 | 56 | return endpoint; 57 | }; 58 | } 59 | 60 | void setUpAccessValidatorFactoryMethods() 61 | { 62 | m_passAccessValidatorFactoryMethod = 63 | [this]() -> std::unique_ptr 64 | { 65 | auto routeAccessValidator = std::make_unique(); 66 | ON_CALL(*routeAccessValidator, hasAccess(_)).WillByDefault(Return(true)); 67 | return routeAccessValidator; 68 | }; 69 | 70 | m_failAccessValidatorFactoryMethod = 71 | [this]() -> std::unique_ptr 72 | { 73 | auto routeAccessValidator = std::make_unique(); 74 | ON_CALL(*routeAccessValidator, hasAccess(_)).WillByDefault(Return(false)); 75 | return routeAccessValidator; 76 | }; 77 | } 78 | 79 | std::unique_ptr buildSimpleRequest(const std::string& method, 80 | const std::string& uri) 81 | { 82 | auto request = std::make_unique(); 83 | request->setMethod(method); 84 | request->setURI(uri); 85 | 86 | return request; 87 | } 88 | 89 | std::unique_ptr buildRequestWithHeaders(const std::string& method, 90 | const std::string& uri, 91 | const web_server::RequestHeaders& headers) 92 | { 93 | auto request = std::make_unique(); 94 | request->setMethod(method); 95 | request->setURI(uri); 96 | 97 | for (auto headerPair : headers.getHeadersMap()) 98 | { 99 | request->getHeaders().addHeader(headerPair.first, headerPair.second); 100 | } 101 | 102 | return request; 103 | } 104 | 105 | 106 | protected: 107 | std::string m_jwtKey; 108 | std::unique_ptr m_routesFactory; 109 | std::function< std::unique_ptr() > m_endpointFactoryMethod; 110 | MockEndpoint m_endpoint; 111 | std::function< std::unique_ptr() > m_passAccessValidatorFactoryMethod; 112 | std::function< std::unique_ptr() > m_failAccessValidatorFactoryMethod; 113 | }; 114 | 115 | 116 | // Basic routes 117 | TEST_F(RouteTest, testPublicRouteReturnsExpectedReplyWhenMatchingRequest) 118 | { 119 | EndpointRequestData expectedEndpointRequestData; 120 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 121 | 122 | auto route = m_routesFactory->buildRoute("POST", "/rest/api/test", {}, m_endpointFactoryMethod); 123 | auto request = buildSimpleRequest("POST", "/rest/api/test"); 124 | auto reply = route->execute(*request); 125 | 126 | ASSERT_TRUE(reply != NULL); 127 | ASSERT_EQ(web_server::Reply::OK, reply->getStatus()); 128 | ASSERT_EQ("{}", reply->getContent()); 129 | } 130 | 131 | TEST_F(RouteTest, testPublicRouteReturnsNullWhenMethodDoesNotMatch) 132 | { 133 | EXPECT_CALL(m_endpoint, executeProxy(_)).Times(0); 134 | 135 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", {}, m_endpointFactoryMethod); 136 | auto request = buildSimpleRequest("POST", "/rest/api/test"); 137 | auto reply = route->execute(*request); 138 | 139 | ASSERT_TRUE(reply == NULL); 140 | } 141 | 142 | TEST_F(RouteTest, testPublicRouteReturnsNullWhenFirstURIFragmentDoesNotMatch) 143 | { 144 | EXPECT_CALL(m_endpoint, executeProxy(_)).Times(0); 145 | 146 | auto route = m_routesFactory->buildRoute("POST", "/rest/api/test", {}, m_endpointFactoryMethod); 147 | auto request = buildSimpleRequest("POST", "/my/api/test"); 148 | auto reply = route->execute(*request); 149 | 150 | ASSERT_TRUE(reply == NULL); 151 | } 152 | 153 | TEST_F(RouteTest, testPublicRouteReturnsNullWhenLastURIFragmentDoesNotMatch) 154 | { 155 | EXPECT_CALL(m_endpoint, executeProxy(_)).Times(0); 156 | 157 | auto route = m_routesFactory->buildRoute("POST", "/rest/api/test", {}, m_endpointFactoryMethod); 158 | auto request = buildSimpleRequest("POST", "/rest/api/anothertest"); 159 | auto reply = route->execute(*request); 160 | 161 | ASSERT_TRUE(reply == NULL); 162 | } 163 | 164 | TEST_F(RouteTest, testPublicRouteReturnsNullWhenDifferentNumberOfURIFragments) 165 | { 166 | EXPECT_CALL(m_endpoint, executeProxy(_)).Times(0); 167 | 168 | auto route = m_routesFactory->buildRoute("POST", "/rest/api/test", {}, m_endpointFactoryMethod); 169 | auto request = buildSimpleRequest("POST", "/rest/api/v2/test"); 170 | auto reply = route->execute(*request); 171 | 172 | ASSERT_TRUE(reply == NULL); 173 | } 174 | 175 | 176 | // Routes with parameters 177 | TEST_F(RouteTest, testPublicRouteWithSingleNumericParameter) 178 | { 179 | EndpointRequestData expectedEndpointRequestData; 180 | expectedEndpointRequestData.setParameters({ {}, { {"id", 23} } }); 181 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 182 | 183 | auto route = m_routesFactory->buildRoute("POST", "/rest/api/resource/+id", {}, m_endpointFactoryMethod); 184 | auto request = buildSimpleRequest("POST", "/rest/api/resource/23"); 185 | auto reply = route->execute(*request); 186 | 187 | ASSERT_TRUE(reply != NULL); 188 | ASSERT_EQ(web_server::Reply::OK, reply->getStatus()); 189 | ASSERT_EQ("{}", reply->getContent()); 190 | } 191 | 192 | TEST_F(RouteTest, testPublicRouteWithMultipleNumericParameters) 193 | { 194 | EndpointRequestData expectedEndpointRequestData; 195 | expectedEndpointRequestData.setParameters({ {}, { {"id", 54}, {"id2", 23} } }); 196 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 197 | 198 | auto route = m_routesFactory->buildRoute("POST", "/rest/api/resource/+id/subresource/+id2", {}, m_endpointFactoryMethod); 199 | auto request = buildSimpleRequest("POST", "/rest/api/resource/54/subresource/23"); 200 | auto reply = route->execute(*request); 201 | 202 | ASSERT_TRUE(reply != NULL); 203 | ASSERT_EQ(web_server::Reply::OK, reply->getStatus()); 204 | ASSERT_EQ("{}", reply->getContent()); 205 | } 206 | 207 | TEST_F(RouteTest, testPublicRouteWithSingleStringParameter) 208 | { 209 | EndpointRequestData expectedEndpointRequestData; 210 | expectedEndpointRequestData.setParameters({ { {"uuid", "C9C70401-807C-4B57-BE62-FC2723D4C43B"} }, {} }); 211 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 212 | 213 | auto route = m_routesFactory->buildRoute("POST", "/rest/api/resource/:uuid", {}, m_endpointFactoryMethod); 214 | auto request = buildSimpleRequest("POST", "/rest/api/resource/C9C70401-807C-4B57-BE62-FC2723D4C43B"); 215 | auto reply = route->execute(*request); 216 | 217 | ASSERT_TRUE(reply != NULL); 218 | ASSERT_EQ(web_server::Reply::OK, reply->getStatus()); 219 | ASSERT_EQ("{}", reply->getContent()); 220 | } 221 | 222 | TEST_F(RouteTest, testPublicRouteWithMultipleStringParameters) 223 | { 224 | EndpointRequestData expectedEndpointRequestData; 225 | expectedEndpointRequestData.setParameters({ { {"start", "peter"}, {"end", "joe"} }, {} }); 226 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 227 | 228 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/:start/:end", {}, m_endpointFactoryMethod); 229 | auto request = buildSimpleRequest("GET", "/rest/api/peter/joe"); 230 | auto reply = route->execute(*request); 231 | 232 | ASSERT_TRUE(reply != NULL); 233 | ASSERT_EQ(web_server::Reply::OK, reply->getStatus()); 234 | ASSERT_EQ("{}", reply->getContent()); 235 | } 236 | 237 | TEST_F(RouteTest, testPublicRouteWithNumericAndStringParameters) 238 | { 239 | EndpointRequestData expectedEndpointRequestData; 240 | expectedEndpointRequestData.setParameters({ { {"param1", "one"}, {"param3", "three"} }, { {"param2", 2} } }); 241 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 242 | 243 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/:param1/item/+param2/item/:param3", {}, m_endpointFactoryMethod); 244 | auto request = buildSimpleRequest("GET", "/rest/api/one/item/2/item/three"); 245 | auto reply = route->execute(*request); 246 | 247 | ASSERT_TRUE(reply != NULL); 248 | ASSERT_EQ(web_server::Reply::OK, reply->getStatus()); 249 | ASSERT_EQ("{}", reply->getContent()); 250 | } 251 | 252 | TEST_F(RouteTest, testRequestWithURIWithStringParameterCodification) 253 | { 254 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", {}, m_endpointFactoryMethod); 255 | auto request = buildSimpleRequest("GET", "/rest/api/:id"); 256 | auto reply = route->execute(*request); 257 | ASSERT_TRUE(reply == NULL); 258 | } 259 | 260 | TEST_F(RouteTest, testRequestWithURIWithNumericParameterCodification) 261 | { 262 | EXPECT_CALL(m_endpoint, executeProxy(_)).Times(0); 263 | 264 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", {}, m_endpointFactoryMethod); 265 | auto request = buildSimpleRequest("GET", "/rest/api/+id"); 266 | auto reply = route->execute(*request); 267 | ASSERT_TRUE(reply == NULL); 268 | } 269 | 270 | 271 | // Headers & Authorization data 272 | TEST_F(RouteTest, testRequestWithAnotherHeaderCallsEndpointWithEmptyAuthorizationData) 273 | { 274 | web_server::RequestHeaders requestHeaders; 275 | requestHeaders.addHeader(web_server::CONTENT_TYPE, "application/json"); 276 | 277 | EndpointRequestData expectedEndpointRequestData; 278 | expectedEndpointRequestData.setHeaders(requestHeaders); 279 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 280 | 281 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", {}, m_endpointFactoryMethod); 282 | auto request = buildRequestWithHeaders("GET", "/rest/api/test", requestHeaders); 283 | route->execute(*request); 284 | } 285 | 286 | TEST_F(RouteTest, testRequestWithAuthorizationHeaderWithoutProtocolCallsEndpointWithEmptyAuthorizationData) 287 | { 288 | std::string authorizationHeader = "anNtaXRoOm15cGFzc3dvcmQ="; 289 | web_server::RequestHeaders requestHeaders; 290 | requestHeaders.addHeader(web_server::AUTHORIZATION, authorizationHeader); 291 | 292 | EndpointRequestData expectedEndpointRequestData; 293 | expectedEndpointRequestData.setHeaders(requestHeaders); 294 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 295 | 296 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", {}, m_endpointFactoryMethod); 297 | auto request = buildRequestWithHeaders("GET", "/rest/api/test", requestHeaders); 298 | route->execute(*request); 299 | } 300 | 301 | TEST_F(RouteTest, testRequestWithAuthorizationHeaderForAnotherProtocolCallsEndpointWithEmptyAuthorizationData) 302 | { 303 | std::string authorizationHeader = "Basic anNtaXRoOm15cGFzc3dvcmQ="; 304 | web_server::RequestHeaders requestHeaders; 305 | requestHeaders.addHeader(web_server::AUTHORIZATION, authorizationHeader); 306 | 307 | EndpointRequestData expectedEndpointRequestData; 308 | expectedEndpointRequestData.setHeaders(requestHeaders); 309 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 310 | 311 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", {}, m_endpointFactoryMethod); 312 | auto request = buildRequestWithHeaders("GET", "/rest/api/test", requestHeaders); 313 | route->execute(*request); 314 | } 315 | 316 | TEST_F(RouteTest, testRequestWithAuthorizationHeaderWithBearerAndInvalidDataCallsEndpointWithEmptyAuthorizationData) 317 | { 318 | std::string authorizationHeader = "Bearer anNtaXRoOm15cGFzc3dvcmQ="; 319 | web_server::RequestHeaders requestHeaders; 320 | requestHeaders.addHeader(web_server::AUTHORIZATION, authorizationHeader); 321 | 322 | EndpointRequestData expectedEndpointRequestData; 323 | expectedEndpointRequestData.setHeaders(requestHeaders); 324 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 325 | 326 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", {}, m_endpointFactoryMethod); 327 | auto request = buildRequestWithHeaders("GET", "/rest/api/test", requestHeaders); 328 | route->execute(*request); 329 | } 330 | 331 | TEST_F(RouteTest, testRequestWithAuthorizationHeaderWithBearerAndValidDataCallsEndpointWithAuthorizationDataClaims) 332 | { 333 | std::string authorizationHeader = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9." 334 | "eyJzdWIiOiJqc21pdGgifQ.0-GCEvX4THSns23xDx2awrWPGPOtvJcEkQbma4nGWZg"; 335 | web_server::RequestHeaders requestHeaders; 336 | requestHeaders.addHeader(web_server::AUTHORIZATION, authorizationHeader); 337 | 338 | EndpointRequestAuthorizationClaims expectedAuthorizationClaims; 339 | expectedAuthorizationClaims.addClaim("sub", "jsmith"); 340 | 341 | EndpointRequestData expectedEndpointRequestData; 342 | expectedEndpointRequestData.setHeaders(requestHeaders); 343 | expectedEndpointRequestData.setAuthorizationClaims(expectedAuthorizationClaims); 344 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 345 | 346 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", {}, m_endpointFactoryMethod); 347 | auto request = buildRequestWithHeaders("GET", "/rest/api/test", requestHeaders); 348 | route->execute(*request); 349 | } 350 | 351 | 352 | // Query strings 353 | TEST_F(RouteTest, testRequestWithQueryStringsCallsEndpointWithSameQueryStrings) 354 | { 355 | web_server::RequestQueryStrings requestQueryStrings; 356 | requestQueryStrings.addItem("sort", "field"); 357 | requestQueryStrings.addItem("page", "23"); 358 | 359 | EndpointRequestData expectedEndpointRequestData; 360 | expectedEndpointRequestData.setQueryStrings(requestQueryStrings); 361 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 362 | 363 | auto request = buildSimpleRequest("GET", "/rest/api/test"); 364 | for (auto queryStringItem : requestQueryStrings.getItemsMap()) 365 | { 366 | request->getQueryStrings().addItem(queryStringItem.first, queryStringItem.second); 367 | } 368 | 369 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", {}, m_endpointFactoryMethod); 370 | route->execute(*request); 371 | } 372 | 373 | 374 | // Content 375 | TEST_F(RouteTest, testRequestWithContentCallsEndpointWithSameContent) 376 | { 377 | std::string requestContent = "This is a fancy request content"; 378 | 379 | EndpointRequestData expectedEndpointRequestData; 380 | expectedEndpointRequestData.setContent(requestContent); 381 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 382 | 383 | auto request = buildSimpleRequest("GET", "/rest/api/test"); 384 | request->setContent(requestContent); 385 | 386 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", {}, m_endpointFactoryMethod); 387 | route->execute(*request); 388 | } 389 | 390 | 391 | // Access validators 392 | TEST_F(RouteTest, testRouteWithSingleAccessValidatorAndNoAuthorizationHeaderReturnsUnauthorizedReply) 393 | { 394 | EXPECT_CALL(m_endpoint, executeProxy(_)).Times(0); 395 | 396 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", { m_passAccessValidatorFactoryMethod }, m_endpointFactoryMethod); 397 | auto request = buildSimpleRequest("GET", "/rest/api/test"); 398 | auto reply = route->execute(*request); 399 | 400 | ASSERT_TRUE(reply != NULL); 401 | ASSERT_EQ(web_server::Reply::UNAUTHORIZED, reply->getStatus()); 402 | ASSERT_EQ("{}", reply->getContent()); 403 | } 404 | 405 | TEST_F(RouteTest, testRouteWithSingleAccessValidatorThatFailsReturnsForbiddenReply) 406 | { 407 | EXPECT_CALL(m_endpoint, executeProxy(_)).Times(0); 408 | 409 | web_server::RequestHeaders requestHeaders; 410 | requestHeaders.addHeader(web_server::AUTHORIZATION, "anNtaXRoOm15cGFzc3dvcmQ="); 411 | auto request = buildRequestWithHeaders("GET", "/rest/api/test", requestHeaders); 412 | 413 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", { m_failAccessValidatorFactoryMethod }, m_endpointFactoryMethod); 414 | auto reply = route->execute(*request); 415 | 416 | ASSERT_TRUE(reply != NULL); 417 | ASSERT_EQ(web_server::Reply::FORBIDDEN, reply->getStatus()); 418 | ASSERT_EQ("{}", reply->getContent()); 419 | } 420 | 421 | TEST_F(RouteTest, testRouteWithSingleAccessValidatorThatPassesCallsEndpoint) 422 | { 423 | web_server::RequestHeaders requestHeaders; 424 | requestHeaders.addHeader(web_server::AUTHORIZATION, "anNtaXRoOm15cGFzc3dvcmQ="); 425 | 426 | EndpointRequestData expectedEndpointRequestData; 427 | expectedEndpointRequestData.setHeaders(requestHeaders); 428 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 429 | 430 | auto request = buildRequestWithHeaders("GET", "/rest/api/test", requestHeaders); 431 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", { m_passAccessValidatorFactoryMethod }, m_endpointFactoryMethod); 432 | auto reply = route->execute(*request); 433 | 434 | ASSERT_TRUE(reply != NULL); 435 | ASSERT_EQ(web_server::Reply::OK, reply->getStatus()); 436 | ASSERT_EQ("{}", reply->getContent()); 437 | } 438 | 439 | TEST_F(RouteTest, testRouteWithMultipleAccessValidatorsAndOneFailsReturnsForbiddenReply) 440 | { 441 | EXPECT_CALL(m_endpoint, executeProxy(_)).Times(0); 442 | 443 | web_server::RequestHeaders requestHeaders; 444 | requestHeaders.addHeader(web_server::AUTHORIZATION, "anNtaXRoOm15cGFzc3dvcmQ="); 445 | auto request = buildRequestWithHeaders("GET", "/rest/api/test", requestHeaders); 446 | 447 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", 448 | { m_passAccessValidatorFactoryMethod, m_failAccessValidatorFactoryMethod, 449 | m_passAccessValidatorFactoryMethod }, m_endpointFactoryMethod); 450 | auto reply = route->execute(*request); 451 | 452 | ASSERT_TRUE(reply != NULL); 453 | ASSERT_EQ(web_server::Reply::FORBIDDEN, reply->getStatus()); 454 | ASSERT_EQ("{}", reply->getContent()); 455 | } 456 | 457 | TEST_F(RouteTest, testRouteWithMultipleAccessValidatorsAndAllPassCallsEndpoint) 458 | { 459 | web_server::RequestHeaders requestHeaders; 460 | requestHeaders.addHeader(web_server::AUTHORIZATION, "anNtaXRoOm15cGFzc3dvcmQ="); 461 | 462 | EndpointRequestData expectedEndpointRequestData; 463 | expectedEndpointRequestData.setHeaders(requestHeaders); 464 | EXPECT_CALL(m_endpoint, executeProxy(isEqTo(expectedEndpointRequestData))); 465 | 466 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", 467 | { m_passAccessValidatorFactoryMethod, m_passAccessValidatorFactoryMethod, 468 | m_passAccessValidatorFactoryMethod }, m_endpointFactoryMethod); 469 | auto request = buildRequestWithHeaders("GET", "/rest/api/test", requestHeaders); 470 | auto reply = route->execute(*request); 471 | 472 | ASSERT_TRUE(reply != NULL); 473 | ASSERT_EQ(web_server::Reply::OK, reply->getStatus()); 474 | ASSERT_EQ("{}", reply->getContent()); 475 | } 476 | 477 | 478 | // Error handling 479 | TEST_F(RouteTest, testUnableToBuildEndpointReturnsInternalServerErrorReply) 480 | { 481 | m_endpointFactoryMethod = 482 | [this]() -> std::unique_ptr 483 | { 484 | return std::unique_ptr(); 485 | }; 486 | 487 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", {}, m_endpointFactoryMethod); 488 | auto request = buildSimpleRequest("GET", "/rest/api/test"); 489 | auto reply = route->execute(*request); 490 | 491 | ASSERT_TRUE(reply != NULL); 492 | ASSERT_EQ(web_server::Reply::INTERNAL_SERVER_ERROR, reply->getStatus()); 493 | ASSERT_EQ("{ \"reason\": \"Unable to build endpoint for GET /rest/api/test\"}", reply->getContent()); 494 | } 495 | 496 | TEST_F(RouteTest, testEndpointExceptionReturnsInternalServerErrorReply) 497 | { 498 | m_endpointFactoryMethod = 499 | [this]() -> std::unique_ptr 500 | { 501 | auto endpoint = std::make_unique(); 502 | ON_CALL(*endpoint, executeProxy(_)).WillByDefault(Throw(std::runtime_error("Thrown error details"))); 503 | return endpoint; 504 | }; 505 | 506 | auto route = m_routesFactory->buildRoute("GET", "/rest/api/test", {}, m_endpointFactoryMethod); 507 | auto request = buildSimpleRequest("GET", "/rest/api/test"); 508 | auto reply = route->execute(*request); 509 | 510 | ASSERT_TRUE(reply != NULL); 511 | ASSERT_EQ(web_server::Reply::INTERNAL_SERVER_ERROR, reply->getStatus()); 512 | ASSERT_EQ("{ \"reason\": \"Unknown error while processing endpoint " 513 | "GET /rest/api/test: Thrown error details\"}", reply->getContent()); 514 | } 515 | 516 | 517 | }}} 518 | -------------------------------------------------------------------------------- /test/RESTAPICoreTest/Tests/Router/RouterTest.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "RESTAPICore/Router/Router.h" 3 | 4 | #include "RESTAPICoreTestUtilities/Mocks/Router/MockRoute.h" 5 | 6 | #include "TestUtilitiesInterface/EntityComparator.h" 7 | 8 | 9 | using namespace testing; 10 | using namespace systelab::rest_api_core::test_utility; 11 | 12 | namespace systelab { namespace rest_api_core { namespace unit_test { 13 | 14 | class RouterTest : public Test 15 | { 16 | public: 17 | void SetUp() 18 | { 19 | m_router = std::make_unique(); 20 | m_router->addRoute(buildRoute("GET")); 21 | m_router->addRoute(buildRoute("POST")); 22 | m_router->addRoute(buildRoute("PUT")); 23 | m_router->addRoute(buildRoute("DELETE")); 24 | } 25 | 26 | std::unique_ptr buildRoute(const std::string& method) 27 | { 28 | auto route = std::make_unique(); 29 | ON_CALL(*route, executeProxy(_)).WillByDefault(Invoke( 30 | [method](const web_server::Request& request) -> web_server::Reply* 31 | { 32 | if (request.getMethod() == method) 33 | { 34 | return new web_server::Reply(web_server::Reply::OK, {}, method); 35 | } 36 | 37 | return NULL; 38 | } 39 | )); 40 | 41 | return route; 42 | } 43 | 44 | protected: 45 | std::unique_ptr m_router; 46 | }; 47 | 48 | 49 | TEST_F(RouterTest, testProcessGETRequestReturnsReplyStatusOKAndGETOnContent) 50 | { 51 | web_server::Request request; 52 | request.setMethod("GET"); 53 | auto reply = m_router->process(request); 54 | ASSERT_TRUE(reply != NULL); 55 | ASSERT_EQ(reply->getStatus(), web_server::Reply::OK); 56 | ASSERT_EQ("GET", reply->getContent()); 57 | } 58 | 59 | TEST_F(RouterTest, testProcessPOSTRequestReturnsReplyStatusOKAndPOSTOnContent) 60 | { 61 | web_server::Request request; 62 | request.setMethod("POST"); 63 | auto reply = m_router->process(request); 64 | ASSERT_TRUE(reply != NULL); 65 | ASSERT_EQ(reply->getStatus(), web_server::Reply::OK); 66 | ASSERT_EQ("POST", reply->getContent()); 67 | } 68 | 69 | TEST_F(RouterTest, testProcessPUTRequestReturnsReplyStatusOKAndPUTOnContent) 70 | { 71 | web_server::Request request; 72 | request.setMethod("PUT"); 73 | auto reply = m_router->process(request); 74 | ASSERT_TRUE(reply != NULL); 75 | ASSERT_EQ(reply->getStatus(), web_server::Reply::OK); 76 | ASSERT_EQ("PUT", reply->getContent()); 77 | } 78 | 79 | TEST_F(RouterTest, testProcessDELETERequestReturnsReplyStatusOKAndDELETEOnContent) 80 | { 81 | web_server::Request request; 82 | request.setMethod("DELETE"); 83 | auto reply = m_router->process(request); 84 | ASSERT_TRUE(reply != NULL); 85 | ASSERT_EQ(reply->getStatus(), web_server::Reply::OK); 86 | ASSERT_EQ("DELETE", reply->getContent()); 87 | } 88 | 89 | TEST_F(RouterTest, testProcessPATCHRequestReturnsNull) 90 | { 91 | web_server::Request request; 92 | request.setMethod("PATCH"); 93 | auto reply = m_router->process(request); 94 | ASSERT_TRUE(reply == NULL); 95 | } 96 | 97 | }}} 98 | -------------------------------------------------------------------------------- /test/RESTAPICoreTest/Tests/RouterComponentTest.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | 3 | #include "RESTAPICore/Endpoint/ClaimConstants.h" 4 | #include "RESTAPICore/RouteAccess/TokenExpirationAccessValidator.h" 5 | #include "RESTAPICore/RouteAccess/UserRoleRouteAccessValidator.h" 6 | #include "RESTAPICore/Router/IRoute.h" 7 | #include "RESTAPICore/Router/Router.h" 8 | #include "RESTAPICore/Router/RoutesFactory.h" 9 | 10 | #include "RapidJSONAdapter/JSONAdapter.h" 11 | 12 | #include "JWTUtils/Services/TokenBuilderService.h" 13 | 14 | #include "WebServerAdapterInterface/Model/Headers.h" 15 | #include "WebServerAdapterInterface/Model/Reply.h" 16 | #include "WebServerAdapterInterface/Model/Request.h" 17 | 18 | #include "RESTAPICoreTestUtilities/Mocks/Endpoint/MockEndpoint.h" 19 | #include "RESTAPICoreTestUtilities/Mocks/RouteAccess/MockUserRoleService.h" 20 | 21 | #include "TestUtilitiesInterface/EntityComparator.h" 22 | #include "JSONAdapterTestUtilities/JSONAdapterUtilities.h" 23 | #include "TimeAdapterTestUtilities/Stubs/StubTimeAdapter.h" 24 | 25 | #include 26 | 27 | 28 | using namespace testing; 29 | using namespace systelab::json::test_utility; 30 | using namespace systelab::rest_api_core::test_utility; 31 | using namespace systelab::time::test_utility; 32 | 33 | namespace systelab { namespace rest_api_core { namespace unit_test { 34 | 35 | namespace { 36 | enum class UserRole 37 | { 38 | ANONYMOUS = 0, 39 | BASIC = 1, 40 | ADMIN = 2 41 | }; 42 | } 43 | 44 | class RouterComponentTest : public Test 45 | { 46 | public: 47 | void SetUp() 48 | { 49 | setUpJWTBuilderService(); 50 | setUpRoutesFactory(); 51 | setUpAccessValidatorFactoryMethods(); 52 | setUpUserRoleService(); 53 | 54 | m_router = std::make_unique(); 55 | m_router->addRoute(buildRoute("GET", "/rest/api/users", UserRole::BASIC)); 56 | m_router->addRoute(buildRoute("GET", "/rest/api/users/+id", UserRole::BASIC)); 57 | m_router->addRoute(buildRoute("POST", "/rest/api/users", UserRole::ADMIN)); 58 | m_router->addRoute(buildRoute("PUT", "/rest/api/users/+id", UserRole::ADMIN)); 59 | m_router->addRoute(buildRoute("DELETE", "/rest/api/users/+id", UserRole::ADMIN)); 60 | m_router->addRoute(buildRoute("POST", "/rest/api/users/login", UserRole::ANONYMOUS)); 61 | } 62 | 63 | void setUpJWTBuilderService() 64 | { 65 | m_jwtKey = "RouterComponentTestKey"; 66 | m_tokenBuilderService = std::make_unique(m_jsonAdapter); 67 | } 68 | 69 | void setUpRoutesFactory() 70 | { 71 | std::map headers = { {"Content-Type", "application/json"}, {"CustomHeader", "CustomValue"} }; 72 | m_unauthorizedReply = systelab::web_server::Reply(systelab::web_server::Reply::UNAUTHORIZED, headers, "{ \"message\": \"Unauthorized reply\" }"); 73 | m_forbiddenReply = systelab::web_server::Reply(systelab::web_server::Reply::FORBIDDEN, headers, "{ \"message\": \"Forbidden reply\" }"); 74 | 75 | m_routesFactory = std::make_unique(m_jwtKey); 76 | m_routesFactory->setUnauthorizedReply(m_unauthorizedReply); 77 | m_routesFactory->setForbiddenReply(m_forbiddenReply); 78 | } 79 | 80 | void setUpAccessValidatorFactoryMethods() 81 | { 82 | m_tokenExpirationAccessValidator = 83 | [this]() -> std::unique_ptr 84 | { 85 | long expirationSeconds = 600; // 10 minutes 86 | return std::make_unique(m_timeAdapter, expirationSeconds); 87 | }; 88 | 89 | m_adminUserAccessValidator = 90 | [this]() -> std::unique_ptr 91 | { 92 | std::vector allowedRoles = { "Admin" }; 93 | return std::make_unique(allowedRoles, m_userRoleService); 94 | }; 95 | 96 | m_basicUserAccessValidator = 97 | [this]() -> std::unique_ptr 98 | { 99 | std::vector allowedRoles = { "Admin", "Basic" }; 100 | return std::make_unique(allowedRoles, m_userRoleService); 101 | }; 102 | } 103 | 104 | void setUpUserRoleService() 105 | { 106 | ON_CALL(m_userRoleService, getUserRoles(_)).WillByDefault(Return(std::vector())); 107 | 108 | m_adminUsername = "Batman"; 109 | ON_CALL(m_userRoleService, getUserRoles(m_adminUsername)).WillByDefault(Invoke( 110 | [this](const std::string&) -> std::vector 111 | { 112 | return {"Admin"}; 113 | } 114 | )); 115 | 116 | m_basicUsername = "Robin"; 117 | ON_CALL(m_userRoleService, getUserRoles(m_basicUsername)).WillByDefault(Invoke( 118 | [this](const std::string&) -> std::vector 119 | { 120 | return {"Basic"}; 121 | } 122 | )); 123 | } 124 | 125 | std::unique_ptr buildRoute(const std::string& method, 126 | const std::string& uri, 127 | UserRole role) 128 | { 129 | std::vector< std::function()> > accessValidators; 130 | if (static_cast(role) > 0) 131 | { 132 | accessValidators.push_back(m_tokenExpirationAccessValidator); 133 | } 134 | 135 | if (role == UserRole::ADMIN) 136 | { 137 | accessValidators.push_back(m_adminUserAccessValidator); 138 | } 139 | else if (role == UserRole::BASIC) 140 | { 141 | accessValidators.push_back(m_basicUserAccessValidator); 142 | } 143 | 144 | return m_routesFactory->buildRoute(method, uri, accessValidators, buildEndpointFactoryMethod(method, uri)); 145 | } 146 | 147 | std::function< std::unique_ptr() > buildEndpointFactoryMethod(const std::string& method, const std::string& uri) 148 | { 149 | return [method, uri]() -> std::unique_ptr 150 | { 151 | auto endpoint = std::make_unique(); 152 | ON_CALL(*endpoint, executeProxy(_)).WillByDefault(Invoke( 153 | [method, uri](const EndpointRequestData& endpointRequestData) -> web_server::Reply* 154 | { 155 | std::stringstream ss; 156 | ss << "{"; 157 | ss << " \"method\": \"" << method << "\", "; 158 | ss << " \"uri\": \"" << uri << "\" "; 159 | ss << "}"; 160 | std::string content = ss.str(); 161 | 162 | auto reply = std::make_unique(); 163 | reply->setStatus(web_server::Reply::OK); 164 | reply->setContent(content); 165 | return reply.release(); 166 | } 167 | )); 168 | 169 | return endpoint; 170 | }; 171 | } 172 | 173 | web_server::Request buildRequest(const std::string& method, 174 | const std::string& uri, 175 | const std::string& username = "") 176 | { 177 | web_server::Request request; 178 | request.setHttpVersionMajor(1); 179 | request.setHttpVersionMinor(1); 180 | request.setMethod(method); 181 | request.setURI(uri); 182 | request.setContent("{}"); 183 | 184 | if (!username.empty()) 185 | { 186 | std::vector< std::pair > authorizationClaims; 187 | authorizationClaims.push_back( std::make_pair(claim::SUBJECT, username) ); 188 | authorizationClaims.push_back( std::make_pair(claim::ISSUED_AT, std::to_string(std::chrono::system_clock::to_time_t(m_timeAdapter.getCurrentUTCTime()))) ); 189 | std::string token = m_tokenBuilderService->buildJWT(m_jwtKey, authorizationClaims); 190 | request.getHeaders().addHeader(web_server::AUTHORIZATION, "Bearer " + token); 191 | } 192 | 193 | return request; 194 | } 195 | 196 | std::string buildExpectedContentJSON(const std::string& method, const std::string& uri) 197 | { 198 | std::stringstream ss; 199 | ss << "{"; 200 | ss << " \"method\": \"" << method << "\", "; 201 | ss << " \"uri\": \"" << uri << "\" "; 202 | ss << "}"; 203 | 204 | return ss.str(); 205 | } 206 | 207 | protected: 208 | std::unique_ptr m_router; 209 | 210 | systelab::json::rapidjson::JSONAdapter m_jsonAdapter; 211 | std::unique_ptr m_tokenBuilderService; 212 | std::string m_jwtKey; 213 | 214 | std::unique_ptr m_routesFactory; 215 | StubTimeAdapter m_timeAdapter; 216 | MockUserRoleService m_userRoleService; 217 | std::string m_adminUsername; 218 | std::string m_basicUsername; 219 | 220 | std::function< std::unique_ptr() > m_tokenExpirationAccessValidator; 221 | std::function< std::unique_ptr() > m_adminUserAccessValidator; 222 | std::function< std::unique_ptr() > m_basicUserAccessValidator; 223 | 224 | systelab::web_server::Reply m_unauthorizedReply; 225 | systelab::web_server::Reply m_forbiddenReply; 226 | }; 227 | 228 | 229 | // Happy path requests 230 | TEST_F(RouterComponentTest, testProcessGETForRESTAPIUsersWithBasicRoleReturnsReplyStatusOK) 231 | { 232 | auto reply = m_router->process(buildRequest("GET", "/rest/api/users", m_basicUsername)); 233 | ASSERT_TRUE(reply != NULL); 234 | EXPECT_EQ(reply->getStatus(), web_server::Reply::OK); 235 | EXPECT_TRUE(compareJSONs(buildExpectedContentJSON("GET", "/rest/api/users"), reply->getContent(), m_jsonAdapter)); 236 | } 237 | 238 | TEST_F(RouterComponentTest, testProcessGETForRESTAPIUsers123WithBasicRoleReturnsReplyStatusOK) 239 | { 240 | auto reply = m_router->process(buildRequest("GET", "/rest/api/users/123", m_basicUsername)); 241 | ASSERT_TRUE(reply != NULL); 242 | EXPECT_EQ(reply->getStatus(), web_server::Reply::OK); 243 | EXPECT_TRUE(compareJSONs(buildExpectedContentJSON("GET", "/rest/api/users/+id"), reply->getContent(), m_jsonAdapter)); 244 | } 245 | 246 | TEST_F(RouterComponentTest, testProcessPOSTForRESTAPIUsersWithAdminRoleReturnsReplyStatusOK) 247 | { 248 | auto reply = m_router->process(buildRequest("POST", "/rest/api/users/", m_adminUsername)); 249 | ASSERT_TRUE(reply != NULL); 250 | EXPECT_EQ(reply->getStatus(), web_server::Reply::OK); 251 | EXPECT_TRUE(compareJSONs(buildExpectedContentJSON("POST", "/rest/api/users"), reply->getContent(), m_jsonAdapter)); 252 | } 253 | 254 | TEST_F(RouterComponentTest, testProcessPUTForRESTAPIUsers456WithAdminRoleReturnsReplyStatusOK) 255 | { 256 | auto reply = m_router->process(buildRequest("PUT", "/rest/api/users/456", m_adminUsername)); 257 | ASSERT_TRUE(reply != NULL); 258 | EXPECT_EQ(reply->getStatus(), web_server::Reply::OK); 259 | EXPECT_TRUE(compareJSONs(buildExpectedContentJSON("PUT", "/rest/api/users/+id"), reply->getContent(), m_jsonAdapter)); 260 | } 261 | 262 | TEST_F(RouterComponentTest, testProcessDELETEForRESTAPIUsers789WithAdminRoleReturnsReplyStatusOK) 263 | { 264 | auto reply = m_router->process(buildRequest("DELETE", "/rest/api/users/789", m_adminUsername)); 265 | ASSERT_TRUE(reply != NULL); 266 | EXPECT_EQ(reply->getStatus(), web_server::Reply::OK); 267 | EXPECT_TRUE(compareJSONs(buildExpectedContentJSON("DELETE", "/rest/api/users/+id"), reply->getContent(), m_jsonAdapter)); 268 | } 269 | 270 | TEST_F(RouterComponentTest, testProcessPOSTForRESTAPIUsersLoginWithAnonymousRequestReturnsReplyStatusOK) 271 | { 272 | auto reply = m_router->process(buildRequest("POST", "/rest/api/users/login")); 273 | ASSERT_TRUE(reply != NULL); 274 | EXPECT_EQ(reply->getStatus(), web_server::Reply::OK); 275 | EXPECT_TRUE(compareJSONs(buildExpectedContentJSON("POST", "/rest/api/users/login"), reply->getContent(), m_jsonAdapter)); 276 | } 277 | 278 | 279 | // Requests without access due to user role 280 | TEST_F(RouterComponentTest, testProcessDELETEForRESTAPIUsers789WithAnonymousRequestReturnsConfiguredUnauthorizedReply) 281 | { 282 | auto reply = m_router->process(buildRequest("DELETE", "/rest/api/users/789")); 283 | ASSERT_TRUE(reply != NULL); 284 | EXPECT_TRUE(systelab::test_utility::EntityComparator()(m_unauthorizedReply, *reply)); 285 | } 286 | 287 | TEST_F(RouterComponentTest, testProcessDELETEForRESTAPIUsers789WithBasicRoleReturnsConfiguredForbiddenReply) 288 | { 289 | auto reply = m_router->process(buildRequest("DELETE", "/rest/api/users/789", m_basicUsername)); 290 | ASSERT_TRUE(reply != NULL); 291 | EXPECT_TRUE(systelab::test_utility::EntityComparator()(m_forbiddenReply, *reply)); 292 | } 293 | 294 | TEST_F(RouterComponentTest, testProcessGETForRESTAPIUsers789WithAnonymousRequestReturnsConfiguredUnauthorizedReply) 295 | { 296 | auto reply = m_router->process(buildRequest("GET", "/rest/api/users/789")); 297 | ASSERT_TRUE(reply != NULL); 298 | EXPECT_TRUE(systelab::test_utility::EntityComparator()(m_unauthorizedReply, *reply)); 299 | } 300 | 301 | 302 | // Requests without access due to expired token 303 | TEST_F(RouterComponentTest, testProcessGETForRESTAPIUsersWithBasicRoleButExpiredTokenReturnsConfiguredForbiddenReply) 304 | { 305 | auto request = buildRequest("GET", "/rest/api/users", m_basicUsername); 306 | m_timeAdapter.addSeconds(601); 307 | auto reply = m_router->process(request); 308 | ASSERT_TRUE(reply != NULL); 309 | EXPECT_TRUE(systelab::test_utility::EntityComparator()(m_forbiddenReply, *reply)); 310 | } 311 | 312 | 313 | // Requests for not existing routes 314 | TEST_F(RouterComponentTest, testProcessGETForRESTAPIUsersIdReturnsNullReply) 315 | { 316 | ASSERT_TRUE(m_router->process(buildRequest("GET", "/rest/api/users/+id")) == NULL); 317 | } 318 | 319 | TEST_F(RouterComponentTest, testProcessGETForRESTAPIUsersSomethingReturnsNullReply) 320 | { 321 | ASSERT_TRUE(m_router->process(buildRequest("GET", "/rest/api/users/something")) == NULL); 322 | } 323 | 324 | TEST_F(RouterComponentTest, testProcessPUTForRESTAPIUsersLoginReturnsNullReply) 325 | { 326 | ASSERT_TRUE(m_router->process(buildRequest("PUT", "/rest/api/users/login")) == NULL); 327 | } 328 | 329 | 330 | }}} 331 | -------------------------------------------------------------------------------- /test/RESTAPICoreTest/stdafx.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING 1 4 | 5 | // STL 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | // GTEST 13 | #include 14 | #include 15 | 16 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Builders/EndpointRequestParamsBuilder.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "EndpointRequestParamsBuilder.h" 3 | 4 | 5 | namespace systelab { namespace rest_api_core { namespace test_utility { 6 | 7 | EndpointRequestParamsBuilder::EndpointRequestParamsBuilder() 8 | :m_entity() 9 | { 10 | } 11 | 12 | EndpointRequestParamsBuilder::~EndpointRequestParamsBuilder() = default; 13 | 14 | EndpointRequestParamsBuilder& EndpointRequestParamsBuilder::addStringParameter(const std::string& name, const std::string& value) 15 | { 16 | m_entity.addStringParameter(name, value); 17 | return *this; 18 | } 19 | 20 | EndpointRequestParamsBuilder& EndpointRequestParamsBuilder::addNumericParameter(const std::string& name, unsigned int value) 21 | { 22 | m_entity.addNumericParameter(name, value); 23 | return *this; 24 | } 25 | 26 | EndpointRequestParams EndpointRequestParamsBuilder::getEntity() const 27 | { 28 | return m_entity; 29 | } 30 | 31 | }}} 32 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Builders/EndpointRequestParamsBuilder.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RESTAPICore/Endpoint/EndpointRequestParams.h" 4 | 5 | 6 | namespace systelab { namespace rest_api_core { namespace test_utility { 7 | 8 | class EndpointRequestParamsBuilder 9 | { 10 | public: 11 | EndpointRequestParamsBuilder(); 12 | virtual ~EndpointRequestParamsBuilder(); 13 | 14 | EndpointRequestParamsBuilder& addStringParameter(const std::string& name, const std::string& value); 15 | EndpointRequestParamsBuilder& addNumericParameter(const std::string& name, unsigned int value); 16 | 17 | EndpointRequestParams getEntity() const; 18 | 19 | private: 20 | EndpointRequestParams m_entity; 21 | }; 22 | 23 | }}} 24 | 25 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2) 2 | 3 | # Add project folder into includes 4 | set(CMAKE_INCLUDE_CURRENT_DIR ON) 5 | 6 | # Find external dependencides 7 | set(GTEST_PACKAGE_NAME gtest) 8 | find_package(${GTEST_PACKAGE_NAME}) 9 | if (NOT TARGET ${GTEST_PACKAGE_NAME}::${GTEST_PACKAGE_NAME}) 10 | set(GTEST_PACKAGE_NAME GTest) 11 | find_package(${GTEST_PACKAGE_NAME}) 12 | endif() 13 | 14 | find_package(TestUtilitiesInterface) 15 | 16 | # Configure test project 17 | set(REST_API_CORE_TEST_UTILITIES_PROJECT RESTAPICoreTestUtilities) 18 | file(GLOB_RECURSE REST_API_CORE_TEST_UTILITIES_PROJECT_SRC "*.cpp") 19 | file(GLOB_RECURSE REST_API_CORE_TEST_UTILITIES_PROJECT_HDR "*.h") 20 | 21 | set(EXCLUDE_DIR test_package) 22 | foreach (TMP_PATH ${REST_API_CORE_TEST_UTILITIES_PROJECT_SRC}) 23 | string (FIND ${TMP_PATH} ${EXCLUDE_DIR} EXCLUDE_DIR_FOUND) 24 | if (NOT ${EXCLUDE_DIR_FOUND} EQUAL -1) 25 | list (REMOVE_ITEM REST_API_CORE_TEST_UTILITIES_PROJECT_SRC ${TMP_PATH}) 26 | endif () 27 | endforeach(TMP_PATH) 28 | 29 | foreach (TMP_PATH ${REST_API_CORE_TEST_UTILITIES_PROJECT_HDR}) 30 | string (FIND ${TMP_PATH} ${EXCLUDE_DIR} EXCLUDE_DIR_FOUND) 31 | if (NOT ${EXCLUDE_DIR_FOUND} EQUAL -1) 32 | list (REMOVE_ITEM REST_API_CORE_TEST_UTILITIES_PROJECT_HDR ${TMP_PATH}) 33 | endif () 34 | endforeach(TMP_PATH) 35 | 36 | add_library(${REST_API_CORE_TEST_UTILITIES_PROJECT} STATIC ${REST_API_CORE_TEST_UTILITIES_PROJECT_SRC} ${REST_API_CORE_TEST_UTILITIES_PROJECT_HDR}) 37 | target_link_libraries(${REST_API_CORE_TEST_UTILITIES_PROJECT} 38 | RESTAPICore 39 | ${GTEST_PACKAGE_NAME}::${GTEST_PACKAGE_NAME} 40 | TestUtilitiesInterface::TestUtilitiesInterface) 41 | 42 | #Configure source groups 43 | foreach(FILE ${REST_API_CORE_TEST_UTILITIES_PROJECT_SRC} ${REST_API_CORE_TEST_UTILITIES_PROJECT_HDR}) 44 | get_filename_component(PARENT_DIR "${FILE}" DIRECTORY) 45 | string(REPLACE "${CMAKE_CURRENT_SOURCE_DIR}" "" GROUP "${PARENT_DIR}") 46 | string(REPLACE "/" "\\" GROUP "${GROUP}") 47 | 48 | if ("${FILE}" MATCHES ".*\\.cpp") 49 | set(GROUP "Source Files${GROUP}") 50 | elseif("${FILE}" MATCHES ".*\\.h") 51 | set(GROUP "Header Files${GROUP}") 52 | endif() 53 | 54 | source_group("${GROUP}" FILES "${FILE}") 55 | endforeach() 56 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Comparators/EndpointRequestAuthorizationClaimsComparator.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "RESTAPICore/Endpoint/EndpointRequestAuthorizationClaims.h" 3 | 4 | #include "TestUtilitiesInterface/EntityComparator.h" 5 | #include "TestUtilitiesInterface/EntityComparatorMacros.h" 6 | 7 | 8 | using namespace testing; 9 | using namespace systelab::rest_api_core; 10 | 11 | namespace systelab { namespace test_utility { 12 | 13 | template<> 14 | AssertionResult EntityComparator::operator() (const EndpointRequestAuthorizationClaims& expected, const EndpointRequestAuthorizationClaims& actual) const 15 | { 16 | COMPARATOR_ASSERT_EQUAL(expected, actual, getClaimCount()); 17 | auto claimNames = expected.getClaimNames(); 18 | for (auto name : claimNames) 19 | { 20 | if (!actual.hasClaim(name)) 21 | { 22 | return AssertionFailure() << "Expected claim '" << name << "' not found."; 23 | } 24 | 25 | std::string expectedValue = expected.getClaim(name); 26 | std::string actualValue = actual.getClaim(name); 27 | if (expectedValue != actualValue) 28 | { 29 | return AssertionFailure() << "Different value for claim '" << name << "': " 30 | << "expected='" << expectedValue << "', " 31 | << "actual='" << actualValue << "'"; 32 | } 33 | } 34 | 35 | return AssertionSuccess(); 36 | } 37 | 38 | }} 39 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Comparators/EndpointRequestDataComparator.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "RESTAPICore/Endpoint/EndpointRequestData.h" 3 | 4 | #include "TestUtilitiesInterface/EntityComparator.h" 5 | #include "TestUtilitiesInterface/EntityComparatorMacros.h" 6 | 7 | 8 | using namespace testing; 9 | using namespace systelab::rest_api_core; 10 | 11 | namespace systelab { namespace test_utility { 12 | 13 | template<> 14 | AssertionResult EntityComparator::operator() (const EndpointRequestData& expected, const EndpointRequestData& actual) const 15 | { 16 | auto& expectedParameters = expected.getParameters(); 17 | auto& actualParameters = actual.getParameters(); 18 | AssertionResult parametersResult = EntityComparator()(expectedParameters, actualParameters); 19 | if (!parametersResult) 20 | { 21 | return AssertionFailure() << "Different parameters data: " << parametersResult.message(); 22 | } 23 | 24 | COMPARATOR_ASSERT_EQUAL(expected, actual, getContent()); 25 | 26 | auto& expectedHeaders = expected.getHeaders(); 27 | auto& actualHeaders = actual.getHeaders(); 28 | AssertionResult headersResult = EntityComparator()(expectedHeaders, actualHeaders); 29 | if (!headersResult) 30 | { 31 | return AssertionFailure() << "Different headers data: " << headersResult.message(); 32 | } 33 | 34 | auto& expectedQueryStrings = expected.getQueryStrings(); 35 | auto& actualQueryStrings = actual.getQueryStrings(); 36 | AssertionResult queryStringsResult = EntityComparator()(expectedQueryStrings, actualQueryStrings); 37 | if (!queryStringsResult) 38 | { 39 | return AssertionFailure() << "Different query strings data: " << queryStringsResult.message(); 40 | } 41 | 42 | auto& expectedAuthorizationClaims = expected.getAuthorizationClaims(); 43 | auto& actualAuthorizationClaims = actual.getAuthorizationClaims(); 44 | AssertionResult authorizationClaimsResult = EntityComparator()(expectedAuthorizationClaims, actualAuthorizationClaims); 45 | if (!authorizationClaimsResult) 46 | { 47 | return AssertionFailure() << "Different authorization claims data: " << authorizationClaimsResult.message(); 48 | } 49 | 50 | return AssertionSuccess(); 51 | } 52 | 53 | }} 54 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Comparators/EndpointRequestParamsComparator.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "RESTAPICore/Endpoint/EndpointRequestParams.h" 3 | 4 | #include "TestUtilitiesInterface/EntityComparator.h" 5 | #include "TestUtilitiesInterface/EntityComparatorMacros.h" 6 | 7 | 8 | using namespace testing; 9 | using namespace systelab::rest_api_core; 10 | 11 | namespace systelab { namespace test_utility { 12 | 13 | template<> 14 | AssertionResult EntityComparator::operator() (const EndpointRequestParams& expected, const EndpointRequestParams& toTest) const 15 | { 16 | // String parameters 17 | COMPARATOR_ASSERT_EQUAL(expected, toTest, getStringParameterCount()); 18 | auto stringParameterNames = expected.getStringParameterNames(); 19 | for (auto parameterName : stringParameterNames) 20 | { 21 | if (!toTest.hasStringParameter(parameterName)) 22 | { 23 | return AssertionFailure() << "Expected string parameter '" << parameterName << "' not found."; 24 | } 25 | 26 | std::string expectedValue = expected.getStringParameter(parameterName); 27 | std::string toTestValue = toTest.getStringParameter(parameterName); 28 | if (expectedValue != toTestValue) 29 | { 30 | return AssertionFailure() << "Different value for string parameter '" << parameterName << "': " 31 | << "expected='" << expectedValue << "', actual='" << toTestValue << "'"; 32 | } 33 | } 34 | 35 | 36 | // Numeric parameters 37 | COMPARATOR_ASSERT_EQUAL(expected, toTest, getNumericParameterCount()); 38 | auto numericParameterNames = expected.getNumericParameterNames(); 39 | for (auto parameterName : numericParameterNames) 40 | { 41 | if (!toTest.hasNumericParameter(parameterName)) 42 | { 43 | return AssertionFailure() << "Expected numeric parameter '" << parameterName << "' not found."; 44 | } 45 | 46 | unsigned int expectedValue = expected.getNumericParameter(parameterName); 47 | unsigned int toTestValue = toTest.getNumericParameter(parameterName); 48 | if (expectedValue != toTestValue) 49 | { 50 | return AssertionFailure() << "Different value for numeric parameter '" << parameterName << "': " 51 | << "expected=" << expectedValue << ", actual=" << toTestValue; 52 | } 53 | } 54 | 55 | return AssertionSuccess(); 56 | } 57 | 58 | }} 59 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Mocks/Endpoint/MockEndpoint.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "MockEndpoint.h" 3 | 4 | 5 | namespace systelab { namespace rest_api_core { namespace test_utility { 6 | 7 | MockEndpoint::MockEndpoint() 8 | { 9 | } 10 | 11 | MockEndpoint::~MockEndpoint() = default; 12 | 13 | }}} 14 | 15 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Mocks/Endpoint/MockEndpoint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RESTAPICore/Endpoint/IEndpoint.h" 4 | #include "RESTAPICore/Endpoint/EndpointRequestData.h" 5 | 6 | #include "WebServerAdapterInterface/Model/Reply.h" 7 | 8 | 9 | namespace systelab { namespace rest_api_core { namespace test_utility { 10 | 11 | class MockEndpoint : public IEndpoint 12 | { 13 | public: 14 | MockEndpoint(); 15 | virtual ~MockEndpoint(); 16 | 17 | MOCK_METHOD1(executeProxy, web_server::Reply* (const EndpointRequestData&)); 18 | std::unique_ptr execute(const EndpointRequestData& endpointRequestData) 19 | { 20 | return std::unique_ptr(executeProxy(endpointRequestData)); 21 | } 22 | }; 23 | 24 | }}} 25 | 26 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Mocks/RouteAccess/MockEpochTimeService.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "MockEpochTimeService.h" 3 | 4 | 5 | namespace systelab { namespace rest_api_core { namespace test_utility { 6 | 7 | MockEpochTimeService::MockEpochTimeService() 8 | { 9 | } 10 | 11 | MockEpochTimeService::~MockEpochTimeService() = default; 12 | 13 | }}} 14 | 15 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Mocks/RouteAccess/MockEpochTimeService.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RESTAPICore/RouteAccess/IEpochTimeService.h" 4 | 5 | 6 | namespace systelab { namespace rest_api_core { namespace test_utility { 7 | 8 | class MockEpochTimeService : public IEpochTimeService 9 | { 10 | public: 11 | MockEpochTimeService(); 12 | virtual ~MockEpochTimeService(); 13 | 14 | MOCK_CONST_METHOD0(getCurrentEpochTime, long long ()); 15 | }; 16 | 17 | }}} 18 | 19 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Mocks/RouteAccess/MockRouteAccessValidator.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "MockRouteAccessValidator.h" 3 | 4 | 5 | namespace systelab { namespace rest_api_core { namespace test_utility { 6 | 7 | MockRouteAccessValidator::MockRouteAccessValidator() 8 | { 9 | } 10 | 11 | MockRouteAccessValidator::~MockRouteAccessValidator() = default; 12 | 13 | }}} 14 | 15 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Mocks/RouteAccess/MockRouteAccessValidator.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RESTAPICore/RouteAccess/IRouteAccessValidator.h" 4 | 5 | #include "RESTAPICore/Endpoint/EndpointRequestData.h" 6 | 7 | 8 | namespace systelab { namespace rest_api_core { namespace test_utility { 9 | 10 | class MockRouteAccessValidator : public IRouteAccessValidator 11 | { 12 | public: 13 | MockRouteAccessValidator(); 14 | virtual ~MockRouteAccessValidator(); 15 | 16 | MOCK_CONST_METHOD1(hasAccess, bool(EndpointRequestData&)); 17 | }; 18 | 19 | }}} 20 | 21 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Mocks/RouteAccess/MockUserRoleService.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "MockUserRoleService.h" 3 | 4 | 5 | namespace systelab { namespace rest_api_core { namespace test_utility { 6 | 7 | MockUserRoleService::MockUserRoleService() 8 | { 9 | } 10 | 11 | MockUserRoleService::~MockUserRoleService() = default; 12 | 13 | }}} 14 | 15 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Mocks/RouteAccess/MockUserRoleService.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RESTAPICore/RouteAccess/IUserRoleService.h" 4 | 5 | 6 | namespace systelab { namespace rest_api_core { namespace test_utility { 7 | 8 | class MockUserRoleService : public IUserRoleService 9 | { 10 | public: 11 | MockUserRoleService(); 12 | virtual ~MockUserRoleService(); 13 | 14 | MOCK_CONST_METHOD1(getUserRoles, std::vector(const std::string&)); 15 | }; 16 | 17 | }}} 18 | 19 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Mocks/Router/MockRoute.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "MockRoute.h" 3 | 4 | 5 | namespace systelab { namespace rest_api_core { namespace test_utility { 6 | 7 | MockRoute::MockRoute() 8 | { 9 | } 10 | 11 | MockRoute::~MockRoute() = default; 12 | 13 | }}} 14 | 15 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Mocks/Router/MockRoute.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RESTAPICore/Router/IRoute.h" 4 | 5 | #include "WebServerAdapterInterface/Model/Reply.h" 6 | #include "WebServerAdapterInterface/Model/Request.h" 7 | 8 | 9 | namespace systelab { namespace rest_api_core { namespace test_utility { 10 | 11 | class MockRoute : public IRoute 12 | { 13 | public: 14 | MockRoute(); 15 | virtual ~MockRoute(); 16 | 17 | MOCK_CONST_METHOD1(executeProxy, web_server::Reply* (const web_server::Request&)); 18 | std::unique_ptr execute(const web_server::Request& request) const 19 | { 20 | return std::unique_ptr(executeProxy(request)); 21 | } 22 | }; 23 | 24 | }}} 25 | 26 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Mocks/Router/MockRoutesFactory.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "MockRoutesFactory.h" 3 | 4 | 5 | namespace systelab { namespace rest_api_core { namespace test_utility { 6 | 7 | MockRoutesFactory::MockRoutesFactory() 8 | { 9 | } 10 | 11 | MockRoutesFactory::~MockRoutesFactory() = default; 12 | 13 | }}} 14 | 15 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Mocks/Router/MockRoutesFactory.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "RESTAPICore/Router/IRoutesFactory.h" 4 | 5 | #include "RESTAPICore/Router/IRoute.h" 6 | 7 | #include "WebServerAdapterInterface/Model/Reply.h" 8 | #include "WebServerAdapterInterface/Model/Request.h" 9 | 10 | 11 | namespace systelab { namespace rest_api_core { namespace test_utility { 12 | 13 | class MockRoutesFactory : public IRoutesFactory 14 | { 15 | public: 16 | MockRoutesFactory(); 17 | virtual ~MockRoutesFactory(); 18 | 19 | MOCK_CONST_METHOD4(buildRouteProxy, IRoute* (const std::string&, const std::string&, const std::vector&, EndpointFactoryMethod)); 20 | std::unique_ptr buildRoute(const std::string& method, 21 | const std::string& uri, 22 | const std::vector& accessValidatorFactoryMethods, 23 | EndpointFactoryMethod endpointFactoryMethod) const 24 | { 25 | return std::unique_ptr(buildRouteProxy(method, uri, accessValidatorFactoryMethods, endpointFactoryMethod)); 26 | } 27 | }; 28 | 29 | }}} 30 | 31 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Stubs/StubEpochTimeService.cpp: -------------------------------------------------------------------------------- 1 | #include "stdafx.h" 2 | #include "StubEpochTimeService.h" 3 | 4 | #include 5 | 6 | 7 | using namespace testing; 8 | 9 | namespace systelab { namespace rest_api_core { namespace test_utility { 10 | 11 | StubEpochTimeService::StubEpochTimeService() 12 | :m_currentTime(std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())) 13 | { 14 | ON_CALL(*this, getCurrentEpochTime()).WillByDefault(Invoke( 15 | [this]() -> std::time_t 16 | { 17 | return m_currentTime; 18 | } 19 | )); 20 | } 21 | 22 | StubEpochTimeService::~StubEpochTimeService() = default; 23 | 24 | void StubEpochTimeService::addSeconds(unsigned int nSeconds) 25 | { 26 | auto currentTimePoint = std::chrono::system_clock::from_time_t(m_currentTime); 27 | m_currentTime = std::chrono::system_clock::to_time_t(currentTimePoint + std::chrono::seconds(nSeconds)); 28 | } 29 | 30 | }}} 31 | 32 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/Stubs/StubEpochTimeService.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "../Mocks/RouteAccess/MockEpochTimeService.h" 4 | 5 | 6 | namespace systelab { namespace rest_api_core { namespace test_utility { 7 | 8 | class StubEpochTimeService : public MockEpochTimeService 9 | { 10 | public: 11 | StubEpochTimeService(); 12 | virtual ~StubEpochTimeService(); 13 | 14 | void addSeconds(unsigned int nSeconds); 15 | 16 | private: 17 | std::time_t m_currentTime; 18 | }; 19 | 20 | }}} 21 | 22 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/conanfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | from conans import ConanFile, tools, CMake 3 | 4 | class RESTAPICoreTestUtilitiesConan(ConanFile): 5 | name = "RESTAPICoreTestUtilities" 6 | description = "Test utilities for C++ REST API framework" 7 | url = "https://github.com/systelab/cpp-rest-api-core" 8 | homepage = "https://github.com/systelab/cpp-rest-api-core" 9 | author = "CSW " 10 | topics = ("conan", "rest", "api", "framework", "test", "utilities") 11 | license = "MIT" 12 | generators = "cmake_find_package" 13 | settings = "os", "compiler", "build_type", "arch" 14 | exports_sources = "*", "!build*", "!*.yml", "!*.md", "!*.in", "!ci", "!.gitattributes", "!.gitignore", "!LICENSE" 15 | 16 | def requirements(self): 17 | self.requires("gtest/1.14.0#4372c5aed2b4018ed9f9da3e218d18b3") 18 | 19 | if ("%s" % self.version) == "None": 20 | self.requires(f"RESTAPICore/{os.environ['VERSION']}@systelab/{os.environ['CHANNEL']}") 21 | else: 22 | self.requires(f"RESTAPICore/{self.version}@systelab/{self.channel}") 23 | 24 | def build(self): 25 | cmake = CMake(self) 26 | cmake.configure() 27 | cmake.build() 28 | 29 | def imports(self): 30 | self.copy("*.dll", dst="bin", src="bin") 31 | self.copy("*.dylib*", dst="bin", src="lib") 32 | self.copy("*.so*", dst="bin", src="lib") 33 | 34 | def package(self): 35 | self.copy("*.h", dst="include/RESTAPICoreTestUtilities", keep_path=True) 36 | self.copy("*RESTAPICoreTestUtilities.lib", dst="lib", keep_path=False) 37 | self.copy("*RESTAPICoreTestUtilities.pdb", dst="lib", keep_path=False) 38 | self.copy("*RESTAPICoreTestUtilities.a", dst="lib", keep_path=False) 39 | 40 | def package_info(self): 41 | self.cpp_info.libs = tools.collect_libs(self) 42 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/stdafx.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #define _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING 1 4 | 5 | // STL 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | 13 | // GTEST 14 | #include 15 | #include 16 | 17 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/test_package/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2) 2 | 3 | project(RESTAPICoreTestUtilitiesPackageTestProject) 4 | 5 | # Configure environment 6 | set(CMAKE_CXX_STANDARD 14) 7 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 8 | set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../cmake ${CMAKE_BINARY_DIR}) 9 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 10 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 11 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 12 | 13 | # Find tested package 14 | find_package(RESTAPICoreTestUtilities) 15 | 16 | # Configure test package project 17 | set(REST_API_CORE_TEST_UTILITIES_PACKAGE_TEST_PROJECT RESTAPICoreTestUtilitiesPackageTest) 18 | add_executable(${REST_API_CORE_TEST_UTILITIES_PACKAGE_TEST_PROJECT} RESTAPICoreTestUtilitiesExample.cpp) 19 | target_link_libraries(${REST_API_CORE_TEST_UTILITIES_PACKAGE_TEST_PROJECT} RESTAPICoreTestUtilities::RESTAPICoreTestUtilities) 20 | 21 | # Register test 22 | enable_testing() 23 | add_test(NAME RESTAPICoreTestUtilitiesPackageTest COMMAND ${REST_API_CORE_TEST_UTILITIES_PACKAGE_TEST_PROJECT}) 24 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/test_package/RESTAPICoreTestUtilitiesExample.cpp: -------------------------------------------------------------------------------- 1 | #define _SILENCE_TR1_NAMESPACE_DEPRECATION_WARNING 1 2 | 3 | // STL 4 | #include 5 | #include 6 | 7 | // GTEST 8 | #include 9 | #include 10 | 11 | #include "RESTAPICoreTestUtilities/Mocks/Endpoint/MockEndpoint.h" 12 | 13 | 14 | int main(int argc, char *argv[]) 15 | { 16 | systelab::rest_api_core::test_utility::MockEndpoint endpoint; 17 | std::cout << "REST API Core test utilities work as expected" << std::endl; 18 | 19 | return 0; 20 | } 21 | -------------------------------------------------------------------------------- /test/RESTAPICoreTestUtilities/test_package/conanfile.py: -------------------------------------------------------------------------------- 1 | import os 2 | from conans import ConanFile, CMake, tools 3 | 4 | 5 | class RESTAPICoreTestUtilitiesTestConan(ConanFile): 6 | settings = "os", "compiler", "build_type", "arch" 7 | generators = "cmake_find_package" 8 | 9 | def build(self): 10 | cmake = CMake(self) 11 | cmake.configure() 12 | cmake.build() 13 | 14 | def imports(self): 15 | self.copy("*.dll", dst="bin", src="bin") 16 | self.copy("*.dylib*", dst="bin", src="lib") 17 | self.copy('*.so*', dst='bin', src='lib') 18 | 19 | def test(self): 20 | cmake = CMake(self) 21 | cmake.test() 22 | -------------------------------------------------------------------------------- /test_package/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.2) 2 | 3 | project(RESTAPICorePackageTestProject) 4 | 5 | # Configure environment 6 | set(CMAKE_CXX_STANDARD 14) 7 | set(CMAKE_CXX_STANDARD_REQUIRED ON) 8 | set(CMAKE_MODULE_PATH ${CMAKE_CURRENT_SOURCE_DIR}/../cmake ${CMAKE_BINARY_DIR}) 9 | set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 10 | set(CMAKE_LIBRARY_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/lib) 11 | set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) 12 | 13 | # Find external dependencies 14 | find_package(RESTAPICore) 15 | 16 | # Configure test package project 17 | set(REST_API_CORE_PACKAGE_TEST_PROJECT RESTAPICorePackageTest) 18 | add_executable(${REST_API_CORE_PACKAGE_TEST_PROJECT} RESTAPICoreExample.cpp) 19 | target_link_libraries(${REST_API_CORE_PACKAGE_TEST_PROJECT} RESTAPICore::RESTAPICore) 20 | 21 | # Register tests 22 | enable_testing() 23 | add_test(NAME RESTAPICorePackageTest COMMAND ${REST_API_CORE_PACKAGE_TEST_PROJECT}) 24 | -------------------------------------------------------------------------------- /test_package/RESTAPICoreExample.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "RESTAPICore/Endpoint/IEndpoint.h" 7 | #include "RESTAPICore/Router/IRoute.h" 8 | #include "RESTAPICore/Router/Router.h" 9 | #include "RESTAPICore/Router/RoutesFactory.h" 10 | 11 | #include "WebServerAdapterInterface/Model/Reply.h" 12 | #include "WebServerAdapterInterface/Model/Request.h" 13 | 14 | 15 | class MyEndpoint : public systelab::rest_api_core::IEndpoint 16 | { 17 | public: 18 | MyEndpoint() = default; 19 | 20 | std::unique_ptr execute(const systelab::rest_api_core::EndpointRequestData&) override 21 | { 22 | auto reply = std::make_unique(); 23 | reply->setStatus(systelab::web_server::Reply::OK); 24 | reply->setContent("{ \"message\": \"This is the reply for MyEndpoint\"}"); 25 | return reply; 26 | } 27 | }; 28 | 29 | int main(int argc, char *argv[]) 30 | { 31 | std::string jwtKey = "HereGoesYourJWTSecretKey"; 32 | auto routesFactory = std::make_unique(jwtKey); 33 | 34 | auto router = std::make_unique(); 35 | router->addRoute(routesFactory->buildRoute("GET", "/rest/api/test", {}, []() { return std::make_unique(); }) ); 36 | 37 | systelab::web_server::Request request; 38 | request.setMethod("GET"); 39 | request.setURI("/rest/api/test"); 40 | 41 | auto reply = router->process(request); 42 | std::cout << "Endpoint reply -> Status " << reply->getStatus() << ", Content:" << reply->getContent() << std::endl; 43 | 44 | return 0; 45 | } 46 | -------------------------------------------------------------------------------- /test_package/conanfile.py: -------------------------------------------------------------------------------- 1 | from conans import ConanFile, CMake 2 | 3 | 4 | class RESTAPICoreTestConan(ConanFile): 5 | settings = "os", "compiler", "build_type", "arch" 6 | generators = "cmake_find_package" 7 | 8 | def build(self): 9 | cmake = CMake(self) 10 | cmake.configure() 11 | cmake.build() 12 | 13 | def imports(self): 14 | self.copy("*.dll", dst=("bin/%s" % self.settings.build_type), src="bin") 15 | self.copy("*.dylib*", dst=("bin/%s" % self.settings.build_type), src="lib") 16 | self.copy('*.so*', dst=("bin/%s" % self.settings.build_type), src='lib') 17 | 18 | def test(self): 19 | cmake = CMake(self) 20 | cmake.test() 21 | -------------------------------------------------------------------------------- /vs2022.conanprofile: -------------------------------------------------------------------------------- 1 | [settings] 2 | os=Windows 3 | os_build=Windows 4 | compiler=Visual Studio 5 | compiler.version=17 6 | compiler.toolset=v143 7 | 8 | [options] 9 | openssl:shared=True --------------------------------------------------------------------------------