├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── docs └── diagrams │ ├── generate.sh │ ├── out │ ├── create_todo_sequence.png │ ├── delete_todo_sequence.png │ ├── deployment_diagram.png │ ├── get_todo_sequence.png │ ├── list_todo_sequence.png │ ├── login_user_sequence.png │ ├── register_user_sequence.png │ ├── sequence_participants.png │ └── update_todo_sequence.png │ └── src │ ├── create_todo_sequence.txt │ ├── delete_todo_sequence.txt │ ├── deployment_diagram.txt │ ├── get_todo_sequence.txt │ ├── list_todo_sequence.txt │ ├── login_user_sequence.txt │ ├── register_user_sequence.txt │ ├── sequence_participants.txt │ └── update_todo_sequence.txt ├── gateway ├── CMakeLists.txt ├── Dockerfile ├── inc │ └── gateway │ │ ├── applicationstarter.hpp │ │ ├── config.hpp │ │ ├── exceptionbase.hpp │ │ ├── gatewayapp.hpp │ │ ├── gatewayauthmetadataprocessor.hpp │ │ ├── todoclient.hpp │ │ ├── todoserviceimpl.hpp │ │ ├── userclient.hpp │ │ └── userserviceimpl.hpp ├── run.sh └── src │ ├── config.cpp │ ├── gatewayapp.cpp │ ├── gatewayauthmetadataprocessor.cpp │ └── main.cpp ├── protos ├── .gitignore ├── Dockerfile ├── buildAll.sh ├── run.sh ├── todo │ └── todo.proto └── user │ └── user.proto ├── test ├── Dockerfile ├── certs │ ├── jwtRS256.key │ ├── jwtRS256.key.pub │ ├── server.crt │ └── server.key ├── docker-compose.yml ├── e2e_base.py ├── gateway-variables.env ├── requirements.txt ├── run.sh ├── settings.py ├── test_todo.py ├── test_user.py ├── todo-variables.env └── user-variables.env ├── todo ├── Dockerfile ├── requirements.txt ├── tests │ └── integration │ │ ├── Dockerfile │ │ ├── __init__.py │ │ ├── certs │ │ ├── jwtRS256.key │ │ └── jwtRS256.key.pub │ │ ├── docker-compose.yml │ │ ├── requirements.txt │ │ ├── run.sh │ │ ├── test.py │ │ └── todo-variables.env └── todo │ ├── app.py │ ├── main.py │ ├── models.py │ ├── session.py │ ├── settings.py │ └── todo_servicer.py └── user ├── Dockerfile ├── requirements.txt ├── tests └── integration │ ├── Dockerfile │ ├── __init__.py │ ├── certs │ ├── jwtRS256.key │ └── jwtRS256.key.pub │ ├── docker-compose.yml │ ├── requirements.txt │ ├── run.sh │ ├── test.py │ └── user-variables.env └── user ├── app.py ├── main.py ├── models.py ├── session.py ├── settings.py └── user_servicer.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | 3 | # Visual Studio Code 4 | .vscode 5 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "gateway/third_party/jwt-cpp"] 2 | path = gateway/third_party/jwt-cpp 3 | url = https://github.com/Thalhammer/jwt-cpp.git 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 rsitko92 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 | # Todo list microservices architecture application 2 | 3 | ## Table of contents 4 | 5 | - [Todo list microservices architecture application](#Todo-list-microservices-architecture-application) 6 | - [Table of contents](#Table-of-contents) 7 | - [Description](#Description) 8 | - [Project structure](#Project-structure) 9 | - [Cloning repository](#Cloning-repository) 10 | - [Prerequisites](#Prerequisites) 11 | - [Generating interfaces](#Generating-interfaces) 12 | - [Generating diagrams](#Generating-diagrams) 13 | - [Running tests](#Running-tests) 14 | - [Running integration tests](#Running-integration-tests) 15 | - [Running end2end tests](#Running-end2end-tests) 16 | - [Technology stack](#Technology-stack) 17 | - [User service technology stack](#User-service-technology-stack) 18 | - [Todo service technology stack](#Todo-service-technology-stack) 19 | - [Gateway API service technology stack](#Gateway-API-service-technology-stack) 20 | - [End2end tests technology stack](#End2end-tests-technology-stack) 21 | - [Diagrams](#Diagrams) 22 | - [Deployment diagram](#Deployment-diagram) 23 | - [Login user sequence diagram](#Login-user-sequence-diagram) 24 | - [Register user sequence diagram](#Register-user-sequence-diagram) 25 | - [Create todo sequence diagram](#Create-todo-sequence-diagram) 26 | - [Get todo sequence diagram](#Get-todo-sequence-diagram) 27 | - [Delete todo sequence diagram](#Delete-todo-sequence-diagram) 28 | - [Update todo sequence diagram](#Update-todo-sequence-diagram) 29 | - [List todo sequence diagram](#List-todo-sequence-diagram) 30 | 31 | ## Description 32 | 33 | This project demonstrates microservices architecture based application. 34 | 35 | Main application features: 36 | 37 | - gRPC APIs 38 | - SSL encryption between client and backend 39 | - users can login and register accounts 40 | - JWT token authorization 41 | - gateway API 42 | - logged users can create, get, list, update and delete todo list entries 43 | - services use MySQL databases 44 | - services are running in Docker containers 45 | 46 | Application consists of three services: 47 | 48 | - Gateway API service - SSL encryption, JWT token verification, authorization and orchestration 49 | - User service - registering users, managing user accounts, authentication and JWT token generation 50 | - Todo service - creating, updating, deleting and querying todos 51 | 52 | ## Project structure 53 | 54 | ``` 55 | . 56 | ├── gateway - directory with gateway service 57 | │   ├── inc 58 | │   │   └── gateway - directory with header files 59 | │   ├── src - directory with source code files 60 | │   └── third_party - external dependencies (libraries) 61 | │   └── jwt-cpp 62 | ├── protos - directory with gRPC interfaces 63 | │   ├── todo - directory with gRPC interface for todo service 64 | │   └── user - directory with gRPC interface for user service 65 | ├── test - directory with end2end tests 66 | │   └── certs - directory with test certificates 67 | ├── todo - directory with source code and tests for todo service 68 | │   ├── tests 69 | │   │   └── integration - directory with integration tests for todo service 70 | │   │   └── certs - directory with test certificates 71 | │   └── todo - package with todo service code 72 | └── user - directory with source code and tests for user service 73 | ├── tests 74 | │   └── integration - directory with integration tests for user service 75 | │   └── certs - directory with test certificates 76 | └── user - package with user service code 77 | ``` 78 | 79 | ## Cloning repository 80 | 81 | ```bash 82 | git clone --recursive https://github.com/rsitko92/todo-app-microservices.git 83 | ``` 84 | 85 | ## Prerequisites 86 | 87 | - [Docker](https://www.docker.com) 88 | - [Docker Compose](https://www.docker.com) 89 | - [PlantUML](http://plantuml.com) (if you want to generate diagrams) 90 | 91 | ## Generating interfaces 92 | 93 | Being in root directory of project run: 94 | 95 | ```bash 96 | sudo protos/run.sh 97 | ``` 98 | 99 | Generated interfaces definitions will be placed in `protos/.gen` directory. It is sufficient to generate they only one time. 100 | 101 | ## Generating diagrams 102 | 103 | Being in root directory of project run: 104 | 105 | ```bash 106 | docs/diagrams/generate.sh 107 | ``` 108 | 109 | Generated images with diagrams will be placed in `docs/diagrams/out` directory. 110 | 111 | ## Running tests 112 | 113 | Before running any kind of tests (integration or end2end) generate the gRPC client and server interfaces from .proto service definitions files. See section [Generating interfaces](#generating-interfaces). 114 | 115 | ### Running integration tests 116 | 117 | - User service: 118 | 119 | Being in root directory of project run: 120 | 121 | ```bash 122 | sudo user/tests/integration/run.sh 123 | ``` 124 | 125 | - Todo service: 126 | 127 | Being in root directory of project run: 128 | 129 | ```bash 130 | sudo todo/tests/integration/run.sh 131 | ``` 132 | 133 | ### Running end2end tests 134 | 135 | Being in root directory of project run: 136 | 137 | ```bash 138 | sudo test/run.sh 139 | ``` 140 | 141 | ## Technology stack 142 | 143 | ### User service technology stack 144 | 145 | - Python3 146 | - Docker 147 | - gRPC Python 148 | - SQLAlchemy 149 | - PyJWT 150 | - MariaDB (MySQL relational database management system) 151 | 152 | ### Todo service technology stack 153 | 154 | - Python3 155 | - Docker 156 | - gRPC Python 157 | - SQLAlchemy 158 | - PyJWT 159 | - MariaDB (MySQL relational database management system) 160 | 161 | ### Gateway API service technology stack 162 | 163 | - C++ 164 | - Docker 165 | - gRPC C++ with SSL 166 | - jwt-cpp 167 | - CMake 168 | 169 | ### End2end tests technology stack 170 | 171 | - Python3 172 | - Docker 173 | - gRPC Python 174 | - PyJWT 175 | 176 | ## Diagrams 177 | 178 | ### Deployment diagram 179 | 180 | ![Deployment diagram](/docs/diagrams/out/deployment_diagram.png?raw=true) 181 | 182 | ### Login user sequence diagram 183 | 184 | ![Login user sequence diagram](/docs/diagrams/out/login_user_sequence.png?raw=true) 185 | 186 | ### Register user sequence diagram 187 | 188 | ![Register user sequence diagram](/docs/diagrams/out/register_user_sequence.png?raw=true) 189 | 190 | ### Create todo sequence diagram 191 | 192 | ![Create todo sequence diagram](/docs/diagrams/out/create_todo_sequence.png?raw=true) 193 | 194 | ### Get todo sequence diagram 195 | 196 | ![Get todo sequence diagram](/docs/diagrams/out/get_todo_sequence.png?raw=true) 197 | 198 | ### Delete todo sequence diagram 199 | 200 | ![Delete todo sequence diagram](/docs/diagrams/out/delete_todo_sequence.png?raw=true) 201 | 202 | ### Update todo sequence diagram 203 | 204 | ![Update todo sequence diagram](/docs/diagrams/out/update_todo_sequence.png?raw=true) 205 | 206 | ### List todo sequence diagram 207 | 208 | ![List todo sequence diagram](/docs/diagrams/out/list_todo_sequence.png?raw=true) 209 | -------------------------------------------------------------------------------- /docs/diagrams/generate.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | SCRIPT_DIR=$(dirname $(readlink -f $0)) 5 | cd $SCRIPT_DIR 6 | 7 | plantuml -output ../out/ src/* 8 | 9 | -------------------------------------------------------------------------------- /docs/diagrams/out/create_todo_sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-sitko/todo-list-app-microservices/3c48dea159be3be1d92c5a0509bb44765a2b7456/docs/diagrams/out/create_todo_sequence.png -------------------------------------------------------------------------------- /docs/diagrams/out/delete_todo_sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-sitko/todo-list-app-microservices/3c48dea159be3be1d92c5a0509bb44765a2b7456/docs/diagrams/out/delete_todo_sequence.png -------------------------------------------------------------------------------- /docs/diagrams/out/deployment_diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-sitko/todo-list-app-microservices/3c48dea159be3be1d92c5a0509bb44765a2b7456/docs/diagrams/out/deployment_diagram.png -------------------------------------------------------------------------------- /docs/diagrams/out/get_todo_sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-sitko/todo-list-app-microservices/3c48dea159be3be1d92c5a0509bb44765a2b7456/docs/diagrams/out/get_todo_sequence.png -------------------------------------------------------------------------------- /docs/diagrams/out/list_todo_sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-sitko/todo-list-app-microservices/3c48dea159be3be1d92c5a0509bb44765a2b7456/docs/diagrams/out/list_todo_sequence.png -------------------------------------------------------------------------------- /docs/diagrams/out/login_user_sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-sitko/todo-list-app-microservices/3c48dea159be3be1d92c5a0509bb44765a2b7456/docs/diagrams/out/login_user_sequence.png -------------------------------------------------------------------------------- /docs/diagrams/out/register_user_sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-sitko/todo-list-app-microservices/3c48dea159be3be1d92c5a0509bb44765a2b7456/docs/diagrams/out/register_user_sequence.png -------------------------------------------------------------------------------- /docs/diagrams/out/sequence_participants.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-sitko/todo-list-app-microservices/3c48dea159be3be1d92c5a0509bb44765a2b7456/docs/diagrams/out/sequence_participants.png -------------------------------------------------------------------------------- /docs/diagrams/out/update_todo_sequence.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-sitko/todo-list-app-microservices/3c48dea159be3be1d92c5a0509bb44765a2b7456/docs/diagrams/out/update_todo_sequence.png -------------------------------------------------------------------------------- /docs/diagrams/src/create_todo_sequence.txt: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title Create todo 4 | 5 | !includesub sequence_participants.txt!todo 6 | 7 | user -> gateway_api_service: CreateToDoReq(item) + JWT user token 8 | gateway_api_service -> gateway_api_service: check and validate JWT token 9 | 10 | alt JWT token exists and JWT token validation success 11 | gateway_api_service -> todo_service: CreateToDoReq(item) + JWT user token 12 | todo_service -> todo_service_db: insert todo 13 | todo_service <-- todo_service_db 14 | gateway_api_service <-- todo_service: CreateToDoRsp() 15 | user <-- gateway_api_service: CreateToDoRsp() 16 | else JWT token does not exists or JWT token validation error 17 | user <-- gateway_api_service: grpc.StatusCode.UNAUTHENTICATED 18 | end 19 | 20 | @enduml 21 | -------------------------------------------------------------------------------- /docs/diagrams/src/delete_todo_sequence.txt: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title Delete todo 4 | 5 | !includesub sequence_participants.txt!todo 6 | 7 | user -> gateway_api_service: DeleteToDoReq(id) + JWT user token 8 | gateway_api_service -> gateway_api_service: check and validate JWT token 9 | 10 | alt JWT token exists and JWT token validation success 11 | gateway_api_service -> todo_service: DeleteToDoReq(id) + JWT user token 12 | todo_service -> todo_service_db: delete todo 13 | todo_service <-- todo_service_db 14 | alt todo deleted 15 | gateway_api_service <-- todo_service: DeleteToDoRsp() 16 | user <-- gateway_api_service: DeleteToDoRsp() 17 | else todo does not exists 18 | gateway_api_service <-- todo_service: grpc.StatusCode.NOT_FOUND 19 | user <-- gateway_api_service: grpc.StatusCode.NOT_FOUND 20 | end 21 | else JWT token does not exists or JWT token validation error 22 | user <-- gateway_api_service: grpc.StatusCode.UNAUTHENTICATED 23 | end 24 | 25 | @enduml 26 | -------------------------------------------------------------------------------- /docs/diagrams/src/deployment_diagram.txt: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title Deployment diagram 4 | 5 | rectangle backend { 6 | component user_service [ 7 | **User service** 8 | ---- 9 | registering users 10 | managing user accounts 11 | authentication 12 | JWT token generation 13 | ] 14 | database "User service db" as user_service_db 15 | 16 | component todo_service [ 17 | **Todo service** 18 | ---- 19 | creating todos 20 | getting todos 21 | listing todos 22 | updating todos 23 | deleting todos 24 | ] 25 | database "Todo service db" as todo_service_db 26 | 27 | component gateway_api_service [ 28 | **Gateway API service** 29 | ---- 30 | SSL encryption 31 | JWT token verification 32 | authorization 33 | orchestration 34 | ] 35 | } 36 | 37 | actor user 38 | user -- gateway_api_service : gRPC with SSL 39 | 40 | user_service -- user_service_db : SQL 41 | todo_service -- todo_service_db : SQL 42 | gateway_api_service -- user_service : user service interface 43 | gateway_api_service -- todo_service : todo service interface 44 | 45 | @enduml 46 | -------------------------------------------------------------------------------- /docs/diagrams/src/get_todo_sequence.txt: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title Get todo 4 | 5 | !includesub sequence_participants.txt!todo 6 | 7 | user -> gateway_api_service: GetToDoReq(id) + JWT user token 8 | gateway_api_service -> gateway_api_service: check and validate JWT token 9 | 10 | alt JWT token exists and JWT token validation success 11 | gateway_api_service -> todo_service: GetToDoReq(id) + JWT user token 12 | todo_service -> todo_service_db: select todo 13 | todo_service <-- todo_service_db 14 | alt todo exists 15 | gateway_api_service <-- todo_service: GetToDoRsp(item) 16 | user <-- gateway_api_service: GetToDoRsp(item) 17 | else todo does not exists 18 | gateway_api_service <-- todo_service: grpc.StatusCode.NOT_FOUND 19 | user <-- gateway_api_service: grpc.StatusCode.NOT_FOUND 20 | end 21 | else JWT token does not exists or JWT token validation error 22 | user <-- gateway_api_service: grpc.StatusCode.UNAUTHENTICATED 23 | end 24 | 25 | @enduml 26 | -------------------------------------------------------------------------------- /docs/diagrams/src/list_todo_sequence.txt: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title List todo 4 | 5 | !includesub sequence_participants.txt!todo 6 | 7 | user -> gateway_api_service: ListToDoReq(limit) + JWT user token 8 | gateway_api_service -> gateway_api_service: check and validate JWT token 9 | 10 | alt JWT token exists and JWT token validation success 11 | gateway_api_service -> todo_service: ListToDoReq(limit) + JWT user token 12 | todo_service -> todo_service_db: select todos 13 | todo_service <-- todo_service_db 14 | gateway_api_service <-- todo_service: ListToDoRsp(items) 15 | user <-- gateway_api_service: ListToDoRsp(items) 16 | else JWT token does not exists or JWT token validation error 17 | user <-- gateway_api_service: grpc.StatusCode.UNAUTHENTICATED 18 | end 19 | 20 | @enduml 21 | -------------------------------------------------------------------------------- /docs/diagrams/src/login_user_sequence.txt: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title Login user 4 | 5 | !includesub sequence_participants.txt!user 6 | 7 | user -> gateway_api_service: LoginReq(username, password) 8 | gateway_api_service -> user_service: LoginReq(username, password) 9 | user_service -> user_service_db: select user 10 | user_service <-- user_service_db 11 | 12 | alt user exists 13 | user_service -> user_service: validate password 14 | alt password matching 15 | user_service -> user_service: generate JWT token 16 | gateway_api_service <-- user_service: LoginRsp(jwt_token, expiration) 17 | user <-- gateway_api_service: LoginRsp(jwt_token, expiration) 18 | else password differs 19 | gateway_api_service <-- user_service: grpc.StatusCode.UNAUTHENTICATED 20 | user <-- gateway_api_service: grpc.StatusCode.UNAUTHENTICATED 21 | end 22 | else user not exists in db 23 | gateway_api_service <-- user_service: grpc.StatusCode.UNAUTHENTICATED 24 | user <-- gateway_api_service: grpc.StatusCode.UNAUTHENTICATED 25 | end 26 | 27 | @enduml 28 | -------------------------------------------------------------------------------- /docs/diagrams/src/register_user_sequence.txt: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title Register user 4 | 5 | !includesub sequence_participants.txt!user 6 | 7 | user -> gateway_api_service: RegisterReq(username, password, email) 8 | gateway_api_service -> user_service: RegisterReq(username, password, email) 9 | user_service -> user_service: validate RegisterReq 10 | 11 | alt successful validation 12 | user_service -> user_service_db: insert user 13 | user_service <-- user_service_db 14 | gateway_api_service <-- user_service: RegisterRsp() 15 | user <-- gateway_api_service: RegisterRsp() 16 | else unsuccessful validation 17 | gateway_api_service <-- user_service: grpc.StatusCode.INVALID_ARGUMENT 18 | user <-- gateway_api_service: grpc.StatusCode.INVALID_ARGUMENT 19 | end 20 | 21 | @enduml 22 | -------------------------------------------------------------------------------- /docs/diagrams/src/sequence_participants.txt: -------------------------------------------------------------------------------- 1 | @startuml sequence_participants 2 | 3 | !startsub todo 4 | actor "User" as user 5 | box "microservices backend" #LightBlue 6 | participant "Gateway API service" as gateway_api_service 7 | participant "Todo service" as todo_service 8 | database "Todo service db" as todo_service_db 9 | end box 10 | !endsub 11 | 12 | !startsub user 13 | actor "User" as user 14 | box "microservices backend" #LightBlue 15 | participant "Gateway API service" as gateway_api_service 16 | participant "User service" as user_service 17 | database "User service db" as user_service_db 18 | end box 19 | !endsub 20 | 21 | @enduml 22 | -------------------------------------------------------------------------------- /docs/diagrams/src/update_todo_sequence.txt: -------------------------------------------------------------------------------- 1 | @startuml 2 | 3 | title Update todo 4 | 5 | !includesub sequence_participants.txt!todo 6 | 7 | user -> gateway_api_service: UpdateToDoReq(item) + JWT user token 8 | gateway_api_service -> gateway_api_service: check and validate JWT token 9 | 10 | alt JWT token exists and JWT token validation success 11 | gateway_api_service -> todo_service: UpdateToDoReq(item) + JWT user token 12 | todo_service -> todo_service_db: update todo 13 | todo_service <-- todo_service_db 14 | alt todo updated 15 | gateway_api_service <-- todo_service: UpdateToDoRsp() 16 | user <-- gateway_api_service: UpdateToDoRsp() 17 | else todo does not exists 18 | gateway_api_service <-- todo_service: grpc.StatusCode.NOT_FOUND 19 | user <-- gateway_api_service: grpc.StatusCode.NOT_FOUND 20 | end 21 | else JWT token does not exists or JWT token validation error 22 | user <-- gateway_api_service: grpc.StatusCode.UNAUTHENTICATED 23 | end 24 | 25 | @enduml 26 | -------------------------------------------------------------------------------- /gateway/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.8) 2 | 3 | project(GatewayService) 4 | 5 | set_property(GLOBAL PROPERTY USE_FOLDERS ON) 6 | 7 | option(ENABLE_TESTING "Build the tests" OFF) 8 | 9 | file(GLOB APP_SOURCE_FILES "src/*.cpp") 10 | 11 | macro(add_grpc_proto_files OUT_SRC) 12 | set(OUT_SRC_TMP) 13 | foreach(name ${ARGN}) 14 | list(APPEND OUT_SRC_TMP "${CMAKE_CURRENT_SOURCE_DIR}/protos/${name}.pb.cc") 15 | list(APPEND OUT_SRC_TMP "${CMAKE_CURRENT_SOURCE_DIR}/protos/${name}.grpc.pb.cc") 16 | endforeach() 17 | set(${OUT_SRC} ${OUT_SRC_TMP}) 18 | endmacro() 19 | 20 | set(GRPC_PROTO_FILES) 21 | add_grpc_proto_files(GRPC_PROTO_FILES "todo/todo" "user/user") 22 | add_executable(${PROJECT_NAME} ${APP_SOURCE_FILES} ${GRPC_PROTO_FILES}) 23 | 24 | set_target_properties( 25 | ${PROJECT_NAME} 26 | PROPERTIES 27 | CXX_STANDARD 17 28 | CXX_STANDARD_REQUIRED YES 29 | CXX_EXTENSIONS NO 30 | ) 31 | target_compile_options( 32 | ${PROJECT_NAME} 33 | PUBLIC 34 | -Wall 35 | -Werror 36 | -Wextra 37 | -Wpedantic 38 | -pedantic-errors 39 | ) 40 | 41 | find_package(OpenSSL REQUIRED) 42 | 43 | target_include_directories( 44 | ${PROJECT_NAME} 45 | PRIVATE 46 | "$" 47 | "$" 48 | "$" 49 | ${OPENSSL_INCLUDE_DIR} 50 | ) 51 | 52 | set(LIBPROTOBUF "/usr/lib/x86_64-linux-gnu/libprotobuf.so") 53 | set(LIBGRPC "/usr/lib/x86_64-linux-gnu/libgrpc++.so") 54 | 55 | target_link_libraries( 56 | ${PROJECT_NAME} 57 | ${LIBPROTOBUF} 58 | ${LIBGRPC} 59 | ${OPENSSL_LIBRARIES} 60 | ) 61 | -------------------------------------------------------------------------------- /gateway/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:disco 2 | 3 | RUN apt-get update \ 4 | && apt-get install --no-install-recommends -y \ 5 | build-essential \ 6 | cmake \ 7 | git \ 8 | libgrpc++-dev \ 9 | libprotobuf-dev \ 10 | openssl \ 11 | libcrypto++6 \ 12 | libssl-dev \ 13 | && rm -rf /var/lib/apt/lists/* 14 | 15 | WORKDIR /home/app 16 | COPY . . 17 | RUN chmod +x run.sh 18 | 19 | ENTRYPOINT ["./run.sh"] 20 | -------------------------------------------------------------------------------- /gateway/inc/gateway/applicationstarter.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | namespace gateway 6 | { 7 | 8 | template 9 | class ApplicationStarter final 10 | { 11 | public: 12 | explicit ApplicationStarter(std::unique_ptr pApp) 13 | : m_pApp{std::move(pApp)} 14 | { 15 | m_pApp->init(); 16 | m_pApp->run(); 17 | } 18 | ~ApplicationStarter() 19 | { 20 | m_pApp->deInit(); 21 | } 22 | 23 | private: 24 | std::unique_ptr m_pApp; 25 | }; 26 | 27 | } 28 | -------------------------------------------------------------------------------- /gateway/inc/gateway/config.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | namespace gateway 8 | { 9 | 10 | class Config final 11 | { 12 | public: 13 | Config() = default; 14 | ~Config() = default; 15 | Config(const Config&) = delete; 16 | Config(Config&&) = delete; 17 | Config& operator=(const Config&) = delete; 18 | Config& operator=(Config&&) = delete; 19 | 20 | void readConfiguration(); 21 | const std::string& getServiceName() const { return m_serviceName; } 22 | uint32_t getServicePort() const { return m_servicePort; } 23 | const std::string& getToDoServiceName() const { return m_todoServiceName; } 24 | uint32_t getToDoServicePort() const { return m_todoServicePort; } 25 | const std::string& getUserServiceName() const { return m_userServiceName; } 26 | uint32_t getUserServicePort() const { return m_userServicePort; } 27 | const std::string& getServerCert() const { return m_serverCert; } 28 | const std::string& getServerPrivateKey() const { return m_serverPrivateKey; } 29 | const std::string& getJwtPrivateKey() const { return m_jwtPrivateKey; } 30 | const std::string& getJwtPublicKey() const { return m_jwtPublicKey; } 31 | 32 | template 33 | T convertTo(const std::string& rStr) const; 34 | 35 | private: 36 | std::string getFile(const std::string& rFileName); 37 | std::string getEnv(const std::string& rName); 38 | 39 | std::string m_serviceName; 40 | uint32_t m_servicePort; 41 | std::string m_todoServiceName; 42 | uint32_t m_todoServicePort; 43 | std::string m_userServiceName; 44 | uint32_t m_userServicePort; 45 | std::string m_certFolder; 46 | std::string m_serverPrivateKeyFile; 47 | std::string m_serverCertFile; 48 | std::string m_serverPrivateKey; 49 | std::string m_serverCert; 50 | std::string m_jwtPrivateKeyFile; 51 | std::string m_jwtPublicKeyFile; 52 | std::string m_jwtPrivateKey; 53 | std::string m_jwtPublicKey; 54 | }; 55 | 56 | template 57 | T Config::convertTo(const std::string& rStr) const 58 | { 59 | std::istringstream ss(rStr); 60 | T ret; 61 | ss >> ret; 62 | return ret; 63 | } 64 | 65 | class NoEnvironmentVariable : public ExceptionBase 66 | { 67 | public: 68 | explicit NoEnvironmentVariable( 69 | std::string msg, 70 | std::string file, 71 | int line, 72 | std::string func) 73 | : ExceptionBase( 74 | std::move("No environment variable found: " + msg), 75 | std::move(file), line, 76 | std::move(func)) 77 | { 78 | } 79 | }; 80 | 81 | class FileOpenProblem : public ExceptionBase 82 | { 83 | public: 84 | explicit FileOpenProblem( 85 | std::string msg, 86 | std::string file, 87 | int line, 88 | std::string func) 89 | : ExceptionBase( 90 | std::move("Failed to open file: " + msg), 91 | std::move(file), line, 92 | std::move(func)) 93 | { 94 | } 95 | }; 96 | 97 | } 98 | -------------------------------------------------------------------------------- /gateway/inc/gateway/exceptionbase.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #define thrower_2(type, msg) throw type(msg, __FILE__, __LINE__, __func__) 8 | #define thrower_1(type) throw type(__FILE__, __LINE__, __func__) 9 | 10 | #define GET_MACRO(_1, _2, NAME, ...) NAME 11 | #define thrower(...) GET_MACRO(__VA_ARGS__, thrower_2, thrower_1, x) (__VA_ARGS__) 12 | 13 | namespace gateway 14 | { 15 | 16 | class ExceptionBase : public std::exception 17 | { 18 | public: 19 | explicit ExceptionBase(std::string msg, std::string file, int line, std::string func) 20 | : m_msg{std::move(msg)}, 21 | m_file{std::move(file)}, 22 | m_line{line}, 23 | m_func{std::move(func)} 24 | { 25 | } 26 | ~ExceptionBase() = default; 27 | ExceptionBase(const ExceptionBase&) = delete; 28 | ExceptionBase(ExceptionBase&&) = delete; 29 | ExceptionBase& operator=(const ExceptionBase&) = delete; 30 | ExceptionBase& operator=(ExceptionBase&&) = delete; 31 | 32 | const char* what() const noexcept override { return m_msg.c_str(); } 33 | const char* file() const { return m_file.c_str(); } 34 | int line() const { return m_line; } 35 | const char* func() const { return m_func.c_str(); } 36 | 37 | virtual void print(std::ostream& os) const 38 | { 39 | os << "Exception occured: " 40 | << m_msg 41 | << " " 42 | << m_file 43 | << " " 44 | << m_func 45 | << " " 46 | << m_line 47 | << std::endl; 48 | } 49 | 50 | friend std::ostream& operator<<(std::ostream& os, const ExceptionBase& rEx) 51 | { 52 | rEx.print(os); 53 | return os; 54 | } 55 | 56 | private: 57 | const std::string m_msg; 58 | const std::string m_file; 59 | const int m_line; 60 | const std::string m_func; 61 | }; 62 | 63 | } 64 | -------------------------------------------------------------------------------- /gateway/inc/gateway/gatewayapp.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | namespace gateway 11 | { 12 | 13 | class GatewayApp final 14 | { 15 | public: 16 | explicit GatewayApp(std::shared_ptr pConfig); 17 | ~GatewayApp() = default; 18 | GatewayApp(const GatewayApp&) = delete; 19 | GatewayApp(GatewayApp&&) = delete; 20 | GatewayApp& operator=(const GatewayApp&) = delete; 21 | GatewayApp& operator=(GatewayApp&&) = delete; 22 | 23 | void init(); 24 | void run(); 25 | void deInit(); 26 | 27 | private: 28 | std::shared_ptr m_pConfig; 29 | std::shared_ptr m_pAuthProcessor; 30 | std::unique_ptr m_pUserService; 31 | std::unique_ptr m_pToDoService; 32 | std::unique_ptr m_pServer; 33 | }; 34 | 35 | } 36 | -------------------------------------------------------------------------------- /gateway/inc/gateway/gatewayauthmetadataprocessor.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace gateway 9 | { 10 | 11 | class GatewayAuthMetadataProcessor final : public grpc::AuthMetadataProcessor 12 | { 13 | public: 14 | explicit GatewayAuthMetadataProcessor(std::shared_ptr pConfig); 15 | ~GatewayAuthMetadataProcessor() = default; 16 | GatewayAuthMetadataProcessor(const GatewayAuthMetadataProcessor&) = delete; 17 | GatewayAuthMetadataProcessor(GatewayAuthMetadataProcessor&&) = delete; 18 | GatewayAuthMetadataProcessor& operator=(const GatewayAuthMetadataProcessor&) = delete; 19 | GatewayAuthMetadataProcessor& operator=(GatewayAuthMetadataProcessor&&) = delete; 20 | 21 | grpc::Status Process( 22 | const InputMetadata& rAuthMetadata, 23 | grpc::AuthContext* pContext, 24 | OutputMetadata* pConsumedAuthMetadata, 25 | OutputMetadata* pResponseMetadata) final; 26 | 27 | void addMethodToWhiteList(const std::string& rMethodName); 28 | 29 | private: 30 | bool authNeeded(const std::string& rMethodName); 31 | 32 | std::shared_ptr m_pConfig; 33 | std::vector m_methodsWhiteList; 34 | 35 | static const std::string m_kPathKey; 36 | static const std::string m_kAuthKey; 37 | static const std::string m_kUserTokenKey; 38 | }; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /gateway/inc/gateway/todoclient.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace gateway 10 | { 11 | 12 | class ToDoClient final 13 | { 14 | public: 15 | explicit ToDoClient(std::unique_ptr pStub) 16 | : m_pStub(std::move(pStub)) 17 | { 18 | } 19 | ~ToDoClient() = default; 20 | ToDoClient(const ToDoClient&) = delete; 21 | ToDoClient(ToDoClient&&) = delete; 22 | ToDoClient& operator=(const ToDoClient&) = delete; 23 | ToDoClient& operator=(ToDoClient&&) = delete; 24 | 25 | grpc::Status createToDo( 26 | const std::string& rUserToken, 27 | const CreateToDoReq& rRequest, 28 | CreateToDoRsp& rResponse) 29 | { 30 | auto pContext = ToDoClientContextBuilder().setUserToken(rUserToken).build(); 31 | return m_pStub->CreateToDo(pContext.get(), rRequest, &rResponse); 32 | } 33 | grpc::Status getToDo( 34 | const std::string& rUserToken, 35 | const GetToDoReq& rRequest, 36 | GetToDoRsp& rResponse) 37 | { 38 | auto pContext = ToDoClientContextBuilder().setUserToken(rUserToken).build(); 39 | return m_pStub->GetToDo(pContext.get(), rRequest, &rResponse); 40 | } 41 | grpc::Status deleteToDo( 42 | const std::string& rUserToken, 43 | const DeleteToDoReq& rRequest, 44 | DeleteToDoRsp& rResponse) 45 | { 46 | auto pContext = ToDoClientContextBuilder().setUserToken(rUserToken).build(); 47 | return m_pStub->DeleteToDo(pContext.get(), rRequest, &rResponse); 48 | } 49 | grpc::Status updateToDo( 50 | const std::string& rUserToken, 51 | const UpdateToDoReq& rRequest, 52 | UpdateToDoRsp& rResponse) 53 | { 54 | auto pContext = ToDoClientContextBuilder().setUserToken(rUserToken).build(); 55 | return m_pStub->UpdateToDo(pContext.get(), rRequest, &rResponse); 56 | } 57 | grpc::Status listToDo( 58 | const std::string& rUserToken, 59 | const ListToDoReq& rRequest, 60 | ListToDoRsp& rResponse) 61 | { 62 | auto pContext = ToDoClientContextBuilder().setUserToken(rUserToken).build(); 63 | return m_pStub->ListToDo(pContext.get(), rRequest, &rResponse); 64 | } 65 | 66 | private: 67 | class ToDoClientContextBuilder 68 | { 69 | using Self = ToDoClientContextBuilder; 70 | using Ctxt = grpc::ClientContext; 71 | using CtxtPtr = std::unique_ptr; 72 | public: 73 | ToDoClientContextBuilder() = default; 74 | ~ToDoClientContextBuilder() = default; 75 | 76 | Self& setUserToken(const std::string& rUserToken) 77 | { 78 | m_pContext->AddMetadata(m_kUserTokenKey, rUserToken); 79 | return *this; 80 | } 81 | 82 | CtxtPtr build() 83 | { 84 | return std::move(m_pContext); 85 | } 86 | 87 | private: 88 | CtxtPtr m_pContext = std::make_unique(); 89 | const std::string m_kUserTokenKey{"user-token"}; 90 | }; 91 | 92 | std::unique_ptr m_pStub; 93 | }; 94 | 95 | } 96 | -------------------------------------------------------------------------------- /gateway/inc/gateway/todoserviceimpl.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace gateway 10 | { 11 | 12 | class ToDoServiceImpl final : public ToDo::Service 13 | { 14 | public: 15 | explicit ToDoServiceImpl(std::unique_ptr pClient) 16 | : m_pClient(std::move(pClient)) 17 | { 18 | } 19 | ~ToDoServiceImpl() = default; 20 | ToDoServiceImpl(const ToDoServiceImpl&) = delete; 21 | ToDoServiceImpl(ToDoServiceImpl&&) = delete; 22 | ToDoServiceImpl& operator=(const ToDoServiceImpl&) = delete; 23 | ToDoServiceImpl& operator=(ToDoServiceImpl&&) = delete; 24 | 25 | private: 26 | grpc::Status CreateToDo( 27 | grpc::ServerContext* pContext, 28 | const CreateToDoReq* pRequest, 29 | CreateToDoRsp* pReply) final 30 | { 31 | return m_pClient->createToDo(getUserTokenFromAuthContext(*pContext), *pRequest, *pReply); 32 | } 33 | grpc::Status GetToDo( 34 | grpc::ServerContext* pContext, 35 | const GetToDoReq* pRequest, 36 | GetToDoRsp* pReply) final 37 | { 38 | return m_pClient->getToDo( 39 | getUserTokenFromAuthContext(*pContext), *pRequest, *pReply); 40 | } 41 | grpc::Status DeleteToDo( 42 | grpc::ServerContext* pContext, 43 | const DeleteToDoReq* pRequest, 44 | DeleteToDoRsp* pReply) final 45 | { 46 | return m_pClient->deleteToDo( 47 | getUserTokenFromAuthContext(*pContext), *pRequest, *pReply); 48 | } 49 | grpc::Status UpdateToDo( 50 | grpc::ServerContext* pContext, 51 | const UpdateToDoReq* pRequest, 52 | UpdateToDoRsp* pReply) final 53 | { 54 | return m_pClient->updateToDo( 55 | getUserTokenFromAuthContext(*pContext), *pRequest, *pReply); 56 | } 57 | grpc::Status ListToDo( 58 | grpc::ServerContext* pContext, 59 | const ListToDoReq* pRequest, 60 | ListToDoRsp* pReply) final 61 | { 62 | return m_pClient->listToDo( 63 | getUserTokenFromAuthContext(*pContext), *pRequest, *pReply); 64 | } 65 | 66 | std::string getUserTokenFromAuthContext(const grpc::ServerContext& rContext) const 67 | { 68 | std::vector tokenVec = 69 | rContext.auth_context()->FindPropertyValues("user-token"); 70 | return std::string(tokenVec.at(0).cbegin(), tokenVec.at(0).cend()); 71 | } 72 | 73 | std::unique_ptr m_pClient; 74 | }; 75 | 76 | } 77 | -------------------------------------------------------------------------------- /gateway/inc/gateway/userclient.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace gateway 9 | { 10 | 11 | class UserClient final 12 | { 13 | public: 14 | explicit UserClient(std::unique_ptr pStub) 15 | : m_pStub(std::move(pStub)) 16 | { 17 | } 18 | ~UserClient() = default; 19 | UserClient(const UserClient&) = delete; 20 | UserClient(UserClient&&) = delete; 21 | UserClient& operator=(const UserClient&) = delete; 22 | UserClient& operator=(UserClient&&) = delete; 23 | 24 | grpc::Status login(const LoginReq& rRequest, LoginRsp& rResponse) 25 | { 26 | grpc::ClientContext context; 27 | return m_pStub->Login(&context, rRequest, &rResponse); 28 | } 29 | 30 | grpc::Status registerCall(const RegisterReq& rRequest, RegisterRsp& rResponse) 31 | { 32 | grpc::ClientContext context; 33 | return m_pStub->Register(&context, rRequest, &rResponse); 34 | } 35 | 36 | private: 37 | std::unique_ptr m_pStub; 38 | }; 39 | 40 | } 41 | -------------------------------------------------------------------------------- /gateway/inc/gateway/userserviceimpl.hpp: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | namespace gateway 9 | { 10 | 11 | class UserServiceImpl final : public User::Service 12 | { 13 | public: 14 | explicit UserServiceImpl(std::unique_ptr pClient) 15 | : m_pClient(std::move(pClient)) 16 | { 17 | } 18 | ~UserServiceImpl() = default; 19 | UserServiceImpl(const UserServiceImpl&) = delete; 20 | UserServiceImpl(UserServiceImpl&&) = delete; 21 | UserServiceImpl& operator=(const UserServiceImpl&) = delete; 22 | UserServiceImpl& operator=(UserServiceImpl&&) = delete; 23 | 24 | private: 25 | grpc::Status Login( 26 | grpc::ServerContext* pContext, 27 | const LoginReq* pRequest, 28 | LoginRsp* pReply) final 29 | { 30 | (void)pContext; 31 | return m_pClient->login(*pRequest, *pReply); 32 | } 33 | grpc::Status Register( 34 | grpc::ServerContext* pContext, 35 | const RegisterReq* pRequest, 36 | RegisterRsp* pReply) final 37 | { 38 | (void)pContext; 39 | return m_pClient->registerCall(*pRequest, *pReply); 40 | } 41 | 42 | std::unique_ptr m_pClient; 43 | }; 44 | 45 | } 46 | -------------------------------------------------------------------------------- /gateway/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | WORK_DIR=$(dirname $(readlink -f $0)) 5 | cd $WORK_DIR 6 | 7 | mkdir -p $WORK_DIR/build 8 | cd $WORK_DIR/build 9 | cmake .. -DENABLE_TESTING=OFF -DCMAKE_BUILD_TYPE=Release 10 | make 11 | ./GatewayService 12 | -------------------------------------------------------------------------------- /gateway/src/config.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | namespace gateway 6 | { 7 | 8 | void Config::readConfiguration() 9 | { 10 | m_serviceName = getEnv("SERVICE_NAME"); 11 | m_servicePort = convertTo(getEnv("SERVICE_PORT")); 12 | m_todoServiceName = getEnv("TODO_SERVICE_NAME"); 13 | m_todoServicePort = convertTo(getEnv("TODO_SERVICE_PORT")); 14 | m_userServiceName = getEnv("USER_SERVICE_NAME"); 15 | m_userServicePort = convertTo(getEnv("USER_SERVICE_PORT")); 16 | m_certFolder = getEnv("CERT_FOLDER"); 17 | m_serverCertFile = getEnv("SERVER_CERT_FILE"); 18 | m_serverPrivateKeyFile = getEnv("SERVER_PRIVATE_KEY_FILE"); 19 | m_serverCert = getFile(m_certFolder + "/" + m_serverCertFile); 20 | m_serverPrivateKey = getFile(m_certFolder + "/" + m_serverPrivateKeyFile); 21 | m_jwtPrivateKeyFile = getEnv("JWT_PRIVATE_KEY_FILE"); 22 | m_jwtPublicKeyFile = getEnv("JWT_PUBLIC_KEY_FILE"); 23 | m_jwtPrivateKey = getFile(m_certFolder + "/" + m_jwtPrivateKeyFile); 24 | m_jwtPublicKey = getFile(m_certFolder + "/" + m_jwtPublicKeyFile); 25 | } 26 | 27 | std::string Config::getFile(const std::string& rFileName) 28 | { 29 | std::string data; 30 | std::ifstream file(rFileName, std::ios::in); 31 | if (file.is_open()) 32 | { 33 | std::stringstream ss; 34 | ss << file.rdbuf(); 35 | file.close(); 36 | data = ss.str(); 37 | } 38 | else 39 | { 40 | thrower(FileOpenProblem, rFileName); 41 | } 42 | 43 | return data; 44 | } 45 | 46 | std::string Config::getEnv(const std::string& rName) 47 | { 48 | char* pValue = std::getenv(rName.c_str()); 49 | if (!pValue) 50 | { 51 | thrower(NoEnvironmentVariable, rName); 52 | } 53 | return pValue; 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /gateway/src/gatewayapp.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | namespace gateway 10 | { 11 | 12 | std::string makeServerAddress( 13 | const std::string& rServiceName, 14 | uint32_t servicePort) 15 | { 16 | std::ostringstream ss; 17 | ss << servicePort; 18 | std::string portNumber = ss.str(); 19 | std::string serverAddress(rServiceName + ":" + portNumber); 20 | return serverAddress; 21 | } 22 | 23 | void waitForConnectedChannel( 24 | std::shared_ptr pChannel, 25 | uint32_t timeoutSec = 10) 26 | { 27 | std::chrono::system_clock::time_point deadline = 28 | std::chrono::system_clock::now() + std::chrono::seconds(timeoutSec); 29 | pChannel->WaitForConnected(deadline); 30 | } 31 | 32 | 33 | GatewayApp::GatewayApp(std::shared_ptr pConfig) 34 | : m_pConfig{pConfig} 35 | { 36 | } 37 | 38 | void GatewayApp::init() 39 | { 40 | try 41 | { 42 | m_pConfig->readConfiguration(); 43 | } 44 | catch (gateway::NoEnvironmentVariable& rEx) 45 | { 46 | std::cerr << rEx; 47 | throw; 48 | } 49 | catch (gateway::FileOpenProblem& rEx) 50 | { 51 | std::cerr << rEx; 52 | throw; 53 | } 54 | 55 | grpc::SslServerCredentialsOptions::PemKeyCertPair pkcp; 56 | pkcp.private_key = m_pConfig->getServerPrivateKey(); 57 | pkcp.cert_chain = m_pConfig->getServerCert(); 58 | 59 | grpc::SslServerCredentialsOptions ssl_opts; 60 | ssl_opts.pem_key_cert_pairs.push_back(pkcp); 61 | ssl_opts.pem_root_certs = ""; 62 | 63 | std::shared_ptr creds = grpc::SslServerCredentials(ssl_opts); 64 | m_pAuthProcessor = std::make_shared(m_pConfig); 65 | m_pAuthProcessor->addMethodToWhiteList("/User/Login"); 66 | m_pAuthProcessor->addMethodToWhiteList("/User/Register"); 67 | creds->SetAuthMetadataProcessor(m_pAuthProcessor); 68 | 69 | std::string userServerAddress = 70 | makeServerAddress( 71 | m_pConfig->getUserServiceName(), 72 | m_pConfig->getUserServicePort()); 73 | std::string todoServerAddress = 74 | makeServerAddress( 75 | m_pConfig->getToDoServiceName(), 76 | m_pConfig->getToDoServicePort()); 77 | 78 | auto pUserChannel = grpc::CreateChannel( 79 | userServerAddress, 80 | grpc::InsecureChannelCredentials()); 81 | waitForConnectedChannel(pUserChannel); 82 | auto pUserStub = User::NewStub(std::move(pUserChannel)); 83 | auto pUserClient = std::make_unique(std::move(pUserStub)); 84 | m_pUserService = std::make_unique(std::move(pUserClient)); 85 | 86 | auto pToDoChannel = grpc::CreateChannel( 87 | todoServerAddress, 88 | grpc::InsecureChannelCredentials()); 89 | waitForConnectedChannel(pToDoChannel); 90 | auto pToDoStub = ToDo::NewStub(std::move(pToDoChannel)); 91 | auto pToDoClient = std::make_unique(std::move(pToDoStub)); 92 | m_pToDoService = std::make_unique(std::move(pToDoClient)); 93 | 94 | std::string gatewayServerAddress = 95 | makeServerAddress( 96 | m_pConfig->getServiceName(), 97 | m_pConfig->getServicePort()); 98 | 99 | grpc::ServerBuilder builder; 100 | builder.AddListeningPort(gatewayServerAddress, creds); 101 | builder.RegisterService(m_pUserService.get()); 102 | builder.RegisterService(m_pToDoService.get()); 103 | m_pServer = builder.BuildAndStart(); 104 | } 105 | 106 | void GatewayApp::run() 107 | { 108 | m_pServer->Wait(); 109 | } 110 | 111 | void GatewayApp::deInit() 112 | { 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /gateway/src/gatewayauthmetadataprocessor.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | namespace gateway 5 | { 6 | 7 | const std::string GatewayAuthMetadataProcessor::m_kPathKey{":path"}; 8 | const std::string GatewayAuthMetadataProcessor::m_kAuthKey{"authorization"}; 9 | const std::string GatewayAuthMetadataProcessor::m_kUserTokenKey{"user-token"}; 10 | 11 | GatewayAuthMetadataProcessor::GatewayAuthMetadataProcessor(std::shared_ptr pConfig) 12 | : m_pConfig{pConfig} 13 | { 14 | } 15 | 16 | grpc::Status GatewayAuthMetadataProcessor::Process( 17 | const InputMetadata& rAuthMetadata, 18 | grpc::AuthContext* pContext, 19 | OutputMetadata* pConsumedAuthMetadata, 20 | OutputMetadata* pResponseMetadata) 21 | { 22 | (void)pResponseMetadata; 23 | 24 | auto methodIt = rAuthMetadata.find(m_kPathKey); 25 | if (methodIt == rAuthMetadata.end()) 26 | { 27 | return grpc::Status(grpc::StatusCode::INTERNAL, "Internal Error"); 28 | } 29 | 30 | auto methodName = std::string(methodIt->second.cbegin(), methodIt->second.cend()); 31 | 32 | if (!authNeeded(methodName)) 33 | { 34 | return grpc::Status::OK; 35 | } 36 | 37 | auto tokenIt = rAuthMetadata.find(m_kAuthKey); 38 | if (tokenIt == rAuthMetadata.end()) 39 | { 40 | return grpc::Status(grpc::StatusCode::UNAUTHENTICATED, "Missing Token"); 41 | } 42 | 43 | auto token = std::string(tokenIt->second.cbegin(), tokenIt->second.cend()); 44 | 45 | try 46 | { 47 | auto decodedToken = jwt::decode(token); 48 | jwt::verify() 49 | .allow_algorithm(jwt::algorithm::rs256{m_pConfig->getJwtPublicKey()}) 50 | .verify(decodedToken); 51 | } 52 | catch (const std::invalid_argument&) 53 | { 54 | return grpc::Status( 55 | grpc::StatusCode::UNAUTHENTICATED, 56 | std::string("Token verification error")); 57 | } 58 | catch (const jwt::token_verification_exception&) 59 | { 60 | return grpc::Status( 61 | grpc::StatusCode::UNAUTHENTICATED, 62 | std::string("Token verification error")); 63 | } 64 | 65 | pContext->AddProperty(m_kUserTokenKey, token); 66 | pContext->SetPeerIdentityPropertyName(m_kUserTokenKey); 67 | pConsumedAuthMetadata->insert(std::make_pair(m_kUserTokenKey, token)); 68 | 69 | return grpc::Status::OK; 70 | } 71 | 72 | void GatewayAuthMetadataProcessor::addMethodToWhiteList(const std::string& rMethodName) 73 | { 74 | if (std::find(m_methodsWhiteList.begin(), m_methodsWhiteList.end(), rMethodName) == m_methodsWhiteList.end()) 75 | { 76 | m_methodsWhiteList.push_back(rMethodName); 77 | } 78 | } 79 | 80 | bool GatewayAuthMetadataProcessor::authNeeded(const std::string& rMethodName) 81 | { 82 | auto whiteListIt = std::find(m_methodsWhiteList.begin(), m_methodsWhiteList.end(), rMethodName); 83 | return m_methodsWhiteList.end() == whiteListIt; 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /gateway/src/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | int main() 11 | { 12 | try 13 | { 14 | auto pConfig = std::make_unique(); 15 | auto pApp = std::make_unique(std::move(pConfig)); 16 | gateway::ApplicationStarter appStarter(std::move(pApp)); 17 | } 18 | catch (const gateway::ExceptionBase& rEx) 19 | { 20 | std::cerr << rEx; 21 | return EXIT_FAILURE; 22 | } 23 | catch (const std::exception& rEx) 24 | { 25 | std::cerr 26 | << "Exception occured: " 27 | << rEx.what() 28 | << std::endl; 29 | return EXIT_FAILURE; 30 | } 31 | catch (...) 32 | { 33 | std::cerr 34 | << "Unknown exception occured: " 35 | << std::endl; 36 | return EXIT_FAILURE; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /protos/.gitignore: -------------------------------------------------------------------------------- 1 | .gen -------------------------------------------------------------------------------- /protos/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:disco 2 | 3 | RUN apt-get update \ 4 | && apt-get install --no-install-recommends -y \ 5 | build-essential \ 6 | git \ 7 | && rm -rf /var/lib/apt/lists/* 8 | 9 | RUN apt-get update \ 10 | && apt-get install --no-install-recommends -y \ 11 | protobuf-compiler \ 12 | protobuf-compiler-grpc \ 13 | libprotobuf-dev 14 | 15 | ENTRYPOINT ["./buildAll.sh"] 16 | -------------------------------------------------------------------------------- /protos/buildAll.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | SCRIPT_DIR=$(dirname $(readlink -f $0)) 5 | cd $SCRIPT_DIR 6 | 7 | PB_LANGS="cpp python" 8 | GEN_CPP_DIR=gen/pb_cpp 9 | GEN_PY_DIR=gen/pb_python 10 | 11 | function buildAll { 12 | echo "Buidling service's protocol buffers" 13 | mkdir -p $SCRIPT_DIR/.gen 14 | for d in */; do 15 | buildDir $d 16 | done 17 | } 18 | 19 | function buildDir { 20 | currentDir="$1" 21 | cd $currentDir 22 | 23 | mkdir -p $SCRIPT_DIR/.gen/pb_cpp/$currentDir && protoc -I ./ --grpc_out=$SCRIPT_DIR/.gen/pb_cpp/$currentDir --cpp_out=$SCRIPT_DIR/.gen/pb_cpp/$currentDir --plugin=protoc-gen-grpc=`which grpc_cpp_plugin` *.proto 24 | mkdir -p $SCRIPT_DIR/.gen/pb_python/$currentDir && protoc -I ./ --grpc_out=$SCRIPT_DIR/.gen/pb_python/$currentDir --python_out=$SCRIPT_DIR/.gen/pb_python/$currentDir --plugin=protoc-gen-grpc=`which grpc_python_plugin` *.proto 25 | 26 | touch $SCRIPT_DIR/.gen/pb_python/__init__.py 27 | sed -i -E 's/^(import) (.*) (as) (.*)/from . \1 \2 \3 \4/' $SCRIPT_DIR/.gen/pb_python/$currentDir/*_grpc.py 28 | cd .. 29 | } 30 | 31 | buildAll 32 | -------------------------------------------------------------------------------- /protos/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | SCRIPT_DIR=$(dirname $(readlink -f $0)) 5 | cd $SCRIPT_DIR 6 | 7 | docker build -t todo-protos -f Dockerfile . 8 | docker run --rm \ 9 | -v $SCRIPT_DIR:/home/build \ 10 | -w /home/build \ 11 | --name todo-protos todo-protos:latest 12 | -------------------------------------------------------------------------------- /protos/todo/todo.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | import "google/protobuf/timestamp.proto"; 4 | 5 | service ToDo { 6 | rpc CreateToDo (CreateToDoReq) returns (CreateToDoRsp) {} 7 | rpc GetToDo (GetToDoReq) returns (GetToDoRsp) {} 8 | rpc DeleteToDo (DeleteToDoReq) returns (DeleteToDoRsp) {} 9 | rpc UpdateToDo (UpdateToDoReq) returns (UpdateToDoRsp) {} 10 | rpc ListToDo (ListToDoReq) returns (ListToDoRsp) {} 11 | } 12 | 13 | message ToDoEntry { 14 | int32 id = 1; 15 | string title = 2; 16 | string text = 3; 17 | google.protobuf.Timestamp created_at = 4; 18 | google.protobuf.Timestamp updated_at = 5; 19 | } 20 | 21 | message CreateToDoReq { 22 | ToDoEntry item = 2; 23 | } 24 | 25 | message CreateToDoRsp { 26 | int32 id = 1; 27 | } 28 | 29 | message GetToDoReq { 30 | int32 id = 1; 31 | } 32 | 33 | message GetToDoRsp { 34 | ToDoEntry item = 1; 35 | } 36 | 37 | message DeleteToDoReq { 38 | int32 id = 1; 39 | } 40 | 41 | message DeleteToDoRsp { 42 | } 43 | 44 | message UpdateToDoReq { 45 | ToDoEntry item = 1; 46 | } 47 | 48 | message UpdateToDoRsp { 49 | } 50 | 51 | message ListToDoReq { 52 | uint32 limit = 1; 53 | } 54 | 55 | message ListToDoRsp { 56 | repeated ToDoEntry items = 1; 57 | } 58 | -------------------------------------------------------------------------------- /protos/user/user.proto: -------------------------------------------------------------------------------- 1 | syntax = "proto3"; 2 | 3 | service User { 4 | rpc Login (LoginReq) returns (LoginRsp) {} 5 | rpc Register (RegisterReq) returns (RegisterRsp) {} 6 | } 7 | 8 | message LoginReq { 9 | string username = 1; 10 | string password = 2; 11 | } 12 | 13 | message LoginRsp { 14 | string jwt_token = 1; 15 | string expiration = 2; 16 | } 17 | 18 | message RegisterReq { 19 | string username = 1; 20 | string password = 2; 21 | string email = 3; 22 | } 23 | 24 | message RegisterRsp { 25 | 26 | } 27 | -------------------------------------------------------------------------------- /test/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:disco 2 | 3 | RUN apt-get update \ 4 | && apt-get install --no-install-recommends -y \ 5 | git \ 6 | build-essential \ 7 | python3 \ 8 | python3-pip \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | WORKDIR /home/tests 12 | 13 | COPY requirements.txt . 14 | 15 | RUN pip3 install --upgrade pip \ 16 | && pip3 install --upgrade setuptools \ 17 | && pip3 install -r requirements.txt 18 | 19 | COPY . . 20 | 21 | ENTRYPOINT ["python3", "-m", "unittest", "-v"] 22 | -------------------------------------------------------------------------------- /test/certs/jwtRS256.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 38 | XalTlxsMaIFnxtgUeMfft8Z1JmjIwEJEx7Nq+YeBFBalAe5B1/W9rqt3VMSx+tk5 39 | DIg241XD9siRjQwpio96UeAA0ykFpCF3ihyJUIOmrdXceZAl09u7zVfwTbJO8s/x 40 | yrUelwKCAQEArJHJ+VQV8q3u4qmWdZBsrMf/7ExwFVZhSvpHwA1UI1zbZiLLdhDn 41 | 1A9Skr/gdEs09yZI/+dxhiEingwOHQzNc2XI2dpaA860kBrMixCvFU8O9lzEUOi0 42 | jm0pG916Jpc1WLkar94WztUYkfnxThIdFb6E9avxC+u/Etu4MPHTtIbkHDrxwJGN 43 | f3v3dDqzX9dZw2UuQX4akf+qPUeqsMkcK41t/8T4InslDgF7qHaT5tfIm88gXNCf 44 | svZhW++5sxFPgO3aFgEMgAn/YHNS+2TGO7G681xiK2Q6YJGg2VynCX4EKakh2yU2 45 | mUPeGOp47AVQZitVD3t7VNvm1CwpqfJ8oQKCAQEAsWMBsB3Wpw2Z3xsn9ogZ/7gg 46 | rrzYU888bn3KOQgfA9nYcjuE0T6eD7R/pEdl4Lekvi9dp4GUK/ff3lgA/cb92u32 47 | d7Th3he/chJPH04BTKCTSxi2Kev1W8SLF+/VIFVIsQN9mm69whZicUM5gCj0Gl83 48 | 5H2TV+9dOQGUokUNL6f+uFqibpTieCG6c4b5KdfwB1iVswHhxFosWkEkmtBm+WPI 49 | TIbVFHi5IGas+Xp7Yx464ZnquO+tqLwMdDb/i7+eNXRP4jumJzJMnBzVmVOXE9aY 50 | joJQwow3Mq6tGCcAh9K8sVluwLcOVhpDd2wmQb29F76jb7lq1lMypdTK6wrSrw== 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /test/certs/jwtRS256.key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxavDCaeQTrI8MZKKCejK 3 | Wgrp4ba6umj3npmBsHHwnOxSZFBRyS7kmA19Uxg0+HQac7/xiCdCJ5M4QnALsOTV 4 | BgGFWIUYZBFqgfmejJ4ri+mo4lNuDmvwgwVDlqFj9mJXM1Ew8az9JKDtCwyPqUbo 5 | /dpZFdk4MgkhFNJJA2A/Bgb2b5V2m79Af0CWhgkzaBGiWqNsWFucplOzuglmHXqa 6 | WWEPdDnTpyRFIAPXXmgIKyxeNvxBXJrp59Ykgt0pldTD7p4Mxd4rUOT2VaBUXCT1 7 | +stuqSX0P6Ec+vaBXL/MvBhAWqyUvOzCm+6xpcErCBN4ANBTcbsxGzTTZwJu+FYM 8 | IT/n+oOt8uGbNP3LxVWZTo8/o0ubW4s+ZdkHrz+UIwAALiudNRPN/Hq3r9o/o0yQ 9 | NUxbK6T9YQTMrHgM3ObDAhSWtM1RrQWxNHeBM4tKrHayw4o5uQ8WvHgQXRkfzbgX 10 | x3D+wrF9idT7DwaOfxDTZpPrO2C2RA7utBTT/vosw4Ny4QKzckcGecM1q9exKyma 11 | qpHE0PM7HO7tQtlkvtHKPnFK2F3CjSsdILuoR/L/IzFVrp1y70Nv5DixiCsAaJtw 12 | toXv9LsS8FdyiSzbAj1nyOQLMhUtc8dU+g0Mdy4kvO8yKPi/n7yK2EMmMWLZtYNd 13 | TKPPpOg0z3aX9E/rSPoSllcCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /test/certs/server.crt: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDlDCCAnygAwIBAgIJAN9h7BPDhhpSMA0GCSqGSIb3DQEBCwUAMF8xCzAJBgNV 3 | BAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBX 4 | aWRnaXRzIFB0eSBMdGQxGDAWBgNVBAMMD2dhdGV3YXlfc2VydmljZTAeFw0xOTA2 5 | MjExNjU4MzBaFw0yMDA2MjAxNjU4MzBaMF8xCzAJBgNVBAYTAkFVMRMwEQYDVQQI 6 | DApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQx 7 | GDAWBgNVBAMMD2dhdGV3YXlfc2VydmljZTCCASIwDQYJKoZIhvcNAQEBBQADggEP 8 | ADCCAQoCggEBAMwx9Wl78Me8zYgypUH3cazKibxqOCEtJBDvMbqWmBQeu0L8NX/7 9 | 5x1hreNhlt55qQBJMRmG3Y8GfBvU7ryaiUuFAwrXb0rt5vzjSGomBebvs0dX49oy 10 | P5L6RCSqh16pV9LiEPuXLW4iOsircQMvMqVO8GxiywI25yJN9WLtdulFdCg9Mn6g 11 | Q0dOHpUyWyxwt6uZp5mMcK7az+FtMv/HsGUTLYar3IESO5jegMBdYgqcMCjuSKzB 12 | c1rNbNjFVlbnQUEXdhi4XHfTPfd3P3nb2wgFRAn/vNHEY5uB06GH1Auw00aObzDl 13 | ocsDebjSPIFWTgZrblW+ekQzTF/Ew8e6Hv8CAwEAAaNTMFEwHQYDVR0OBBYEFA/5 14 | g07MjybzXrH0dGFVZ4x/qJcjMB8GA1UdIwQYMBaAFA/5g07MjybzXrH0dGFVZ4x/ 15 | qJcjMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAC62MkUhP2Ii 16 | hwwMgc3AwTBYXT+5HWaDL9axHKuaBG5mn1vJiSrlirveIxVD57RFzJlRaOU97RIl 17 | bCb/7wGJMvXd8lWjXJlQ/Ga8g8DX1Io52Cvr9nE1Bp2FJPE9HWKRF9ItKLG0ng1I 18 | w6BJLSlmPcsFXR9p14TdqYc/54/Zz4vrXF9ennbAYEYoA3XTUOkGTiuZAJGlehyo 19 | w7DIIfXY3ih1QGlktUg5OPsvM/0lyVhUJaMAR+3in9M29w3NM2TLxvOmeori0++B 20 | iRwM0We0A4rwnRsL0UT52vUopasO6YriLUYHNEHn0Evifwru2TgcK9TZLlNlnLGI 21 | 0gZeYS/kM2M= 22 | -----END CERTIFICATE----- 23 | -------------------------------------------------------------------------------- /test/certs/server.key: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDMMfVpe/DHvM2I 3 | MqVB93Gsyom8ajghLSQQ7zG6lpgUHrtC/DV/++cdYa3jYZbeeakASTEZht2PBnwb 4 | 1O68molLhQMK129K7eb840hqJgXm77NHV+PaMj+S+kQkqodeqVfS4hD7ly1uIjrI 5 | q3EDLzKlTvBsYssCNuciTfVi7XbpRXQoPTJ+oENHTh6VMlsscLermaeZjHCu2s/h 6 | bTL/x7BlEy2Gq9yBEjuY3oDAXWIKnDAo7kiswXNazWzYxVZW50FBF3YYuFx30z33 7 | dz9529sIBUQJ/7zRxGObgdOhh9QLsNNGjm8w5aHLA3m40jyBVk4Ga25VvnpEM0xf 8 | xMPHuh7/AgMBAAECggEAGBGneNiYKCMUIwK9QgpVxuVU2PW4SgjVIH3hYboQkQaS 9 | UjICaGSb+HWkFpJRQ0YZ6qO1+ulPwmUkSe+OeILKvjEei6+bObzTJmnwBjsXJQKK 10 | IlW00OptXfjMAXpsXHcfmAb3s4A9Zab5QOAcgSCWGrA0XMJEZa1tDzkbPJ77DkHg 11 | hJuN21NvKFvN+9QkO0MsBB1xuFvMQupdYGzskHhN/sGxLyoi6PeR2/cLqU5LYKLV 12 | Op1Sv625LmyVt4ZUaGIZNMmVhi4jwA8dJSmT4e4a83ogTrXSKW5dBZhLe0aye5DC 13 | dHm1Z2Pxw2Fvy8OPJVeO2kE9CWar2WsVlS4U54raGQKBgQD2C0VYy6i/coidwj2Q 14 | h1JiDpyzsGFDnpSKM+E1chmybdbUdifICWgHBz56tWfIgATiX/sPQq3jXUwyKomh 15 | yviBZnmaJZx5PihYv0ZrQ80jXSpJcnZVBjE3UV22NtRDcAaf1R0xUauLILWOs+b8 16 | rNtBZTbp/h6N4YwGwFylpuMZLQKBgQDUdS6lrLGkP4OT/Zona9c1ZHh3xy8Q3yQ6 17 | hFhLL3gonSQGlf1mYk6uRXD6dzF+IzkX/Dsww9WyQFIzOHz93CVPYimpt+78gPzP 18 | AdGNaLpIZmMnoiuPQW74VCv3YyPINPVknSXBMv28roGkdZSPbvw4Jcq6I5scIsvk 19 | BOPSyERcWwKBgFLbXNEYVI2UTnmCDiV7zLyE2zSP81FIOBGKgw7yi8DQyIXn/GGa 20 | +VNUly/Po1Gb+wkY9X0gIVrt8lj6hjrcFW/OSKkmfVk/G2s3jslUneKI31K4nyzm 21 | 1vAgEITqVfUX5sMwi5IuRKynqTyTTPhOZc/R+/o0v6lcCUHEye5nExdJAoGBANCO 22 | z+HW6vM/XhzSoJb+J97D5/Y805kOU3QWIs57pMrJp2nHp17wNiihBkz44N7gGNXQ 23 | xxIRzB8VQRwC4cAacY3B4QA4DSRWQtW7BUh5fBRCXiez7GrOYu7nQdmxuph4VEQW 24 | d892DNtBuwYEXneHDZTYlG+xMDyoc9o8aqCp82FtAoGBAIqscQ3KlGcCmtIWf0iz 25 | lU2Mf3pYhS3+feQc5sWyIlFrLlVNUZbahLJCSu34su+bQE93nrOQZPNVpGC6+iEE 26 | KqKSePjOZURxTCeCc4uJSECT0c5vdTnAnxnA6GDS8oXFZpT2ZQfnX4QEqszR+IK3 27 | YO3yjpXhnH/euTy2J5LXKRbB 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /test/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.0' 2 | services: 3 | e2e_tests: 4 | build: . 5 | image: e2e_tests 6 | env_file: 7 | - gateway-variables.env 8 | volumes: 9 | - ../protos/.gen/pb_python/:/home/tests/protos 10 | - ./certs/:/home/certs 11 | depends_on: 12 | - todo_service 13 | - user_service 14 | - gateway_service 15 | todo_service: 16 | build: ../todo 17 | image: todo_service 18 | expose: 19 | - 50050 20 | env_file: 21 | - todo-variables.env 22 | volumes: 23 | - ../protos/.gen/pb_python/:/home/app/todo/protos 24 | - ./certs/:/home/certs 25 | depends_on: 26 | - todo_db 27 | todo_db: 28 | image: mariadb 29 | env_file: 30 | - todo-variables.env 31 | logging: 32 | driver: "none" 33 | user_service: 34 | build: ../user 35 | image: user_service 36 | expose: 37 | - 50050 38 | env_file: 39 | - user-variables.env 40 | volumes: 41 | - ../protos/.gen/pb_python/:/home/app/user/protos 42 | - ./certs/:/home/certs 43 | depends_on: 44 | - user_db 45 | user_db: 46 | image: mariadb 47 | env_file: 48 | - user-variables.env 49 | logging: 50 | driver: "none" 51 | gateway_service: 52 | build: ../gateway 53 | image: gateway_service 54 | env_file: 55 | - gateway-variables.env 56 | expose: 57 | - 50050 58 | volumes: 59 | - ../protos/.gen/pb_cpp/:/home/app/protos 60 | - ./certs/:/home/certs 61 | depends_on: 62 | - user_service 63 | - todo_service 64 | -------------------------------------------------------------------------------- /test/e2e_base.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | 4 | import grpc 5 | 6 | from settings import settings 7 | 8 | logger = logging.getLogger(__name__) 9 | 10 | class End2EndTestBase(unittest.TestCase): 11 | @classmethod 12 | def setUpClass(cls): 13 | log_fmt = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 14 | logging.basicConfig(level=logging.DEBUG, format=log_fmt) 15 | 16 | @classmethod 17 | def tearDownClass(cls): 18 | pass 19 | 20 | def setUp(self): 21 | credentials = grpc.ssl_channel_credentials(root_certificates=settings.SERVER_CERT) 22 | self.channel = grpc.secure_channel("{service}:{port}" 23 | .format(service=settings.SERVICE_NAME, port=settings.SERVICE_PORT), credentials) 24 | grpc.channel_ready_future(self.channel).result() 25 | 26 | def tearDown(self): 27 | self.channel.close() 28 | -------------------------------------------------------------------------------- /test/gateway-variables.env: -------------------------------------------------------------------------------- 1 | SERVICE_NAME=gateway_service 2 | SERVICE_PORT=50050 3 | TODO_SERVICE_NAME=todo_service 4 | TODO_SERVICE_PORT=50050 5 | USER_SERVICE_NAME=user_service 6 | USER_SERVICE_PORT=50050 7 | CERT_FOLDER=/home/certs 8 | SERVER_PRIVATE_KEY_FILE=server.key 9 | SERVER_CERT_FILE=server.crt 10 | JWT_PRIVATE_KEY_FILE=jwtRS256.key 11 | JWT_PUBLIC_KEY_FILE=jwtRS256.key.pub 12 | -------------------------------------------------------------------------------- /test/requirements.txt: -------------------------------------------------------------------------------- 1 | protobuf 2 | grpcio 3 | PyJWT 4 | grift 5 | cryptography 6 | -------------------------------------------------------------------------------- /test/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | TESTS_DIR=$(dirname $(readlink -f $0)) 5 | cd $TESTS_DIR 6 | 7 | docker-compose build 8 | docker-compose up 9 | docker-compose rm -f -v 10 | -------------------------------------------------------------------------------- /test/settings.py: -------------------------------------------------------------------------------- 1 | import grift 2 | import schematics.types as sch_types 3 | 4 | class AppConfig(grift.BaseConfig): 5 | def __init__(self, *args, **kwargs): 6 | super().__init__(*args, **kwargs) 7 | 8 | with open(self.CERT_FOLDER + "/" + self.SERVER_CERT_FILE, "rb") as fh: 9 | self.SERVER_CERT = fh.read() 10 | with open(self.CERT_FOLDER + "/" + self.JWT_PRIVATE_KEY_FILE, "rb") as fh: 11 | self.JWT_PRIVATE_KEY = fh.read() 12 | with open(self.CERT_FOLDER + "/" + self.JWT_PUBLIC_KEY_FILE, "rb") as fh: 13 | self.JWT_PUBLIC_KEY = fh.read() 14 | 15 | SERVICE_NAME = grift.ConfigProperty( 16 | property_type=sch_types.StringType(), 17 | exclude_from_varz=True) 18 | SERVICE_PORT = grift.ConfigProperty( 19 | property_type=sch_types.IntType(), 20 | exclude_from_varz=True) 21 | CERT_FOLDER = grift.ConfigProperty( 22 | property_type=sch_types.StringType(), 23 | exclude_from_varz=True) 24 | SERVER_CERT_FILE = grift.ConfigProperty( 25 | property_type=sch_types.StringType(), 26 | exclude_from_varz=True) 27 | JWT_PRIVATE_KEY_FILE = grift.ConfigProperty( 28 | property_type=sch_types.StringType(), 29 | exclude_from_varz=True) 30 | JWT_PUBLIC_KEY_FILE = grift.ConfigProperty( 31 | property_type=sch_types.StringType(), 32 | exclude_from_varz=True) 33 | 34 | 35 | loaders = [grift.EnvLoader()] 36 | settings = AppConfig(loaders) 37 | -------------------------------------------------------------------------------- /test/test_todo.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import unittest 4 | 5 | import grpc 6 | import jwt 7 | 8 | import e2e_base 9 | import protos.todo.todo_pb2 as todo_pb2 10 | import protos.todo.todo_pb2_grpc as todo_pb2_grpc 11 | import protos.user.user_pb2 as user_pb2 12 | import protos.user.user_pb2_grpc as user_pb2_grpc 13 | from settings import settings 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | class ToDoServiceEnd2EndTest(e2e_base.End2EndTestBase): 18 | def setUp(self): 19 | super().setUp() 20 | self.user_stub = user_pb2_grpc.UserStub(self.channel) 21 | self.todo_stub = todo_pb2_grpc.ToDoStub(self.channel) 22 | 23 | def test_create_get_update_list_delete_todos(self): 24 | response = self.user_stub.Register(user_pb2.RegisterReq( 25 | username="todo_user", password="todo_pass", email="todo_test@todo_test.com")) 26 | response = self.user_stub.Login(user_pb2.LoginReq( 27 | username="todo_user", password="todo_pass")) 28 | 29 | jwt_token = (("authorization", response.jwt_token),) 30 | 31 | test_todo_entry_1 = todo_pb2.ToDoEntry( 32 | title="Title 1", 33 | text="Text 1" 34 | ) 35 | test_todo_entry_2 = todo_pb2.ToDoEntry( 36 | title="Title 2", 37 | text="Text 2" 38 | ) 39 | 40 | # Create two todos 41 | response = self.todo_stub.CreateToDo( 42 | todo_pb2.CreateToDoReq(item=test_todo_entry_1), 43 | metadata=jwt_token) 44 | test_todo_entry_1.id = response.id 45 | response = self.todo_stub.CreateToDo( 46 | todo_pb2.CreateToDoReq(item=test_todo_entry_2), 47 | metadata=jwt_token) 48 | test_todo_entry_2.id = response.id 49 | 50 | # Modify first todo 51 | test_todo_entry_1.title = "New Title 1" 52 | test_todo_entry_1.text = "New Text 1" 53 | self.todo_stub.UpdateToDo( 54 | todo_pb2.UpdateToDoReq(item=test_todo_entry_1), 55 | metadata=jwt_token) 56 | 57 | # Try modify nonexisting todo (should response with error) 58 | with self.assertRaises(grpc.RpcError) as cm: 59 | self.todo_stub.UpdateToDo( 60 | todo_pb2.UpdateToDoReq(item=todo_pb2.ToDoEntry( 61 | id=10000, 62 | title="Fake todo", 63 | text="Fake todo" 64 | )), 65 | metadata=jwt_token) 66 | self.assertEqual(cm.exception.code(), grpc.StatusCode.NOT_FOUND) 67 | 68 | # List todos (should be two todos) 69 | response = self.todo_stub.ListToDo( 70 | todo_pb2.ListToDoReq(limit=10), 71 | metadata=jwt_token) 72 | self.assertEqual(len(response.items), 2) 73 | self.assertEqual(response.items[0], test_todo_entry_1) 74 | self.assertEqual(response.items[1], test_todo_entry_2) 75 | 76 | # Get second todo 77 | response = self.todo_stub.GetToDo( 78 | todo_pb2.GetToDoReq(id=test_todo_entry_2.id), 79 | metadata=jwt_token) 80 | self.assertEqual(response.item, test_todo_entry_2) 81 | 82 | # Delete first todo 83 | self.todo_stub.DeleteToDo( 84 | request=todo_pb2.DeleteToDoReq(id=test_todo_entry_1.id), 85 | metadata=jwt_token) 86 | 87 | # List todos (should be one todo) 88 | response = self.todo_stub.ListToDo( 89 | todo_pb2.ListToDoReq(limit=10), 90 | metadata=jwt_token) 91 | self.assertEqual(len(response.items), 1) 92 | self.assertEqual(response.items[0], test_todo_entry_2) 93 | 94 | # Try to get deleted todo (should response with error) 95 | with self.assertRaises(grpc.RpcError) as cm: 96 | self.todo_stub.GetToDo( 97 | todo_pb2.GetToDoReq(id=test_todo_entry_1.id), 98 | metadata=jwt_token) 99 | self.assertEqual(cm.exception.code(), grpc.StatusCode.NOT_FOUND) 100 | 101 | # Delete second todo 102 | self.todo_stub.DeleteToDo( 103 | request=todo_pb2.DeleteToDoReq(id=test_todo_entry_2.id), 104 | metadata=jwt_token) 105 | 106 | # Try to delete nonexisting todo (should response with error) 107 | with self.assertRaises(grpc.RpcError) as cm: 108 | self.todo_stub.DeleteToDo( 109 | request=todo_pb2.DeleteToDoReq(id=test_todo_entry_2.id), 110 | metadata=jwt_token) 111 | self.assertEqual(cm.exception.code(), grpc.StatusCode.NOT_FOUND) 112 | 113 | # List todos (should be zero todo) 114 | response = self.todo_stub.ListToDo( 115 | todo_pb2.ListToDoReq(limit=10), 116 | metadata=jwt_token) 117 | self.assertEqual(len(response.items), 0) 118 | 119 | def test_error_when_wrong_token(self): 120 | test_metadata = (("authorization", "wrong_token"),) 121 | 122 | with self.assertRaises(grpc.RpcError) as cm: 123 | self.todo_stub.CreateToDo( 124 | todo_pb2.CreateToDoReq( 125 | item=todo_pb2.ToDoEntry( 126 | title="Title 2", 127 | text="Text 2") 128 | ), 129 | metadata=test_metadata) 130 | self.assertEqual(cm.exception.code(), grpc.StatusCode.UNAUTHENTICATED) 131 | 132 | with self.assertRaises(grpc.RpcError) as cm: 133 | self.todo_stub.GetToDo( 134 | todo_pb2.GetToDoReq(id=1), 135 | metadata=test_metadata) 136 | self.assertEqual(cm.exception.code(), grpc.StatusCode.UNAUTHENTICATED) 137 | 138 | with self.assertRaises(grpc.RpcError) as cm: 139 | self.todo_stub.DeleteToDo( 140 | request=todo_pb2.DeleteToDoReq(id=1), 141 | metadata=test_metadata) 142 | self.assertEqual(cm.exception.code(), grpc.StatusCode.UNAUTHENTICATED) 143 | 144 | with self.assertRaises(grpc.RpcError) as cm: 145 | self.todo_stub.UpdateToDo( 146 | todo_pb2.UpdateToDoReq(item=todo_pb2.ToDoEntry( 147 | id=1, 148 | title="title", 149 | text="text" 150 | )), 151 | metadata=test_metadata) 152 | self.assertEqual(cm.exception.code(), grpc.StatusCode.UNAUTHENTICATED) 153 | 154 | with self.assertRaises(grpc.RpcError) as cm: 155 | self.todo_stub.ListToDo( 156 | todo_pb2.ListToDoReq(limit=10), 157 | metadata=test_metadata) 158 | self.assertEqual(cm.exception.code(), grpc.StatusCode.UNAUTHENTICATED) 159 | -------------------------------------------------------------------------------- /test/test_user.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import unittest 3 | 4 | import grpc 5 | import jwt 6 | 7 | import e2e_base 8 | import protos.user.user_pb2 as user_pb2 9 | import protos.user.user_pb2_grpc as user_pb2_grpc 10 | from settings import settings 11 | 12 | logger = logging.getLogger(__name__) 13 | 14 | class UserServiceEnd2EndTest(e2e_base.End2EndTestBase): 15 | def setUp(self): 16 | super().setUp() 17 | self.stub = user_pb2_grpc.UserStub(self.channel) 18 | 19 | def test_register_user_and_login(self): 20 | response = self.stub.Register(user_pb2.RegisterReq( 21 | username="me", password="pass", email="example@example.com")) 22 | response = self.stub.Login(user_pb2.LoginReq( 23 | username="me", password="pass")) 24 | decoded_payload = jwt.decode( 25 | response.jwt_token, 26 | settings.JWT_PUBLIC_KEY, 27 | algorithms=["RS256"]) 28 | assert "sub" in decoded_payload 29 | assert "exp" in decoded_payload 30 | 31 | def test_login_not_registered_user(self): 32 | with self.assertRaises(grpc.RpcError) as cm: 33 | self.stub.Login(user_pb2.LoginReq(username="you", password="pass")) 34 | self.assertEqual(cm.exception.code(), grpc.StatusCode.UNAUTHENTICATED) 35 | 36 | def test_register_user_wrong_email(self): 37 | with self.assertRaises(grpc.RpcError) as cm: 38 | self.stub.Register(user_pb2.RegisterReq( 39 | username="John", password="pass", email="wrong_email")) 40 | self.assertEqual(cm.exception.code(), grpc.StatusCode.INVALID_ARGUMENT) 41 | -------------------------------------------------------------------------------- /test/todo-variables.env: -------------------------------------------------------------------------------- 1 | SERVICE_NAME=todo_service 2 | SERVICE_PORT=50050 3 | MYSQL_HOST=todo_db 4 | MYSQL_PORT=3306 5 | MYSQL_USER=todo 6 | MYSQL_PASSWORD=todo 7 | MYSQL_DATABASE=todo 8 | MYSQL_ROOT_PASSWORD=root 9 | CERT_FOLDER=/home/certs 10 | JWT_PRIVATE_KEY_FILE=jwtRS256.key 11 | JWT_PUBLIC_KEY_FILE=jwtRS256.key.pub 12 | -------------------------------------------------------------------------------- /test/user-variables.env: -------------------------------------------------------------------------------- 1 | SERVICE_NAME=user_service 2 | SERVICE_PORT=50050 3 | MYSQL_HOST=user_db 4 | MYSQL_PORT=3306 5 | MYSQL_USER=user 6 | MYSQL_PASSWORD=user 7 | MYSQL_DATABASE=user 8 | MYSQL_ROOT_PASSWORD=root 9 | CERT_FOLDER=/home/certs 10 | JWT_PRIVATE_KEY_FILE=jwtRS256.key 11 | JWT_PUBLIC_KEY_FILE=jwtRS256.key.pub 12 | -------------------------------------------------------------------------------- /todo/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:disco 2 | 3 | RUN apt-get update \ 4 | && apt-get install --no-install-recommends -y \ 5 | git \ 6 | build-essential \ 7 | python3 \ 8 | python3-pip \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | WORKDIR /home/app 12 | 13 | COPY requirements.txt . 14 | 15 | RUN pip3 install --upgrade pip \ 16 | && pip3 install --upgrade setuptools \ 17 | && pip3 install -r requirements.txt 18 | 19 | COPY . . 20 | 21 | ENTRYPOINT ["python3", "todo/main.py"] 22 | -------------------------------------------------------------------------------- /todo/requirements.txt: -------------------------------------------------------------------------------- 1 | protobuf 2 | grpcio 3 | mysql-connector-python 4 | grift 5 | PyJWT 6 | SQLAlchemy 7 | SQLAlchemy-Utils 8 | validate_email 9 | cryptography 10 | -------------------------------------------------------------------------------- /todo/tests/integration/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:disco 2 | 3 | RUN apt-get update \ 4 | && apt-get install --no-install-recommends -y \ 5 | git \ 6 | build-essential \ 7 | python3 \ 8 | python3-pip \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | WORKDIR /home/tests 12 | 13 | COPY requirements.txt . 14 | 15 | RUN pip3 install --upgrade pip \ 16 | && pip3 install --upgrade setuptools \ 17 | && pip3 install -r requirements.txt 18 | 19 | COPY . . 20 | 21 | ENTRYPOINT ["python3", "-m", "unittest", "-v"] 22 | -------------------------------------------------------------------------------- /todo/tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-sitko/todo-list-app-microservices/3c48dea159be3be1d92c5a0509bb44765a2b7456/todo/tests/integration/__init__.py -------------------------------------------------------------------------------- /todo/tests/integration/certs/jwtRS256.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 38 | XalTlxsMaIFnxtgUeMfft8Z1JmjIwEJEx7Nq+YeBFBalAe5B1/W9rqt3VMSx+tk5 39 | DIg241XD9siRjQwpio96UeAA0ykFpCF3ihyJUIOmrdXceZAl09u7zVfwTbJO8s/x 40 | yrUelwKCAQEArJHJ+VQV8q3u4qmWdZBsrMf/7ExwFVZhSvpHwA1UI1zbZiLLdhDn 41 | 1A9Skr/gdEs09yZI/+dxhiEingwOHQzNc2XI2dpaA860kBrMixCvFU8O9lzEUOi0 42 | jm0pG916Jpc1WLkar94WztUYkfnxThIdFb6E9avxC+u/Etu4MPHTtIbkHDrxwJGN 43 | f3v3dDqzX9dZw2UuQX4akf+qPUeqsMkcK41t/8T4InslDgF7qHaT5tfIm88gXNCf 44 | svZhW++5sxFPgO3aFgEMgAn/YHNS+2TGO7G681xiK2Q6YJGg2VynCX4EKakh2yU2 45 | mUPeGOp47AVQZitVD3t7VNvm1CwpqfJ8oQKCAQEAsWMBsB3Wpw2Z3xsn9ogZ/7gg 46 | rrzYU888bn3KOQgfA9nYcjuE0T6eD7R/pEdl4Lekvi9dp4GUK/ff3lgA/cb92u32 47 | d7Th3he/chJPH04BTKCTSxi2Kev1W8SLF+/VIFVIsQN9mm69whZicUM5gCj0Gl83 48 | 5H2TV+9dOQGUokUNL6f+uFqibpTieCG6c4b5KdfwB1iVswHhxFosWkEkmtBm+WPI 49 | TIbVFHi5IGas+Xp7Yx464ZnquO+tqLwMdDb/i7+eNXRP4jumJzJMnBzVmVOXE9aY 50 | joJQwow3Mq6tGCcAh9K8sVluwLcOVhpDd2wmQb29F76jb7lq1lMypdTK6wrSrw== 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /todo/tests/integration/certs/jwtRS256.key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxavDCaeQTrI8MZKKCejK 3 | Wgrp4ba6umj3npmBsHHwnOxSZFBRyS7kmA19Uxg0+HQac7/xiCdCJ5M4QnALsOTV 4 | BgGFWIUYZBFqgfmejJ4ri+mo4lNuDmvwgwVDlqFj9mJXM1Ew8az9JKDtCwyPqUbo 5 | /dpZFdk4MgkhFNJJA2A/Bgb2b5V2m79Af0CWhgkzaBGiWqNsWFucplOzuglmHXqa 6 | WWEPdDnTpyRFIAPXXmgIKyxeNvxBXJrp59Ykgt0pldTD7p4Mxd4rUOT2VaBUXCT1 7 | +stuqSX0P6Ec+vaBXL/MvBhAWqyUvOzCm+6xpcErCBN4ANBTcbsxGzTTZwJu+FYM 8 | IT/n+oOt8uGbNP3LxVWZTo8/o0ubW4s+ZdkHrz+UIwAALiudNRPN/Hq3r9o/o0yQ 9 | NUxbK6T9YQTMrHgM3ObDAhSWtM1RrQWxNHeBM4tKrHayw4o5uQ8WvHgQXRkfzbgX 10 | x3D+wrF9idT7DwaOfxDTZpPrO2C2RA7utBTT/vosw4Ny4QKzckcGecM1q9exKyma 11 | qpHE0PM7HO7tQtlkvtHKPnFK2F3CjSsdILuoR/L/IzFVrp1y70Nv5DixiCsAaJtw 12 | toXv9LsS8FdyiSzbAj1nyOQLMhUtc8dU+g0Mdy4kvO8yKPi/n7yK2EMmMWLZtYNd 13 | TKPPpOg0z3aX9E/rSPoSllcCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /todo/tests/integration/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.0' 2 | services: 3 | integration_tests: 4 | build: . 5 | image: integration_tests 6 | env_file: 7 | - todo-variables.env 8 | volumes: 9 | - ../../../protos/.gen/pb_python/:/home/tests/protos 10 | - ./certs/:/home/certs 11 | depends_on: 12 | - todo_service 13 | todo_service: 14 | build: ../../../todo 15 | image: todo_service 16 | env_file: 17 | - todo-variables.env 18 | expose: 19 | - 50050 20 | volumes: 21 | - ../../../protos/.gen/pb_python/:/home/app/todo/protos 22 | - ./certs/:/home/certs 23 | depends_on: 24 | - todo_db 25 | todo_db: 26 | image: mariadb 27 | env_file: 28 | - todo-variables.env 29 | logging: 30 | driver: "none" 31 | -------------------------------------------------------------------------------- /todo/tests/integration/requirements.txt: -------------------------------------------------------------------------------- 1 | protobuf 2 | grpcio 3 | grift 4 | PyJWT 5 | cryptography 6 | SQLAlchemy 7 | mysql-connector-python 8 | -------------------------------------------------------------------------------- /todo/tests/integration/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | TESTS_DIR=$(dirname $(readlink -f $0)) 5 | cd $TESTS_DIR 6 | 7 | docker-compose up --build 8 | docker-compose rm -f 9 | -------------------------------------------------------------------------------- /todo/tests/integration/test.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import os 4 | import socket 5 | import sys 6 | import time 7 | import unittest 8 | 9 | import grift 10 | import grpc 11 | import jwt 12 | import schematics.types as sch_types 13 | import sqlalchemy 14 | 15 | import protos.todo.todo_pb2 as todo_pb2 16 | import protos.todo.todo_pb2_grpc as todo_pb2_grpc 17 | 18 | logger = logging.getLogger(__name__) 19 | 20 | class TestConfig(grift.BaseConfig): 21 | def __init__(self, *args, **kwargs): 22 | super().__init__(*args, **kwargs) 23 | 24 | with open(self.CERT_FOLDER + "/" + self.JWT_PRIVATE_KEY_FILE, "rb") as fh: 25 | self.JWT_PRIVATE_KEY = fh.read() 26 | with open(self.CERT_FOLDER + "/" + self.JWT_PUBLIC_KEY_FILE, "rb") as fh: 27 | self.JWT_PUBLIC_KEY = fh.read() 28 | 29 | SERVICE_NAME = grift.ConfigProperty( 30 | property_type=sch_types.StringType(), 31 | exclude_from_varz=True) 32 | SERVICE_PORT = grift.ConfigProperty( 33 | property_type=sch_types.IntType(), 34 | exclude_from_varz=True) 35 | MYSQL_HOST = grift.ConfigProperty( 36 | property_type=sch_types.StringType(), 37 | exclude_from_varz=True) 38 | MYSQL_PORT = grift.ConfigProperty( 39 | property_type=sch_types.IntType(), 40 | exclude_from_varz=True) 41 | MYSQL_ROOT_PASSWORD = grift.ConfigProperty( 42 | property_type=sch_types.StringType(), 43 | exclude_from_varz=True) 44 | MYSQL_DATABASE = grift.ConfigProperty( 45 | property_type=sch_types.StringType(), 46 | exclude_from_varz=True) 47 | CERT_FOLDER = grift.ConfigProperty( 48 | property_type=sch_types.StringType(), 49 | exclude_from_varz=True) 50 | JWT_PRIVATE_KEY_FILE = grift.ConfigProperty( 51 | property_type=sch_types.StringType(), 52 | exclude_from_varz=True) 53 | JWT_PUBLIC_KEY_FILE = grift.ConfigProperty( 54 | property_type=sch_types.StringType(), 55 | exclude_from_varz=True) 56 | 57 | 58 | loaders = [grift.EnvLoader()] 59 | settings = TestConfig(loaders) 60 | 61 | 62 | def get_token_expiration_time(): 63 | return datetime.datetime.utcnow() \ 64 | + datetime.timedelta(hours=10) 65 | 66 | def generate_token(user_id, expiration_time): 67 | return jwt.encode( 68 | {"sub": user_id, "exp": expiration_time}, 69 | settings.JWT_PRIVATE_KEY, 70 | algorithm="RS256") 71 | 72 | class TestToDoService(unittest.TestCase): 73 | @classmethod 74 | def setUpClass(cls): 75 | log_fmt = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 76 | logging.basicConfig(level=logging.DEBUG, format=log_fmt) 77 | 78 | @classmethod 79 | def tearDownClass(cls): 80 | pass 81 | 82 | @property 83 | def test_metadata(self): 84 | test_user_id = "12345678123456781234567812345678" 85 | test_token_expiration_time = get_token_expiration_time() 86 | return [("user-token", generate_token(test_user_id, test_token_expiration_time))] 87 | 88 | def setUp(self): 89 | self.channel = grpc.insecure_channel("{service}:{port}" 90 | .format(service=settings.SERVICE_NAME, port=settings.SERVICE_PORT)) 91 | grpc.channel_ready_future(self.channel).result() 92 | self.stub = todo_pb2_grpc.ToDoStub(self.channel) 93 | 94 | def tearDown(self): 95 | self.channel.close() 96 | self._drop_all_service_tables() 97 | 98 | def _drop_all_service_tables(self): 99 | db_ip = socket.gethostbyname(settings.MYSQL_HOST) 100 | db_url = "mysql+mysqlconnector://{user}:{password}@{host}:{port}/{dbname}".format( 101 | user="root", 102 | password=settings.MYSQL_ROOT_PASSWORD, 103 | host=db_ip, 104 | port=settings.MYSQL_PORT, 105 | dbname=settings.MYSQL_DATABASE) 106 | engine = sqlalchemy.create_engine(db_url) 107 | meta_data = sqlalchemy.MetaData() 108 | meta_data.reflect(bind=engine) 109 | for table in reversed(meta_data.sorted_tables): 110 | engine.execute(table.delete()) 111 | 112 | def test_create_get_update_list_delete_todos(self): 113 | test_todo_entry_1 = todo_pb2.ToDoEntry( 114 | title="Title 1", 115 | text="Text 1" 116 | ) 117 | test_todo_entry_2 = todo_pb2.ToDoEntry( 118 | title="Title 2", 119 | text="Text 2" 120 | ) 121 | 122 | # Create two todos 123 | response = self.stub.CreateToDo( 124 | todo_pb2.CreateToDoReq(item=test_todo_entry_1), 125 | metadata=self.test_metadata) 126 | test_todo_entry_1.id = response.id 127 | response = self.stub.CreateToDo( 128 | todo_pb2.CreateToDoReq(item=test_todo_entry_2), 129 | metadata=self.test_metadata) 130 | test_todo_entry_2.id = response.id 131 | 132 | # Modify first todo 133 | test_todo_entry_1.title = "New Title 1" 134 | test_todo_entry_1.text = "New Text 1" 135 | self.stub.UpdateToDo( 136 | todo_pb2.UpdateToDoReq(item=test_todo_entry_1), 137 | metadata=self.test_metadata) 138 | 139 | # Try modify nonexisting todo (should response with error) 140 | with self.assertRaises(grpc.RpcError) as cm: 141 | self.stub.UpdateToDo( 142 | todo_pb2.UpdateToDoReq(item=todo_pb2.ToDoEntry( 143 | id=10000, 144 | title="Fake todo", 145 | text="Fake todo" 146 | )), 147 | metadata=self.test_metadata) 148 | self.assertEqual(cm.exception.code(), grpc.StatusCode.NOT_FOUND) 149 | 150 | # List todos (should be two todos) 151 | response = self.stub.ListToDo( 152 | todo_pb2.ListToDoReq(limit=10), 153 | metadata=self.test_metadata) 154 | self.assertEqual(len(response.items), 2) 155 | self.assertEqual(response.items[0], test_todo_entry_1) 156 | self.assertEqual(response.items[1], test_todo_entry_2) 157 | 158 | # Get second todo 159 | response = self.stub.GetToDo( 160 | todo_pb2.GetToDoReq(id=test_todo_entry_2.id), 161 | metadata=self.test_metadata) 162 | self.assertEqual(response.item, test_todo_entry_2) 163 | 164 | # Delete first todo 165 | self.stub.DeleteToDo( 166 | request=todo_pb2.DeleteToDoReq(id=test_todo_entry_1.id), 167 | metadata=self.test_metadata) 168 | 169 | # List todos (should be one todo) 170 | response = self.stub.ListToDo( 171 | todo_pb2.ListToDoReq(limit=10), 172 | metadata=self.test_metadata) 173 | self.assertEqual(len(response.items), 1) 174 | self.assertEqual(response.items[0], test_todo_entry_2) 175 | 176 | # Try to get deleted todo (should response with error) 177 | with self.assertRaises(grpc.RpcError) as cm: 178 | self.stub.GetToDo( 179 | todo_pb2.GetToDoReq(id=test_todo_entry_1.id), 180 | metadata=self.test_metadata) 181 | self.assertEqual(cm.exception.code(), grpc.StatusCode.NOT_FOUND) 182 | 183 | # Delete second todo 184 | self.stub.DeleteToDo( 185 | request=todo_pb2.DeleteToDoReq(id=test_todo_entry_2.id), 186 | metadata=self.test_metadata) 187 | 188 | # Try to delete nonexisting todo (should response with error) 189 | with self.assertRaises(grpc.RpcError) as cm: 190 | self.stub.DeleteToDo( 191 | request=todo_pb2.DeleteToDoReq(id=test_todo_entry_2.id), 192 | metadata=self.test_metadata) 193 | self.assertEqual(cm.exception.code(), grpc.StatusCode.NOT_FOUND) 194 | 195 | # List todos (should be zero todo) 196 | response = self.stub.ListToDo( 197 | todo_pb2.ListToDoReq(limit=10), 198 | metadata=self.test_metadata) 199 | self.assertEqual(len(response.items), 0) 200 | -------------------------------------------------------------------------------- /todo/tests/integration/todo-variables.env: -------------------------------------------------------------------------------- 1 | SERVICE_NAME=todo_service 2 | SERVICE_PORT=50050 3 | MYSQL_HOST=todo_db 4 | MYSQL_PORT=3306 5 | MYSQL_USER=todo 6 | MYSQL_PASSWORD=todo 7 | MYSQL_DATABASE=todo 8 | MYSQL_ROOT_PASSWORD=root 9 | CERT_FOLDER=/home/certs 10 | JWT_PRIVATE_KEY_FILE=jwtRS256.key 11 | JWT_PUBLIC_KEY_FILE=jwtRS256.key.pub 12 | -------------------------------------------------------------------------------- /todo/todo/app.py: -------------------------------------------------------------------------------- 1 | from concurrent import futures 2 | import logging 3 | import os 4 | import time 5 | 6 | import grpc 7 | import mysql.connector 8 | 9 | import protos.todo.todo_pb2_grpc as todo_pb2_grpc 10 | from todo_servicer import ToDoServicer 11 | from settings import settings 12 | import models 13 | import session 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | def database_not_ready_yet(error, checking_interval_seconds): 18 | print( 19 | "Database is not ready yet. \ 20 | Retrying after {} second(s). \ 21 | Returned database error: {}." 22 | .format(checking_interval_seconds, 23 | repr(error))) 24 | time.sleep(checking_interval_seconds) 25 | 26 | def wait_for_db_ready(host, port, db, user, password, checking_interval_seconds): 27 | logger.info("Waiting for database.") 28 | database_ready = False 29 | while not database_ready: 30 | db_connection = None 31 | try: 32 | db_connection = mysql.connector.connect( 33 | host=host, 34 | port=port, 35 | db=db, 36 | user=user, 37 | password=password, 38 | connect_timeout=5) 39 | db_connection.ping() 40 | logger.info("Database ping successful.") 41 | database_ready = True 42 | logger.info("Database is ready.") 43 | except mysql.connector.InterfaceError as err: 44 | database_not_ready_yet(err, checking_interval_seconds) 45 | except Exception as err: 46 | database_not_ready_yet(err, checking_interval_seconds) 47 | else: 48 | db_connection.close() 49 | 50 | class App(): 51 | __ONE_DAY_IN_SECONDS = 60 * 60 * 24 52 | 53 | def __enter__(self): 54 | log_fmt = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 55 | logging.basicConfig(level=logging.INFO, format=log_fmt) 56 | 57 | logger.info("{} is starting".format(settings.SERVICE_NAME)) 58 | 59 | wait_for_db_ready( 60 | settings.MYSQL_HOST, 61 | int(settings.MYSQL_PORT), 62 | settings.MYSQL_DATABASE, 63 | settings.MYSQL_USER, 64 | settings.MYSQL_PASSWORD, 65 | 1) 66 | 67 | session.DbSession.init_db_session() 68 | models.Base.metadata.create_all(session.DbSession.get_engine()) 69 | 70 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=4)) 71 | todo_pb2_grpc.add_ToDoServicer_to_server(ToDoServicer(), server) 72 | server.add_insecure_port("{service}:{port}" 73 | .format(service=settings.SERVICE_NAME, port=settings.SERVICE_PORT)) 74 | server.start() 75 | 76 | try: 77 | while True: 78 | time.sleep(App.__ONE_DAY_IN_SECONDS) 79 | except KeyboardInterrupt: 80 | server.stop(0) 81 | 82 | def __exit__(self, exc_type, exc_value, traceback): 83 | logger.info("{} is going down".format(settings.SERVICE_NAME)) 84 | -------------------------------------------------------------------------------- /todo/todo/main.py: -------------------------------------------------------------------------------- 1 | import app 2 | 3 | if __name__ == "__main__": 4 | with app.App() as app: 5 | app.run() 6 | -------------------------------------------------------------------------------- /todo/todo/models.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | import jwt 4 | 5 | from sqlalchemy import Column, String, Integer, DateTime 6 | from sqlalchemy_utils import UUIDType 7 | from sqlalchemy.ext.declarative import declarative_base 8 | from sqlalchemy.ext.hybrid import hybrid_property 9 | 10 | Base = declarative_base() 11 | 12 | class ToDoEntry(Base): 13 | __tablename__ = "ToDos" 14 | id = Column("id", Integer, primary_key=True, autoincrement=True) 15 | __user_id = Column("user_id", UUIDType(binary=False), nullable=False) 16 | title = Column(String(200), nullable=False) 17 | text = Column(String(2000), nullable=False) 18 | created_at = Column(DateTime, nullable=False, default=datetime.datetime.utcnow) 19 | updated_at = Column(DateTime) 20 | 21 | @hybrid_property 22 | def user_id(self): 23 | return self.__user_id 24 | 25 | @user_id.setter 26 | def user_id(self, user_id): 27 | self.__user_id = user_id 28 | -------------------------------------------------------------------------------- /todo/todo/session.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import socket 3 | 4 | from sqlalchemy import create_engine 5 | from sqlalchemy.orm import scoped_session 6 | from sqlalchemy.orm import sessionmaker 7 | from sqlalchemy_utils import database_exists, create_database 8 | 9 | from settings import settings 10 | 11 | class DbSession(): 12 | __engine = None 13 | __session = None 14 | 15 | @staticmethod 16 | def init_db_session(): 17 | db_ip = socket.gethostbyname(settings.MYSQL_HOST) 18 | db_url = "mysql+mysqlconnector://{user}:{password}@{host}:{port}/{dbname}".format( 19 | user=settings.MYSQL_USER, 20 | password=settings.MYSQL_PASSWORD, 21 | host=db_ip, 22 | port=settings.MYSQL_PORT, 23 | dbname=settings.MYSQL_DATABASE) 24 | DbSession.__engine = create_engine(db_url) 25 | if not database_exists(DbSession.__engine.url): 26 | create_database(DbSession.__engine.url) 27 | session_factory = sessionmaker(bind=DbSession.__engine) 28 | DbSession.__session = scoped_session(session_factory) 29 | 30 | @staticmethod 31 | @contextmanager 32 | def session_scope(): 33 | session = DbSession.get_session() 34 | try: 35 | yield session 36 | session.commit() 37 | except: 38 | session.rollback() 39 | raise 40 | finally: 41 | session.close() 42 | 43 | @staticmethod 44 | def get_engine(): 45 | return DbSession.__engine 46 | 47 | @staticmethod 48 | def get_session(): 49 | return DbSession.__session() 50 | -------------------------------------------------------------------------------- /todo/todo/settings.py: -------------------------------------------------------------------------------- 1 | import grift 2 | import schematics.types as sch_types 3 | 4 | class AppConfig(grift.BaseConfig): 5 | SERVICE_NAME = grift.ConfigProperty( 6 | property_type=sch_types.StringType(), 7 | exclude_from_varz=True) 8 | SERVICE_PORT = grift.ConfigProperty( 9 | property_type=sch_types.IntType(), 10 | exclude_from_varz=True) 11 | MYSQL_HOST = grift.ConfigProperty( 12 | property_type=sch_types.StringType(), 13 | exclude_from_varz=True) 14 | MYSQL_PORT = grift.ConfigProperty( 15 | property_type=sch_types.IntType(), 16 | exclude_from_varz=True) 17 | MYSQL_USER = grift.ConfigProperty( 18 | property_type=sch_types.StringType(), 19 | exclude_from_varz=True) 20 | MYSQL_PASSWORD = grift.ConfigProperty( 21 | property_type=sch_types.StringType(), 22 | exclude_from_varz=True) 23 | MYSQL_DATABASE = grift.ConfigProperty( 24 | property_type=sch_types.StringType(), 25 | exclude_from_varz=True) 26 | 27 | 28 | loaders = [grift.EnvLoader()] 29 | settings = AppConfig(loaders) 30 | -------------------------------------------------------------------------------- /todo/todo/todo_servicer.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import uuid 4 | 5 | import grpc 6 | import jwt 7 | from google.protobuf.timestamp_pb2 import Timestamp 8 | import sqlalchemy.orm.exc 9 | 10 | import models 11 | import protos.todo.todo_pb2 as todo_pb2 12 | import protos.todo.todo_pb2_grpc as todo_pb2_grpc 13 | from session import DbSession 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | class ToDoEntryMapper(): 18 | @staticmethod 19 | def to_protobuf(todo): 20 | updated_at = None 21 | if todo.updated_at is not None: 22 | updated_at = Timestamp().FromDatetime(todo.updated_at) 23 | 24 | return todo_pb2.ToDoEntry( 25 | id = todo.id, 26 | title = todo.title, 27 | text = todo.text, 28 | created_at = Timestamp().FromDatetime(todo.created_at), 29 | updated_at = updated_at) 30 | 31 | 32 | class ToDoServicer(todo_pb2_grpc.ToDoServicer): 33 | def CreateToDo(self, request, context): 34 | response = todo_pb2.CreateToDoRsp() 35 | user_id = self.__get_user_id_from_token(context) 36 | todo_item = request.item 37 | 38 | try: 39 | with DbSession.session_scope() as session: 40 | new_todo_item = models.ToDoEntry( 41 | user_id=user_id, 42 | title=todo_item.title, 43 | text=todo_item.text) 44 | session.add(new_todo_item) 45 | session.commit() 46 | response.id = new_todo_item.id 47 | except sqlalchemy.exc.SQLAlchemyError as err: 48 | logger.error("SQLAlchemyError {}".format(str(err))) 49 | context.set_code(grpc.StatusCode.UNKNOWN) 50 | 51 | return response 52 | 53 | def GetToDo(self, request, context): 54 | response = todo_pb2.GetToDoRsp() 55 | user_id = self.__get_user_id_from_token(context) 56 | todo_id = request.id 57 | 58 | try: 59 | with DbSession.session_scope() as session: 60 | todo_item = session.query(models.ToDoEntry) \ 61 | .filter_by(id=todo_id, user_id=user_id).one() 62 | response.item.CopyFrom(ToDoEntryMapper.to_protobuf(todo_item)) 63 | except sqlalchemy.orm.exc.NoResultFound as err: 64 | logger.error("NoResultFound {}".format(str(err))) 65 | context.set_code(grpc.StatusCode.NOT_FOUND) 66 | context.set_details("ToDoEntry not found") 67 | except sqlalchemy.exc.SQLAlchemyError as err: 68 | logger.error("SQLAlchemyError {}".format(str(err))) 69 | context.set_code(grpc.StatusCode.UNKNOWN) 70 | 71 | return response 72 | 73 | def DeleteToDo(self, request, context): 74 | user_id = self.__get_user_id_from_token(context) 75 | todo_id = request.id 76 | 77 | try: 78 | with DbSession.session_scope() as session: 79 | number = session.query(models.ToDoEntry) \ 80 | .filter_by(id=todo_id, user_id=user_id) \ 81 | .delete(synchronize_session="fetch") 82 | session.commit() 83 | if number == 0: 84 | context.set_code(grpc.StatusCode.NOT_FOUND) 85 | context.set_details("ToDoEntry not found") 86 | except sqlalchemy.exc.SQLAlchemyError as err: 87 | logger.error("SQLAlchemyError {}".format(str(err))) 88 | context.set_code(grpc.StatusCode.UNKNOWN) 89 | 90 | return todo_pb2.DeleteToDoRsp() 91 | 92 | def UpdateToDo(self, request, context): 93 | user_id = self.__get_user_id_from_token(context) 94 | todo_item = request.item 95 | todo_id = todo_item.id 96 | 97 | try: 98 | with DbSession.session_scope() as session: 99 | number = session.query(models.ToDoEntry) \ 100 | .filter_by(id=todo_id, user_id=user_id) \ 101 | .update( 102 | {models.ToDoEntry.title: todo_item.title, 103 | models.ToDoEntry.text: todo_item.text, 104 | models.ToDoEntry.updated_at: datetime.datetime.utcnow()}, 105 | synchronize_session="fetch") 106 | if number == 0: 107 | context.set_code(grpc.StatusCode.NOT_FOUND) 108 | context.set_details("ToDoEntry not found") 109 | except sqlalchemy.exc.SQLAlchemyError as err: 110 | logger.error("SQLAlchemyError {}".format(str(err))) 111 | context.set_code(grpc.StatusCode.UNKNOWN) 112 | 113 | return todo_pb2.UpdateToDoRsp() 114 | 115 | def ListToDo(self, request, context): 116 | response = todo_pb2.ListToDoRsp() 117 | user_id = self.__get_user_id_from_token(context) 118 | limit = request.limit 119 | 120 | try: 121 | with DbSession.session_scope() as session: 122 | todo_items_db = session.query(models.ToDoEntry) \ 123 | .filter_by(user_id=user_id) \ 124 | .limit(limit) 125 | for todo_item_db in todo_items_db: 126 | response.items.append(ToDoEntryMapper.to_protobuf(todo_item_db)) 127 | except sqlalchemy.exc.SQLAlchemyError as err: 128 | logger.error("SQLAlchemyError {}".format(str(err))) 129 | context.set_code(grpc.StatusCode.UNKNOWN) 130 | 131 | return response 132 | 133 | def __get_user_id_from_token(self, context): 134 | metadata = dict(context.invocation_metadata()) 135 | decoded_payload = jwt.decode(metadata["user-token"], verify=False) 136 | return decoded_payload["sub"] 137 | -------------------------------------------------------------------------------- /user/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:disco 2 | 3 | RUN apt-get update \ 4 | && apt-get install --no-install-recommends -y \ 5 | git \ 6 | build-essential \ 7 | python3 \ 8 | python3-pip \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | WORKDIR /home/app 12 | 13 | COPY requirements.txt . 14 | 15 | RUN pip3 install --upgrade pip \ 16 | && pip3 install --upgrade setuptools \ 17 | && pip3 install -r requirements.txt 18 | 19 | COPY . . 20 | 21 | ENTRYPOINT ["python3", "user/main.py"] 22 | -------------------------------------------------------------------------------- /user/requirements.txt: -------------------------------------------------------------------------------- 1 | protobuf 2 | grpcio 3 | mysql-connector-python 4 | grift 5 | PyJWT 6 | SQLAlchemy 7 | SQLAlchemy-Utils 8 | validate_email 9 | cryptography 10 | -------------------------------------------------------------------------------- /user/tests/integration/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:disco 2 | 3 | RUN apt-get update \ 4 | && apt-get install --no-install-recommends -y \ 5 | git \ 6 | build-essential \ 7 | python3 \ 8 | python3-pip \ 9 | && rm -rf /var/lib/apt/lists/* 10 | 11 | WORKDIR /home/tests 12 | 13 | COPY requirements.txt . 14 | 15 | RUN pip3 install --upgrade pip \ 16 | && pip3 install --upgrade setuptools \ 17 | && pip3 install -r requirements.txt 18 | 19 | COPY . . 20 | 21 | ENTRYPOINT ["python3", "-m", "unittest", "-v"] 22 | -------------------------------------------------------------------------------- /user/tests/integration/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/r-sitko/todo-list-app-microservices/3c48dea159be3be1d92c5a0509bb44765a2b7456/user/tests/integration/__init__.py -------------------------------------------------------------------------------- /user/tests/integration/certs/jwtRS256.key: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | 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 38 | XalTlxsMaIFnxtgUeMfft8Z1JmjIwEJEx7Nq+YeBFBalAe5B1/W9rqt3VMSx+tk5 39 | DIg241XD9siRjQwpio96UeAA0ykFpCF3ihyJUIOmrdXceZAl09u7zVfwTbJO8s/x 40 | yrUelwKCAQEArJHJ+VQV8q3u4qmWdZBsrMf/7ExwFVZhSvpHwA1UI1zbZiLLdhDn 41 | 1A9Skr/gdEs09yZI/+dxhiEingwOHQzNc2XI2dpaA860kBrMixCvFU8O9lzEUOi0 42 | jm0pG916Jpc1WLkar94WztUYkfnxThIdFb6E9avxC+u/Etu4MPHTtIbkHDrxwJGN 43 | f3v3dDqzX9dZw2UuQX4akf+qPUeqsMkcK41t/8T4InslDgF7qHaT5tfIm88gXNCf 44 | svZhW++5sxFPgO3aFgEMgAn/YHNS+2TGO7G681xiK2Q6YJGg2VynCX4EKakh2yU2 45 | mUPeGOp47AVQZitVD3t7VNvm1CwpqfJ8oQKCAQEAsWMBsB3Wpw2Z3xsn9ogZ/7gg 46 | rrzYU888bn3KOQgfA9nYcjuE0T6eD7R/pEdl4Lekvi9dp4GUK/ff3lgA/cb92u32 47 | d7Th3he/chJPH04BTKCTSxi2Kev1W8SLF+/VIFVIsQN9mm69whZicUM5gCj0Gl83 48 | 5H2TV+9dOQGUokUNL6f+uFqibpTieCG6c4b5KdfwB1iVswHhxFosWkEkmtBm+WPI 49 | TIbVFHi5IGas+Xp7Yx464ZnquO+tqLwMdDb/i7+eNXRP4jumJzJMnBzVmVOXE9aY 50 | joJQwow3Mq6tGCcAh9K8sVluwLcOVhpDd2wmQb29F76jb7lq1lMypdTK6wrSrw== 51 | -----END RSA PRIVATE KEY----- 52 | -------------------------------------------------------------------------------- /user/tests/integration/certs/jwtRS256.key.pub: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAxavDCaeQTrI8MZKKCejK 3 | Wgrp4ba6umj3npmBsHHwnOxSZFBRyS7kmA19Uxg0+HQac7/xiCdCJ5M4QnALsOTV 4 | BgGFWIUYZBFqgfmejJ4ri+mo4lNuDmvwgwVDlqFj9mJXM1Ew8az9JKDtCwyPqUbo 5 | /dpZFdk4MgkhFNJJA2A/Bgb2b5V2m79Af0CWhgkzaBGiWqNsWFucplOzuglmHXqa 6 | WWEPdDnTpyRFIAPXXmgIKyxeNvxBXJrp59Ykgt0pldTD7p4Mxd4rUOT2VaBUXCT1 7 | +stuqSX0P6Ec+vaBXL/MvBhAWqyUvOzCm+6xpcErCBN4ANBTcbsxGzTTZwJu+FYM 8 | IT/n+oOt8uGbNP3LxVWZTo8/o0ubW4s+ZdkHrz+UIwAALiudNRPN/Hq3r9o/o0yQ 9 | NUxbK6T9YQTMrHgM3ObDAhSWtM1RrQWxNHeBM4tKrHayw4o5uQ8WvHgQXRkfzbgX 10 | x3D+wrF9idT7DwaOfxDTZpPrO2C2RA7utBTT/vosw4Ny4QKzckcGecM1q9exKyma 11 | qpHE0PM7HO7tQtlkvtHKPnFK2F3CjSsdILuoR/L/IzFVrp1y70Nv5DixiCsAaJtw 12 | toXv9LsS8FdyiSzbAj1nyOQLMhUtc8dU+g0Mdy4kvO8yKPi/n7yK2EMmMWLZtYNd 13 | TKPPpOg0z3aX9E/rSPoSllcCAwEAAQ== 14 | -----END PUBLIC KEY----- 15 | -------------------------------------------------------------------------------- /user/tests/integration/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.0' 2 | services: 3 | integration_tests: 4 | build: . 5 | image: integration_tests 6 | env_file: 7 | - user-variables.env 8 | volumes: 9 | - ../../../protos/.gen/pb_python/:/home/tests/protos 10 | - ./certs/:/home/certs 11 | depends_on: 12 | - user_service 13 | user_service: 14 | build: ../../../user 15 | image: user_service 16 | env_file: 17 | - user-variables.env 18 | expose: 19 | - 50050 20 | volumes: 21 | - ../../../protos/.gen/pb_python/:/home/app/user/protos 22 | - ./certs/:/home/certs 23 | depends_on: 24 | - user_db 25 | user_db: 26 | image: mariadb 27 | env_file: 28 | - user-variables.env 29 | logging: 30 | driver: "none" 31 | -------------------------------------------------------------------------------- /user/tests/integration/requirements.txt: -------------------------------------------------------------------------------- 1 | protobuf 2 | grpcio 3 | PyJWT 4 | grift 5 | cryptography 6 | SQLAlchemy 7 | mysql-connector-python 8 | -------------------------------------------------------------------------------- /user/tests/integration/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -ex 3 | 4 | TESTS_DIR=$(dirname $(readlink -f $0)) 5 | cd $TESTS_DIR 6 | 7 | docker-compose up --build 8 | docker-compose rm -f 9 | -------------------------------------------------------------------------------- /user/tests/integration/test.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import socket 4 | import sys 5 | import time 6 | import unittest 7 | 8 | import grift 9 | import grpc 10 | import jwt 11 | import schematics.types as sch_types 12 | import sqlalchemy 13 | 14 | import protos.user.user_pb2 as user_pb2 15 | import protos.user.user_pb2_grpc as user_pb2_grpc 16 | 17 | logger = logging.getLogger(__name__) 18 | 19 | class TestConfig(grift.BaseConfig): 20 | def __init__(self, *args, **kwargs): 21 | super().__init__(*args, **kwargs) 22 | 23 | with open(self.CERT_FOLDER + "/" + self.JWT_PUBLIC_KEY_FILE, "rb") as fh: 24 | self.JWT_PUBLIC_KEY = fh.read() 25 | 26 | SERVICE_NAME = grift.ConfigProperty( 27 | property_type=sch_types.StringType(), 28 | exclude_from_varz=True) 29 | SERVICE_PORT = grift.ConfigProperty( 30 | property_type=sch_types.IntType(), 31 | exclude_from_varz=True) 32 | MYSQL_HOST = grift.ConfigProperty( 33 | property_type=sch_types.StringType(), 34 | exclude_from_varz=True) 35 | MYSQL_PORT = grift.ConfigProperty( 36 | property_type=sch_types.IntType(), 37 | exclude_from_varz=True) 38 | MYSQL_ROOT_PASSWORD = grift.ConfigProperty( 39 | property_type=sch_types.StringType(), 40 | exclude_from_varz=True) 41 | MYSQL_DATABASE = grift.ConfigProperty( 42 | property_type=sch_types.StringType(), 43 | exclude_from_varz=True) 44 | CERT_FOLDER = grift.ConfigProperty( 45 | property_type=sch_types.StringType(), 46 | exclude_from_varz=True) 47 | JWT_PUBLIC_KEY_FILE = grift.ConfigProperty( 48 | property_type=sch_types.StringType(), 49 | exclude_from_varz=True) 50 | 51 | 52 | loaders = [grift.EnvLoader()] 53 | settings = TestConfig(loaders) 54 | 55 | class TestUserService(unittest.TestCase): 56 | @classmethod 57 | def setUpClass(cls): 58 | log_fmt = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 59 | logging.basicConfig(level=logging.DEBUG, format=log_fmt) 60 | 61 | @classmethod 62 | def tearDownClass(cls): 63 | pass 64 | 65 | def setUp(self): 66 | self.channel = grpc.insecure_channel("{service}:{port}" 67 | .format(service=settings.SERVICE_NAME, port=settings.SERVICE_PORT)) 68 | grpc.channel_ready_future(self.channel).result() 69 | self.stub = user_pb2_grpc.UserStub(self.channel) 70 | 71 | def tearDown(self): 72 | self.channel.close() 73 | self._drop_all_service_tables() 74 | 75 | def _drop_all_service_tables(self): 76 | db_ip = socket.gethostbyname(settings.MYSQL_HOST) 77 | db_url = "mysql+mysqlconnector://{user}:{password}@{host}:{port}/{dbname}".format( 78 | user="root", 79 | password=settings.MYSQL_ROOT_PASSWORD, 80 | host=db_ip, 81 | port=settings.MYSQL_PORT, 82 | dbname=settings.MYSQL_DATABASE) 83 | engine = sqlalchemy.create_engine(db_url) 84 | meta_data = sqlalchemy.MetaData() 85 | meta_data.reflect(bind=engine) 86 | for table in reversed(meta_data.sorted_tables): 87 | engine.execute(table.delete()) 88 | 89 | def test_register_user_and_login(self): 90 | response = self.stub.Register(user_pb2.RegisterReq( 91 | username="me", password="pass", email="example@example.com")) 92 | response = self.stub.Login(user_pb2.LoginReq( 93 | username="me", password="pass")) 94 | decoded_payload = jwt.decode( 95 | response.jwt_token, 96 | settings.JWT_PUBLIC_KEY, 97 | algorithms=["RS256"]) 98 | assert "sub" in decoded_payload 99 | assert "exp" in decoded_payload 100 | 101 | def test_login_not_registered_user(self): 102 | with self.assertRaises(grpc.RpcError) as cm: 103 | self.stub.Login(user_pb2.LoginReq(username="you", password="pass")) 104 | self.assertEqual(cm.exception.code(), grpc.StatusCode.UNAUTHENTICATED) 105 | 106 | def test_register_user_wrong_email(self): 107 | with self.assertRaises(grpc.RpcError) as cm: 108 | self.stub.Register(user_pb2.RegisterReq( 109 | username="John", password="pass", email="wrong_email")) 110 | self.assertEqual(cm.exception.code(), grpc.StatusCode.INVALID_ARGUMENT) 111 | -------------------------------------------------------------------------------- /user/tests/integration/user-variables.env: -------------------------------------------------------------------------------- 1 | SERVICE_NAME=user_service 2 | SERVICE_PORT=50050 3 | MYSQL_HOST=user_db 4 | MYSQL_PORT=3306 5 | MYSQL_USER=user 6 | MYSQL_PASSWORD=user 7 | MYSQL_DATABASE=user 8 | MYSQL_ROOT_PASSWORD=root 9 | CERT_FOLDER=/home/certs 10 | JWT_PRIVATE_KEY_FILE=jwtRS256.key 11 | JWT_PUBLIC_KEY_FILE=jwtRS256.key.pub 12 | -------------------------------------------------------------------------------- /user/user/app.py: -------------------------------------------------------------------------------- 1 | from concurrent import futures 2 | import logging 3 | import os 4 | import time 5 | 6 | import grpc 7 | import mysql.connector 8 | 9 | import protos.user.user_pb2_grpc as user_pb2_grpc 10 | from user_servicer import UserServicer 11 | from settings import settings 12 | import models 13 | import session 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | def database_not_ready_yet(error, checking_interval_seconds): 18 | print( 19 | "Database is not ready yet. \ 20 | Retrying after {} second(s). \ 21 | Returned database error: {}." 22 | .format(checking_interval_seconds, 23 | repr(error))) 24 | time.sleep(checking_interval_seconds) 25 | 26 | def wait_for_db_ready(host, port, db, user, password, checking_interval_seconds): 27 | logger.info("Waiting for database.") 28 | database_ready = False 29 | while not database_ready: 30 | db_connection = None 31 | try: 32 | db_connection = mysql.connector.connect( 33 | host=host, 34 | port=port, 35 | db=db, 36 | user=user, 37 | password=password, 38 | connect_timeout=5) 39 | db_connection.ping() 40 | logger.info("Database ping successful.") 41 | database_ready = True 42 | logger.info("Database is ready.") 43 | except mysql.connector.InterfaceError as err: 44 | database_not_ready_yet(err, checking_interval_seconds) 45 | except Exception as err: 46 | database_not_ready_yet(err, checking_interval_seconds) 47 | else: 48 | db_connection.close() 49 | 50 | class App(): 51 | __ONE_DAY_IN_SECONDS = 60 * 60 * 24 52 | 53 | def __enter__(self): 54 | log_fmt = "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 55 | logging.basicConfig(level=logging.INFO, format=log_fmt) 56 | 57 | logger.info("{} is starting".format(settings.SERVICE_NAME)) 58 | 59 | wait_for_db_ready( 60 | settings.MYSQL_HOST, 61 | int(settings.MYSQL_PORT), 62 | settings.MYSQL_DATABASE, 63 | settings.MYSQL_USER, 64 | settings.MYSQL_PASSWORD, 65 | 1) 66 | 67 | session.DbSession.init_db_session() 68 | models.Base.metadata.create_all(session.DbSession.get_engine()) 69 | 70 | server = grpc.server(futures.ThreadPoolExecutor(max_workers=4)) 71 | user_pb2_grpc.add_UserServicer_to_server(UserServicer(), server) 72 | server.add_insecure_port("{service}:{port}" 73 | .format(service=settings.SERVICE_NAME, port=settings.SERVICE_PORT)) 74 | server.start() 75 | 76 | try: 77 | while True: 78 | time.sleep(App.__ONE_DAY_IN_SECONDS) 79 | except KeyboardInterrupt: 80 | server.stop(0) 81 | 82 | def __exit__(self, exc_type, exc_value, traceback): 83 | logger.info("{} is going down".format(settings.SERVICE_NAME)) 84 | -------------------------------------------------------------------------------- /user/user/main.py: -------------------------------------------------------------------------------- 1 | import app 2 | 3 | if __name__ == "__main__": 4 | with app.App() as app: 5 | app.run() 6 | -------------------------------------------------------------------------------- /user/user/models.py: -------------------------------------------------------------------------------- 1 | import uuid 2 | 3 | from sqlalchemy import Column, String 4 | from sqlalchemy_utils import UUIDType 5 | from sqlalchemy.ext.declarative import declarative_base 6 | from sqlalchemy.ext.hybrid import hybrid_property 7 | from sqlalchemy.orm import validates 8 | from validate_email import validate_email 9 | 10 | Base = declarative_base() 11 | 12 | class User(Base): 13 | __tablename__ = "Users" 14 | __id = Column("id", UUIDType(binary=False), primary_key=True, 15 | unique=True, nullable=False, default=uuid.uuid4) 16 | username = Column(String(100), nullable=False) 17 | password = Column(String(128), nullable=False) 18 | __salt = Column("salt", UUIDType(binary=False), unique=True, nullable=False) 19 | email = Column(String(100), unique=True, nullable=False) 20 | 21 | @validates("email") 22 | def validate_email(self, key, address): 23 | if not validate_email(address): 24 | raise EmailUserValidationError() 25 | return address 26 | 27 | @hybrid_property 28 | def id(self): 29 | return str(self.__id) 30 | 31 | @property 32 | def salt(self): 33 | return str(self.__salt) 34 | 35 | @salt.setter 36 | def salt(self, salt): 37 | self.__salt = salt 38 | 39 | 40 | class UserValidationError(Exception): 41 | def __init__(self, message): 42 | super().__init__(message) 43 | 44 | class EmailUserValidationError(UserValidationError): 45 | def __init__(self): 46 | super().__init__("Email is incorrect.") 47 | -------------------------------------------------------------------------------- /user/user/session.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import socket 3 | 4 | from sqlalchemy import create_engine 5 | from sqlalchemy.orm import scoped_session 6 | from sqlalchemy.orm import sessionmaker 7 | from sqlalchemy_utils import database_exists, create_database 8 | 9 | from settings import settings 10 | 11 | class DbSession(): 12 | __engine = None 13 | __session = None 14 | 15 | @staticmethod 16 | def init_db_session(): 17 | db_ip = socket.gethostbyname(settings.MYSQL_HOST) 18 | db_url = "mysql+mysqlconnector://{user}:{password}@{host}:{port}/{dbname}".format( 19 | user=settings.MYSQL_USER, 20 | password=settings.MYSQL_PASSWORD, 21 | host=db_ip, 22 | port=settings.MYSQL_PORT, 23 | dbname=settings.MYSQL_DATABASE) 24 | DbSession.__engine = create_engine(db_url) 25 | if not database_exists(DbSession.__engine.url): 26 | create_database(DbSession.__engine.url) 27 | session_factory = sessionmaker(bind=DbSession.__engine) 28 | DbSession.__session = scoped_session(session_factory) 29 | 30 | @staticmethod 31 | @contextmanager 32 | def session_scope(): 33 | session = DbSession.get_session() 34 | try: 35 | yield session 36 | session.commit() 37 | except: 38 | session.rollback() 39 | raise 40 | finally: 41 | session.close() 42 | 43 | @staticmethod 44 | def get_engine(): 45 | return DbSession.__engine 46 | 47 | @staticmethod 48 | def get_session(): 49 | return DbSession.__session() 50 | -------------------------------------------------------------------------------- /user/user/settings.py: -------------------------------------------------------------------------------- 1 | import grift 2 | import schematics.types as sch_types 3 | 4 | class AppConfig(grift.BaseConfig): 5 | def __init__(self, *args, **kwargs): 6 | super().__init__(*args, **kwargs) 7 | 8 | with open(self.CERT_FOLDER + "/" + self.JWT_PRIVATE_KEY_FILE, "rb") as fh: 9 | self.JWT_PRIVATE_KEY = fh.read() 10 | with open(self.CERT_FOLDER + "/" + self.JWT_PUBLIC_KEY_FILE, "rb") as fh: 11 | self.JWT_PUBLIC_KEY = fh.read() 12 | 13 | SERVICE_NAME = grift.ConfigProperty( 14 | property_type=sch_types.StringType(), 15 | exclude_from_varz=True) 16 | SERVICE_PORT = grift.ConfigProperty( 17 | property_type=sch_types.IntType(), 18 | exclude_from_varz=True) 19 | MYSQL_HOST = grift.ConfigProperty( 20 | property_type=sch_types.StringType(), 21 | exclude_from_varz=True) 22 | MYSQL_PORT = grift.ConfigProperty( 23 | property_type=sch_types.IntType(), 24 | exclude_from_varz=True) 25 | MYSQL_USER = grift.ConfigProperty( 26 | property_type=sch_types.StringType(), 27 | exclude_from_varz=True) 28 | MYSQL_PASSWORD = grift.ConfigProperty( 29 | property_type=sch_types.StringType(), 30 | exclude_from_varz=True) 31 | MYSQL_DATABASE = grift.ConfigProperty( 32 | property_type=sch_types.StringType(), 33 | exclude_from_varz=True) 34 | CERT_FOLDER = grift.ConfigProperty( 35 | property_type=sch_types.StringType(), 36 | exclude_from_varz=True) 37 | JWT_PRIVATE_KEY_FILE = grift.ConfigProperty( 38 | property_type=sch_types.StringType(), 39 | exclude_from_varz=True) 40 | JWT_PUBLIC_KEY_FILE = grift.ConfigProperty( 41 | property_type=sch_types.StringType(), 42 | exclude_from_varz=True) 43 | 44 | 45 | loaders = [grift.EnvLoader()] 46 | settings = AppConfig(loaders) 47 | -------------------------------------------------------------------------------- /user/user/user_servicer.py: -------------------------------------------------------------------------------- 1 | import binascii 2 | import datetime 3 | import hashlib 4 | import logging 5 | import uuid 6 | 7 | import grpc 8 | import jwt 9 | import sqlalchemy.orm.exc 10 | import sqlalchemy.exc 11 | 12 | import models 13 | import protos.user.user_pb2 as user_pb2 14 | import protos.user.user_pb2_grpc as user_pb2_grpc 15 | from settings import settings 16 | from session import DbSession 17 | 18 | _TOKEN_EXPIRATION_HOURS = 24 19 | 20 | logger = logging.getLogger(__name__) 21 | 22 | def hash_password(password, salt, hash_name="sha512", iterations=100000): 23 | dk = hashlib.pbkdf2_hmac( 24 | hash_name, 25 | password.encode("utf-8"), 26 | salt.encode("utf-8"), 27 | iterations) 28 | return binascii.hexlify(dk).decode("ascii") 29 | 30 | def get_expiration_time(): 31 | return datetime.datetime.utcnow() \ 32 | + datetime.timedelta(hours=_TOKEN_EXPIRATION_HOURS) 33 | 34 | def generate_token(user, expiration_time): 35 | return jwt.encode( 36 | {"sub": user.id, "exp": expiration_time}, 37 | settings.JWT_PRIVATE_KEY, 38 | algorithm="RS256") 39 | 40 | class UserServicer(user_pb2_grpc.UserServicer): 41 | def Register(self, request, context): 42 | username = request.username 43 | password = request.password 44 | email = request.email 45 | salt = uuid.uuid4() 46 | hashed_password = hash_password(password, str(salt)) 47 | 48 | try: 49 | with DbSession.session_scope() as session: 50 | new_user = models.User( 51 | username=username, 52 | password=hashed_password, 53 | email=email, 54 | salt=salt) 55 | session.add(new_user) 56 | session.commit() 57 | except models.EmailUserValidationError as err: 58 | logger.error("EmailUserValidationError {}".format(str(err))) 59 | context.set_code(grpc.StatusCode.INVALID_ARGUMENT) 60 | context.set_details(str(err)) 61 | except sqlalchemy.exc.SQLAlchemyError as err: 62 | logger.error("SQLAlchemyError {}".format(str(err))) 63 | context.set_code(grpc.StatusCode.UNKNOWN) 64 | 65 | return user_pb2.RegisterRsp() 66 | 67 | def Login(self, request, context): 68 | response = user_pb2.LoginRsp() 69 | username = request.username 70 | password = request.password 71 | 72 | try: 73 | with DbSession.session_scope() as session: 74 | user = session.query(models.User).filter_by(username=username).one() 75 | hashed_password = hash_password(password, user.salt) 76 | if user.password == hashed_password: 77 | expiration_time = get_expiration_time() 78 | token = generate_token(user, expiration_time) 79 | response.jwt_token = token 80 | response.expiration = str(expiration_time) 81 | except sqlalchemy.orm.exc.NoResultFound as err: 82 | logger.error("NoResultFound {}".format(str(err))) 83 | context.set_code(grpc.StatusCode.UNAUTHENTICATED) 84 | context.set_details("Invalid username or password") 85 | except sqlalchemy.exc.SQLAlchemyError as err: 86 | logger.error("SQLAlchemyError {}".format(str(err))) 87 | context.set_code(grpc.StatusCode.UNKNOWN) 88 | 89 | return response 90 | --------------------------------------------------------------------------------