├── products-service ├── src │ ├── models │ │ ├── mod.rs │ │ └── coffee.rs │ ├── authorization │ │ ├── mod.rs │ │ └── guard.rs │ ├── graphql │ │ ├── mod.rs │ │ ├── schema.rs │ │ ├── routes.rs │ │ └── query.rs │ ├── config.rs │ └── main.rs ├── .gitignore ├── .dockerignore ├── environments │ ├── testing.yaml │ └── development.yaml ├── .docker │ └── products-service.dockerfile ├── docker-compose.yml └── Cargo.toml ├── acpm-service ├── src │ ├── middlewares │ │ ├── mod.rs │ │ └── basic_auth.rs │ ├── routes │ │ ├── mod.rs │ │ └── authorization.rs │ ├── migrations │ │ ├── 02_clients.sql │ │ └── 01_casbin_rules.sql │ ├── models │ │ ├── mod.rs │ │ ├── auth_client.rs │ │ ├── permission_query.rs │ │ └── rbac.rs │ ├── config.rs │ └── main.rs ├── environments │ ├── testing.yaml │ ├── development.yaml │ └── access_model │ │ └── rbac_model.conf ├── .dockerignore ├── .gitignore ├── .docker │ └── acpm-service.dockerfile ├── Cargo.toml └── docker-compose.yml ├── identity-service ├── src │ ├── models │ │ ├── mod.rs │ │ ├── role.rs │ │ └── user.rs │ ├── authentication │ │ ├── mod.rs │ │ ├── hasher.rs │ │ └── routes.rs │ ├── graphql │ │ ├── mod.rs │ │ ├── schema.rs │ │ ├── routes.rs │ │ └── query.rs │ ├── errors │ │ └── custom_error.rs │ ├── config.rs │ └── main.rs ├── environments │ └── development.yaml ├── .gitignore ├── .docker │ └── identity-service.dockerfile └── Cargo.toml ├── benchmark ├── coffee.query.graphql ├── hasura-graphql-bench.dockerfile ├── createCoffee.mutation.graphql ├── docker-compose.yml └── config.query.yaml ├── gateway ├── .prettierrc.js ├── .docker │ └── gateway.dockerfile ├── src │ ├── config │ │ └── index.ts │ └── index.ts ├── .vscode │ └── launch.json ├── .eslintrc.js ├── package.json ├── .gitignore └── tsconfig.json ├── .vscode └── workspace.code-workspace ├── docker-compose.yml ├── LICENSE ├── .drone.yml ├── docker-compose.prod.yml ├── README.md └── workspace.json /products-service/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod coffee; 2 | 3 | pub use coffee::*; 4 | -------------------------------------------------------------------------------- /products-service/src/authorization/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod guard; 2 | 3 | pub use guard::*; 4 | -------------------------------------------------------------------------------- /acpm-service/src/middlewares/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod basic_auth; 2 | 3 | pub use basic_auth::*; 4 | -------------------------------------------------------------------------------- /identity-service/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod role; 2 | pub mod user; 3 | 4 | pub use role::*; 5 | pub use user::*; 6 | -------------------------------------------------------------------------------- /benchmark/coffee.query.graphql: -------------------------------------------------------------------------------- 1 | query Coffees { 2 | coffees { 3 | id, 4 | name, 5 | description 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /identity-service/src/authentication/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod hasher; 2 | pub mod routes; 3 | 4 | pub use hasher::*; 5 | pub use routes::*; 6 | -------------------------------------------------------------------------------- /acpm-service/src/routes/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod authorization; 2 | // pub mod authentication; 3 | 4 | pub use authorization::*; 5 | // pub use authentication::*; 6 | -------------------------------------------------------------------------------- /gateway/.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | trailingComma: "all", 4 | singleQuote: false, 5 | printWidth: 120, 6 | tabWidth: 2 7 | }; 8 | -------------------------------------------------------------------------------- /benchmark/hasura-graphql-bench.dockerfile: -------------------------------------------------------------------------------- 1 | FROM hasura/graphql-bench:2.0.1-beta 2 | 3 | COPY ./queries.graphql /graphql-bench/ws/queries.graphql 4 | 5 | EXPOSE 8050 6 | -------------------------------------------------------------------------------- /identity-service/src/graphql/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod query; 2 | pub mod routes; 3 | pub mod schema; 4 | 5 | pub use query::*; 6 | pub use routes::*; 7 | pub use schema::*; 8 | -------------------------------------------------------------------------------- /products-service/src/graphql/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod query; 2 | pub mod routes; 3 | pub mod schema; 4 | 5 | pub use query::*; 6 | pub use routes::*; 7 | pub use schema::*; 8 | -------------------------------------------------------------------------------- /acpm-service/src/migrations/02_clients.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "clients" ( 2 | "id" SERIAL PRIMARY KEY, 3 | "client_id" VARCHAR NOT NULL, 4 | "client_secret" VARCHAR NOT NULL 5 | ); 6 | -------------------------------------------------------------------------------- /acpm-service/src/models/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth_client; 2 | pub mod permission_query; 3 | pub mod rbac; 4 | 5 | pub use auth_client::*; 6 | pub use permission_query::*; 7 | pub use rbac::*; 8 | -------------------------------------------------------------------------------- /products-service/src/graphql/schema.rs: -------------------------------------------------------------------------------- 1 | use super::{Mutation, Query, Subscription}; 2 | use async_graphql::{Schema}; 3 | 4 | pub type ProductsServiceSchema = Schema; 5 | -------------------------------------------------------------------------------- /identity-service/src/graphql/schema.rs: -------------------------------------------------------------------------------- 1 | use super::Query; 2 | use async_graphql::{EmptyMutation, EmptySubscription, Schema}; 3 | 4 | pub type IdentityServiceSchema = Schema; 5 | -------------------------------------------------------------------------------- /acpm-service/src/models/auth_client.rs: -------------------------------------------------------------------------------- 1 | use sqlx::FromRow; 2 | 3 | #[derive(FromRow, Debug)] 4 | pub struct AuthClient { 5 | pub id: i32, 6 | pub client_id: String, 7 | pub client_secret: String, 8 | } 9 | -------------------------------------------------------------------------------- /gateway/.docker/gateway.dockerfile: -------------------------------------------------------------------------------- 1 | FROM node 2 | 3 | WORKDIR /gateway 4 | 5 | COPY --chown=node:node . . 6 | 7 | RUN npm install && npm cache clean --force --loglevel=error 8 | 9 | CMD [ "npm", "start"] 10 | -------------------------------------------------------------------------------- /benchmark/createCoffee.mutation.graphql: -------------------------------------------------------------------------------- 1 | mutation createCoffee($name: String!){ 2 | createCoffee(input: {name: $name, price: 1.0, imageUrl: "https://google.com"}){ 3 | id 4 | name 5 | price 6 | imageUrl 7 | description 8 | } 9 | } -------------------------------------------------------------------------------- /identity-service/src/models/role.rs: -------------------------------------------------------------------------------- 1 | use paperclip::actix::Apiv2Schema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Serialize, Deserialize, Apiv2Schema)] 5 | pub enum Role { 6 | Guest, 7 | Customer, 8 | Admin, 9 | } 10 | -------------------------------------------------------------------------------- /.vscode/workspace.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "..\\acpm-service" 5 | }, 6 | { 7 | "path": "..\\products-service" 8 | }, 9 | { 10 | "path": "..\\gateway" 11 | }, 12 | { 13 | "path": "..\\identity-service" 14 | } 15 | ], 16 | "settings": {} 17 | } -------------------------------------------------------------------------------- /benchmark/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | 3 | services: 4 | benchmark: 5 | build: 6 | context: . 7 | dockerfile: hasura-graphql-bench.dockerfile 8 | ports: 9 | - 8050:8050 10 | command: ["graphql-bench", "query"] 11 | environment: 12 | DEBUG: "*" 13 | -------------------------------------------------------------------------------- /acpm-service/src/models/permission_query.rs: -------------------------------------------------------------------------------- 1 | use paperclip::actix::Apiv2Schema; 2 | use serde::{Deserialize, Serialize}; 3 | 4 | #[derive(Debug, Serialize, Deserialize, Apiv2Schema)] 5 | pub struct PermissionQuery { 6 | pub subject: String, 7 | pub action: String, 8 | pub object: String, 9 | } 10 | -------------------------------------------------------------------------------- /acpm-service/environments/testing.yaml: -------------------------------------------------------------------------------- 1 | # Enable additional debugging logging 2 | debug: true 3 | # Database 4 | database: 5 | url: postgres://casbin:casbin@127.0.0.1:5432/casbin 6 | poolsize: 8 7 | # Casbin access model 8 | accessmodelpath: ./environments/access_model/rbac_model.conf 9 | # HTTP Server 10 | server: 11 | port: 3999 12 | -------------------------------------------------------------------------------- /acpm-service/environments/development.yaml: -------------------------------------------------------------------------------- 1 | # Enable additional debugging logging 2 | debug: true 3 | # Database 4 | database: 5 | url: postgres://casbin:casbin@127.0.0.1:5432/casbin 6 | poolsize: 8 7 | # Casbin access model 8 | accessmodelpath: ./environments/access_model/rbac_model.conf 9 | # HTTP Server 10 | server: 11 | port: 3999 12 | -------------------------------------------------------------------------------- /acpm-service/environments/access_model/rbac_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act || r.sub == "root" # root is the super user 15 | -------------------------------------------------------------------------------- /identity-service/environments/development.yaml: -------------------------------------------------------------------------------- 1 | # Enable additional debugging logging 2 | debug: true 3 | # Redis 4 | redis: 5 | url: redis://127.0.0.1:6379 6 | # Session 7 | session: 8 | secret: N7WoK3mG7lSb0CpK8UhAabUZNi27n5ub 9 | # Mongo Database 10 | database: 11 | url: mongodb://root:example@127.0.0.1:27017/admin 12 | # HTTP Server 13 | server: 14 | port: 4001 15 | -------------------------------------------------------------------------------- /acpm-service/.dockerignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | -------------------------------------------------------------------------------- /acpm-service/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | -------------------------------------------------------------------------------- /identity-service/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | -------------------------------------------------------------------------------- /products-service/.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | -------------------------------------------------------------------------------- /products-service/.dockerignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 7 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 8 | Cargo.lock 9 | 10 | # These are backup files generated by rustfmt 11 | **/*.rs.bk 12 | -------------------------------------------------------------------------------- /acpm-service/src/migrations/01_casbin_rules.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "casbin_rules" ( 2 | id SERIAL PRIMARY KEY, 3 | ptype VARCHAR NOT NULL, 4 | v0 VARCHAR NOT NULL, 5 | v1 VARCHAR NOT NULL, 6 | v2 VARCHAR NOT NULL, 7 | v3 VARCHAR NOT NULL, 8 | v4 VARCHAR NOT NULL, 9 | v5 VARCHAR NOT NULL, 10 | CONSTRAINT unique_key_sqlx_adapter UNIQUE(ptype, v0, v1, v2, v3, v4, v5) 11 | ); 12 | -------------------------------------------------------------------------------- /gateway/src/config/index.ts: -------------------------------------------------------------------------------- 1 | const config = { 2 | gateway: { 3 | // logger: { 4 | // prettyPrint: TODO: Disabled if prod 5 | // }, 6 | port: process.env.PORT || 4000, 7 | }, 8 | services: [ 9 | { 10 | name: "identity-service", 11 | url: process.env.IDENTITY_SERVICE_URL || "http://127.0.0.1:4001/graphql", 12 | }, 13 | { 14 | name: "products-service", 15 | url: process.env.PRODUCTS_SERVICE_URL || "http://127.0.0.1:4002/graphql", 16 | }, 17 | ], 18 | } 19 | 20 | export default config 21 | -------------------------------------------------------------------------------- /acpm-service/.docker/acpm-service.dockerfile: -------------------------------------------------------------------------------- 1 | # FROM rust as builder 2 | FROM rustlang/rust:nightly as builder 3 | 4 | WORKDIR /usr/src/acpm-service 5 | 6 | COPY . . 7 | 8 | RUN cargo install --path . 9 | 10 | FROM debian:stable-slim as production 11 | 12 | RUN apt-get update && apt-get install -y libssl-dev && rm -rf /var/lib/apt/lists/* 13 | 14 | WORKDIR /acpm-service 15 | 16 | COPY --from=builder /usr/local/cargo/bin/acpm-service /acpm-service 17 | 18 | COPY ./environments/ /acpm-service/environments/ 19 | 20 | CMD ["/acpm-service/acpm-service"] 21 | -------------------------------------------------------------------------------- /products-service/environments/testing.yaml: -------------------------------------------------------------------------------- 1 | # Enable additional debugging logging 2 | debug: true 3 | # Redis 4 | redis: 5 | url: 127.0.0.1:6379 6 | # Session 7 | session: 8 | secret: N7WoK3mG7lSb0CpK8UhAabUZNi27n5ub 9 | # Mongo Database 10 | database: 11 | url: mongodb://root:example@127.0.0.1:27017/admin 12 | # HTTP Server 13 | server: 14 | port: 4002 15 | # Authorization server 16 | authorization: 17 | url: http://127.0.0.1:3999/api/v1/authorization/is-authorized 18 | skip: true 19 | auth: 20 | username: "products-service" 21 | password: "password" 22 | -------------------------------------------------------------------------------- /identity-service/.docker/identity-service.dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustlang/rust:nightly as builder 2 | 3 | WORKDIR /usr/src/identity-service 4 | 5 | COPY . . 6 | 7 | RUN cargo install --path . 8 | 9 | FROM debian:stable-slim as production 10 | 11 | # RUN apt-get update && apt-get install -y extra-runtime-dependencies && rm -rf /var/lib/apt/lists/* 12 | 13 | WORKDIR /identity-service 14 | 15 | COPY --from=builder /usr/local/cargo/bin/identity-service /identity-service 16 | 17 | COPY ./environments/ /identity-service/environments/ 18 | 19 | CMD ["/identity-service/identity-service"] 20 | -------------------------------------------------------------------------------- /products-service/.docker/products-service.dockerfile: -------------------------------------------------------------------------------- 1 | FROM rustlang/rust:nightly as builder 2 | 3 | WORKDIR /usr/src/products-service 4 | 5 | COPY . . 6 | 7 | RUN cargo install --path . 8 | 9 | FROM debian:stable-slim as production 10 | 11 | # RUN apt-get update && apt-get install -y extra-runtime-dependencies && rm -rf /var/lib/apt/lists/* 12 | 13 | WORKDIR /products-service 14 | 15 | COPY --from=builder /usr/local/cargo/bin/products-service /products-service 16 | 17 | COPY ./environments/ /products-service/environments/ 18 | 19 | CMD ["/products-service/products-service"] 20 | -------------------------------------------------------------------------------- /products-service/environments/development.yaml: -------------------------------------------------------------------------------- 1 | # Enable additional debugging logging 2 | debug: true 3 | # Redis 4 | redis: 5 | url: 127.0.0.1:6379 6 | # Session 7 | session: 8 | secret: N7WoK3mG7lSb0CpK8UhAabUZNi27n5ub 9 | # Mongo Database 10 | database: 11 | url: mongodb://root:example@127.0.0.1:27017/admin 12 | # HTTP Server 13 | server: 14 | port: 4002 15 | # Authorization server 16 | authorization: 17 | url: https://acpm.graphql.simoneromano.eu/api/v1/authorization/is-authorized 18 | skip: false 19 | auth: 20 | username: "products-service" 21 | password: "password" 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | 3 | services: 4 | redis: 5 | image: redis:alpine 6 | ports: 7 | - 6379:6379 8 | 9 | mongo: 10 | image: mongo 11 | restart: always 12 | ports: 13 | - 27017:27017 14 | environment: 15 | MONGO_INITDB_ROOT_USERNAME: root 16 | MONGO_INITDB_ROOT_PASSWORD: example 17 | 18 | mongo-express: 19 | image: mongo-express 20 | restart: always 21 | ports: 22 | - 8081:8081 23 | environment: 24 | ME_CONFIG_MONGODB_ADMINUSERNAME: root 25 | ME_CONFIG_MONGODB_ADMINPASSWORD: example 26 | 27 | -------------------------------------------------------------------------------- /gateway/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch via nodemon", 9 | "type": "node", 10 | "request": "launch", 11 | "protocol": "inspector", 12 | "cwd": "${workspaceRoot}", 13 | "runtimeExecutable": "${workspaceRoot}/node_modules/.bin/ts-node-dev", 14 | "args": ["${workspaceRoot}/src/index.ts"], 15 | "restart": true 16 | } 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /identity-service/src/authentication/hasher.rs: -------------------------------------------------------------------------------- 1 | use argon2::{self, Config}; 2 | // use lazy_static::lazy_static; 3 | use rand::{RngCore, rngs::OsRng}; 4 | 5 | // lazy_static! { 6 | // // static ref GENERATOR: OsRng = OsRng::default(); 7 | // // static ref CONFIG: Config = Config::default(); 8 | // } 9 | 10 | pub fn hash_password(password: &str) -> String { 11 | let mut r = OsRng::default(); 12 | // Random bytes. 13 | let mut salt = vec![0u8; 32]; 14 | r.fill_bytes(&mut salt); 15 | 16 | let hash = argon2::hash_encoded(password.as_bytes(), &salt, &Config::default()).unwrap(); 17 | hash 18 | } 19 | 20 | pub fn verify_hash(hash: &str, expected: &str) -> bool { 21 | argon2::verify_encoded(hash, expected.as_bytes()).unwrap() 22 | } 23 | -------------------------------------------------------------------------------- /gateway/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: "@typescript-eslint/parser", // Specifies the ESLint parser 3 | parserOptions: { 4 | ecmaVersion: 2020, // Allows for the parsing of modern ECMAScript features 5 | sourceType: "module", // Allows for the use of imports 6 | }, 7 | extends: [ 8 | "plugin:@typescript-eslint/recommended", // Uses the recommended rules from the @typescript-eslint/eslint-plugin 9 | "prettier/@typescript-eslint", // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier 10 | "plugin:prettier/recommended", // Enables eslint-plugin-prettier and eslint-config-prettier. This will display prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array. 11 | ], 12 | rules: { 13 | // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs 14 | // e.g. "@typescript-eslint/explicit-function-return-type": "off", 15 | }, 16 | } 17 | -------------------------------------------------------------------------------- /acpm-service/src/models/rbac.rs: -------------------------------------------------------------------------------- 1 | use paperclip::actix::Apiv2Schema; 2 | use serde::{Deserialize, Serialize}; 3 | #[derive(Debug, Serialize, Deserialize, Apiv2Schema)] 4 | #[serde(rename_all = "camelCase")] 5 | pub struct AddRolesForUser { 6 | /// User's ID 7 | pub user_id: String, 8 | /// Roles 9 | pub roles: Vec, 10 | /// Domain of the role, ex. app-1, app-2 11 | pub domain: String, 12 | } 13 | 14 | #[derive(Debug, Serialize, Deserialize, Apiv2Schema)] 15 | #[serde(rename_all = "camelCase")] 16 | pub struct Permission { 17 | /// The action that the role is permitted to do, ex. read, create, update, delete 18 | pub action: String, 19 | /// The object that receives the action, ex. product, post, coffee, etc. 20 | pub object: String, 21 | } 22 | 23 | #[derive(Debug, Serialize, Deserialize, Apiv2Schema)] 24 | #[serde(rename_all = "camelCase")] 25 | pub struct AddPermissionToRole { 26 | /// Role name, ex. admin, customer, guest 27 | pub role: String, 28 | /// Permissions granted to the role 29 | pub permissions: Vec 30 | } 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Simone Romano 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 | -------------------------------------------------------------------------------- /identity-service/src/graphql/routes.rs: -------------------------------------------------------------------------------- 1 | use super::IdentityServiceSchema; 2 | use crate::models::User; 3 | use actix_session::Session; 4 | use actix_web::{web::Data, HttpResponse}; 5 | use async_graphql::http::{playground_source, GraphQLPlaygroundConfig}; 6 | use async_graphql_actix_web::{Request, Response}; 7 | use wither::{bson::oid::ObjectId, mongodb::Database}; 8 | 9 | pub async fn index( 10 | schema: Data, 11 | // req: HttpRequest, 12 | db: Data, 13 | gql_request: Request, 14 | session: Session, 15 | ) -> Response { 16 | let mut request = gql_request.into_inner(); 17 | if let Some(id) = session.get::("user_id").unwrap_or(None) { 18 | let user = User::find_by_id(&db, &id).await.unwrap(); 19 | request = request.data(user); 20 | } 21 | 22 | schema.execute(request).await.into() 23 | } 24 | 25 | pub async fn gql_playgound() -> HttpResponse { 26 | HttpResponse::Ok() 27 | .content_type("text/html; charset=utf-8") 28 | .body(playground_source( 29 | GraphQLPlaygroundConfig::new("/graphql").subscription_endpoint("/graphql"), 30 | )) 31 | } 32 | -------------------------------------------------------------------------------- /acpm-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "acpm-service" 3 | version = "0.1.0" 4 | authors = ["Simone Romano "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | # HTTP Server 11 | actix-web = { version = "3.3.2", default-features = false } 12 | # Basic auth 13 | actix-web-httpauth = "0.5.0" 14 | # Serialization/Deserialization 15 | serde = "1.0.123" 16 | # OpenAPI specification with Actix 17 | paperclip = { git = "https://github.com/wafflespeanut/paperclip.git", features = ["actix3-nightly"] } 18 | # Authorization enforcer policies with Adapter for SQLx 19 | sqlx-adapter = { git = "https://github.com/casbin-rs/sqlx-adapter", default-features = false, features = ["postgres", "runtime-actix-native-tls", "offline"] } 20 | # SQLx 21 | sqlx = { version = "0.4.2", default-features = false, features = [ "runtime-actix-native-tls", "macros", "postgres", "migrate" ] } 22 | # Config 23 | config = { version = "0.10.1", features = ["yaml"] } 24 | # Lazy evaluation 25 | lazy_static = "1.4.0" 26 | # Logging 27 | log = "0.4.14" 28 | pretty_env_logger = "0.4.0" 29 | -------------------------------------------------------------------------------- /gateway/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gateway", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "./src/index.ts", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "lint": "eslint './src/**/*.ts' --fix", 9 | "dev": "ts-node-dev --inspect --respawn --transpile-only src/index.ts", 10 | "start": "ts-node src/index.ts" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "@graphql-tools/load": "~6.2.5", 16 | "@graphql-tools/mock": "~7.0.0", 17 | "@graphql-tools/schema": "~7.1.2", 18 | "@graphql-tools/stitch": "~7.2.1", 19 | "@graphql-tools/url-loader": "~6.8.0", 20 | "fastify": "~3.11.0", 21 | "mercurius": "~6.11.0", 22 | "pino-pretty": "^4.4.0", 23 | "ts-node": "~9.1.1", 24 | "ws": "~7.4.2" 25 | }, 26 | "devDependencies": { 27 | "@types/node": "~14.14.22", 28 | "@types/ws": "~7.4.0", 29 | "@typescript-eslint/eslint-plugin": "~4.14.1", 30 | "eslint": "~7.19.0", 31 | "eslint-config-prettier": "~7.2.0", 32 | "eslint-plugin-prettier": "~3.3.1", 33 | "prettier": "~2.2.1", 34 | "ts-node-dev": "~1.1.1", 35 | "typescript": "^4.1.3" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /acpm-service/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | postgres: 5 | # build: 6 | # context: ./.docker 7 | # dockerfile: postgres.dockerfile 8 | image: postgres:alpine 9 | restart: unless-stopped 10 | environment: 11 | POSTGRES_USER: casbin 12 | POSTGRES_PASSWORD: casbin 13 | POSTGRES_DB: casbin 14 | ports: 15 | - 5432:5432 16 | volumes: 17 | - postgres_data:/var/lib/postgresql/data 18 | command: ["postgres", "-c", "log_statement=all"] 19 | 20 | adminer: 21 | image: adminer 22 | restart: unless-stopped 23 | ports: 24 | - 8080:8080 25 | 26 | app: 27 | build: 28 | context: . 29 | dockerfile: ./.docker/acpm-service.dockerfile 30 | restart: unless-stopped 31 | depends_on: 32 | - postgres 33 | environment: 34 | APP_DATABASE_URL: postgres://casbin:casbin@postgres:5432/casbin 35 | APP_DATABASE_POOLSIZE: 16 36 | APP_SERVER_PORT: 80 37 | APP_DEBUG: "true" 38 | APP_ACCESSMODELPATH: /acpm-service/environments/access_model/rbac_model.conf 39 | ports: 40 | - 3999:80 41 | 42 | 43 | volumes: 44 | postgres_data: 45 | -------------------------------------------------------------------------------- /identity-service/src/graphql/query.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{User, UserInfo}; 2 | use async_graphql::{Context, Object, Result, ID}; 3 | use wither::{bson::doc, bson::oid::ObjectId, mongodb::Database}; 4 | 5 | pub struct Query; 6 | 7 | #[Object(extends)] 8 | impl Query { 9 | /// Get current user info 10 | async fn me(&self, ctx: &Context<'_>) -> Result { 11 | // let user = ctx.data::(); 12 | if let Ok(user) = ctx.data::() { 13 | Ok(user.to_user_info()) 14 | } else { 15 | Err("Not logged in".into()) 16 | } 17 | } 18 | 19 | /// Get a user by its ID 20 | #[graphql(entity)] 21 | async fn find_user_info_by_id(&self, ctx: &Context<'_>, id: ID) -> Result { 22 | let oid_result = ObjectId::with_string(&id.to_string()); 23 | if let Ok(oid) = oid_result { 24 | let db: &Database = ctx.data()?; 25 | let maybe_user = User::find_by_id(db, &oid).await; 26 | if let Some(user) = maybe_user { 27 | Ok(user.to_user_info()) 28 | } else { 29 | Err("No user found".into()) 30 | } 31 | } else { 32 | Err("Invalid ID".into()) 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /products-service/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | 3 | services: 4 | redis: 5 | image: redis:alpine 6 | ports: 7 | - 6379:6379 8 | 9 | mongo: 10 | image: mongo 11 | restart: always 12 | ports: 13 | - 27017:27017 14 | environment: 15 | MONGO_INITDB_ROOT_USERNAME: root 16 | MONGO_INITDB_ROOT_PASSWORD: example 17 | 18 | mongo-express: 19 | image: mongo-express 20 | restart: always 21 | ports: 22 | - 8081:8081 23 | environment: 24 | ME_CONFIG_MONGODB_ADMINUSERNAME: root 25 | ME_CONFIG_MONGODB_ADMINPASSWORD: example 26 | depends_on: 27 | - mongo 28 | 29 | #products: 30 | # build: 31 | # context: . 32 | # dockerfile: ./.docker/products-service.dockerfile 33 | # restart: unless-stopped 34 | # environment: 35 | # APP_DEBUG: "true" 36 | # APP_REDIS_URL: redis:6379 37 | # APP_DATABASE_URL: mongodb://root:example@mongo:27017/admin 38 | # APP_SERVER_PORT: 80 39 | # APP_AUTHORIZATION_URL: http://acpm-service/api/v1/authorization/is-authorized 40 | # APP_AUTH_USERNAME: products-service 41 | # APP_AUTH_PASSWORD: password 42 | # ports: 43 | # - 4002:80 44 | # depends_on: 45 | # - redis 46 | # - mongo 47 | -------------------------------------------------------------------------------- /identity-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "identity-service" 3 | version = "0.1.0" 4 | authors = ["Simone Romano "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | # HTTP Server 11 | actix-web = "3.3.2" 12 | # Session adapter 13 | actix-session = "0.4.0" 14 | # Redis Session 15 | actix-redis = "0.9.1" 16 | # Serialization/Deserialization 17 | serde = "1.0.123" 18 | # Json specific Serialization/Deserialization 19 | serde_json = "1.0.61" 20 | # Argon password hashing 21 | # argonautica = "0.2.0" 22 | rust-argon2 = "0.8.3" 23 | # Cryptographically Secure Randomness 24 | rand = "0.8.3" 25 | # Evaluate some stuff only once 26 | # lazy_static = "1.4.0" 27 | # Mongo DB ODM (Should be merged soon) 28 | wither = { git = "https://github.com/simoneromano96/wither.git", branch = "master" } 29 | # OpenAPI specification with Actix 30 | paperclip = { version = "0.5.0", features = ["actix3-nightly", "actix-session"] } 31 | # GraphQL implementation 32 | async-graphql = "2.5.1" 33 | # GraphQL Actix Adapter 34 | async-graphql-actix-web = "2.5.1" 35 | config = "0.10.1" 36 | lazy_static = "1.4.0" 37 | # Logging 38 | pretty_env_logger = "0.4.0" 39 | log = "0.4.14" 40 | # Authorization enforcer policies 41 | # casbin = { version = "2.0.2", features = ["runtime-tokio"] } 42 | -------------------------------------------------------------------------------- /products-service/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "products-service" 3 | version = "0.1.0" 4 | authors = ["Simone Romano "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | # HTTP Server 11 | actix-web = "3.3.2" 12 | # WebSocket extensions 13 | # actix-web-actors = "3.0.0" 14 | # Session adapter 15 | actix-session = "0.4.0" 16 | # Redis Session 17 | actix-redis = "0.9.1" 18 | # Redis client (PUB SUB) 19 | # redis-async = "0.6.3" 20 | # Redis client 21 | redis = { version = "0.19.0", features = ["tokio-comp"] } 22 | # Serialization/Deserialization 23 | serde = "1.0.123" 24 | # JSON Serialization/Deserialization 25 | serde_json = "1.0.61" 26 | # Mongo DB ODM (Should be merged soon) 27 | wither = { git = "https://github.com/simoneromano96/wither.git" } 28 | # GraphQL implementation 29 | async-graphql = { version = "2.5.1", features = ["log", "tracing"] } 30 | # GraphQL Actix Adapter 31 | async-graphql-actix-web = "2.5.1" 32 | # Type-safe URLs 33 | url = { version = "2.2.0", features = ["serde"] } 34 | # Futures traits and methods 35 | futures = "0.3.12" 36 | # HTTP Client 37 | reqwest = { version = "0.10.0", default-features = false, features = ["rustls-tls"] } 38 | # Config 39 | config = { version = "0.10.1", features = ["yaml"] } 40 | # Evaluate config only once 41 | lazy_static = "1.4.0" 42 | # Loggers 43 | pretty_env_logger = "0.4.0" 44 | log = "0.4.14" 45 | # Base64 encode/decode 46 | base64 = "0.13.0" 47 | -------------------------------------------------------------------------------- /identity-service/src/errors/custom_error.rs: -------------------------------------------------------------------------------- 1 | 2 | #[api_v2_errors( 3 | code = 400, 4 | description = "Bad Request: Errors in the body", 5 | code = 401, 6 | description = "Unauthorized: Can't read session from cookie", 7 | code = 500 8 | )] 9 | #[derive(Debug, Serialize, Deserialize)] 10 | pub enum CustomError { 11 | BadRequest(String), 12 | } 13 | 14 | impl std::fmt::Display for CustomError { 15 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 16 | let mut s = String::new(); 17 | write!(&mut s, "{:?}", self) 18 | } 19 | } 20 | 21 | impl ResponseError for CustomError { 22 | fn status_code(&self) -> actix_web::http::StatusCode { 23 | match self { 24 | CustomError::BadRequest(_) => { 25 | actix_web::http::StatusCode::BAD_REQUEST 26 | } 27 | } 28 | // actix_web::http::StatusCode::INTERNAL_SERVER_ERROR 29 | } 30 | 31 | fn error_response(&self) -> HttpResponse { 32 | let mut resp = HttpResponse::new(self.status_code()); 33 | resp.headers_mut().insert( 34 | actix_web::http::header::CONTENT_TYPE, 35 | actix_web::http::HeaderValue::from_static("text/plain; charset=utf-8"), 36 | ); 37 | let mut buf = web::BytesMut::new(); 38 | 39 | match self { 40 | CustomError::BadRequest(message) => { 41 | write!(&mut buf, "{:?}", message).unwrap(); 42 | } 43 | } 44 | // let _ = write!(Writer(&mut buf), "{}", self); 45 | resp.set_body(actix_web::dev::Body::from(buf)) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /gateway/src/index.ts: -------------------------------------------------------------------------------- 1 | import fastify from "fastify" 2 | import mercurius from "mercurius" 3 | import { stitchSchemas } from "@graphql-tools/stitch" 4 | import { loadSchema } from "@graphql-tools/load" 5 | import { UrlLoader } from "@graphql-tools/url-loader" 6 | import WebSocket from "ws" 7 | import config from "./config" 8 | 9 | interface Service { 10 | name: string 11 | url: string 12 | } 13 | 14 | const loadSchemas = (services: Service[]) => 15 | services.map(({ url }) => 16 | loadSchema(url, { 17 | enableSubscriptions: true, 18 | loaders: [new UrlLoader()], 19 | webSocketImpl: WebSocket, 20 | }), 21 | ) 22 | 23 | // Run the server! 24 | const main = async () => { 25 | try { 26 | const app = fastify({ 27 | logger: { 28 | prettyPrint: true, 29 | }, 30 | }) 31 | 32 | /* 33 | const schema1 = await loadSchema("http://localhost:4002/graphql", { 34 | // load from endpoint 35 | loaders: [new UrlLoader()], 36 | enableSubscriptions: true, 37 | webSocketImpl: WebSocket, 38 | }) 39 | */ 40 | 41 | const subschemas = await Promise.all(loadSchemas(config.services)) 42 | 43 | console.log(subschemas) 44 | 45 | const gatewaySchema = stitchSchemas({ 46 | subschemas, 47 | }) 48 | 49 | console.log(gatewaySchema) 50 | 51 | app.register(mercurius, { 52 | schema: gatewaySchema, 53 | subscription: true, 54 | }) 55 | 56 | await app.listen(config.gateway.port, "0.0.0.0") 57 | 58 | // app.log.info(`server listening on ?`, app.server.address()) 59 | } catch (err) { 60 | // app.log.error(err) 61 | process.exit(1) 62 | } 63 | } 64 | 65 | main() 66 | -------------------------------------------------------------------------------- /acpm-service/src/middlewares/basic_auth.rs: -------------------------------------------------------------------------------- 1 | use crate::models::AuthClient; 2 | use actix_web::{dev::ServiceRequest, Error}; 3 | use actix_web_httpauth::extractors::{ 4 | basic::{BasicAuth, Config}, 5 | AuthenticationError, 6 | }; 7 | use log::info; 8 | use sqlx::{Pool, Postgres}; 9 | 10 | const CLIENT_QUERY: &str = r#" 11 | SELECT * 12 | FROM "clients" 13 | WHERE 14 | "client_id" = $1 AND 15 | "client_secret" = $2 16 | LIMIT 1 17 | "#; 18 | 19 | async fn validate_credentials(pool: &Pool, client_id: &str, client_secret: &str) -> bool { 20 | info!("Authenticating: {:?} - {:?}", client_id, client_secret); 21 | 22 | let client: Option = sqlx::query_as::<_, AuthClient>(CLIENT_QUERY) 23 | .bind(client_id) 24 | .bind(client_secret) 25 | .fetch_optional(pool) 26 | .await 27 | .unwrap(); 28 | 29 | info!("{:?}", client); 30 | 31 | if let Some(_) = client { 32 | true 33 | } else { 34 | false 35 | } 36 | } 37 | 38 | pub async fn basic_auth_validator( 39 | req: ServiceRequest, 40 | credentials: BasicAuth, 41 | ) -> Result { 42 | info!("Requested basic auth"); 43 | 44 | let config = req 45 | .app_data::() 46 | .map(|data| data.clone()) 47 | .unwrap_or_else(Default::default); 48 | 49 | let pool = req.app_data::>().expect("No pool found"); 50 | 51 | if validate_credentials( 52 | pool, 53 | credentials.user_id(), 54 | credentials.password().unwrap().trim(), 55 | ) 56 | .await 57 | { 58 | Ok(req) 59 | } else { 60 | Err(AuthenticationError::from(config).into()) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /products-service/src/graphql/routes.rs: -------------------------------------------------------------------------------- 1 | use super::ProductsServiceSchema; 2 | use actix_session::Session; 3 | use actix_web::{ 4 | web::{Data, Payload}, 5 | HttpRequest, HttpResponse, Result, 6 | }; 7 | use async_graphql::Schema; 8 | use async_graphql_actix_web::{Request, Response, WSSubscription}; 9 | // use wither::bson::oid::ObjectId; 10 | 11 | pub async fn index( 12 | schema: Data, 13 | // req: HttpRequest, 14 | gql_request: Request, 15 | session: Session, 16 | // http_client: Data, 17 | ) -> Response { 18 | let maybe_user: Option = session.get("user_id").unwrap_or(None); 19 | // let maybe_user_role: Option = session.get("user_role").unwrap_or(None); 20 | 21 | // println!("{:?}", maybe_user); 22 | 23 | let mut request = gql_request.into_inner(); 24 | if let Some(user_id) = maybe_user { 25 | // println!("Add User Info: id: {:?}", &user_id); 26 | request = request.data(user_id); 27 | } 28 | // if let Some(user_role) = maybe_user_role { 29 | // // println!("Add User Info: role: {:?}", &user_role); 30 | // request = request.data(user_role); 31 | // } 32 | 33 | schema.execute(request).await.into() 34 | } 35 | 36 | pub async fn index_ws( 37 | schema: Data, 38 | req: HttpRequest, 39 | payload: Payload, 40 | ) -> Result { 41 | WSSubscription::start(Schema::clone(&*schema), &req, payload) 42 | } 43 | 44 | /* 45 | pub async fn gql_playgound() -> HttpResponse { 46 | HttpResponse::Ok() 47 | .content_type("text/html; charset=utf-8") 48 | .body(playground_source( 49 | GraphQLPlaygroundConfig::new("/graphql").subscription_endpoint("/graphql"), 50 | )) 51 | } 52 | */ 53 | -------------------------------------------------------------------------------- /gateway/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | -------------------------------------------------------------------------------- /products-service/src/models/coffee.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{self, Enum, InputObject, Object, ID}; 2 | use serde::{Deserialize, Serialize}; 3 | use url::Url; 4 | use wither::bson::{doc, oid::ObjectId}; 5 | use wither::prelude::*; 6 | 7 | /// Define the Coffee Model 8 | #[derive(Clone, Debug, Model, Serialize, Deserialize)] 9 | #[model( 10 | collection_name = "coffees", 11 | index(keys = r#"doc!{"name": 1}"#, options = r#"doc!{"unique": true}"#) 12 | )] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct Coffee { 15 | /// The ID of the model. 16 | #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] 17 | pub id: Option, 18 | /// Coffee's name. 19 | pub name: String, 20 | /// Coffee's price. 21 | pub price: f64, 22 | /// Coffee's image. 23 | pub image_url: String, 24 | /// Coffee's description (optional). 25 | pub description: Option, 26 | } 27 | 28 | #[Object] 29 | impl Coffee { 30 | async fn id(&self) -> String { 31 | if let Some(id) = &self.id { 32 | id.clone().to_string() 33 | } else { 34 | String::from("") 35 | } 36 | } 37 | 38 | async fn name(&self) -> &str { 39 | &self.name 40 | } 41 | 42 | async fn price(&self) -> &f64 { 43 | &self.price 44 | } 45 | 46 | async fn image_url(&self) -> &str { 47 | &self.image_url 48 | } 49 | 50 | async fn description(&self) -> String { 51 | if let Some(description) = &self.description { 52 | description.clone() 53 | } else { 54 | String::from("") 55 | } 56 | } 57 | } 58 | 59 | #[derive(Clone, InputObject)] 60 | pub struct CreateCoffeeInput { 61 | pub name: String, 62 | pub price: f64, 63 | pub image_url: Url, 64 | pub description: Option, 65 | } 66 | 67 | #[derive(Clone, InputObject)] 68 | pub struct UpdateCoffeeInput { 69 | pub id: String, 70 | pub name: Option, 71 | pub price: Option, 72 | pub image_url: Option, 73 | pub description: Option, 74 | } 75 | 76 | #[derive(Debug, Enum, Eq, PartialEq, Copy, Clone, Deserialize, Serialize)] 77 | pub enum MutationType { 78 | Created, 79 | Updated, 80 | Deleted, 81 | } 82 | 83 | #[derive(Clone, Debug, Deserialize, Serialize)] 84 | pub struct CoffeeChanged { 85 | pub id: ID, 86 | pub mutation_type: MutationType, 87 | } 88 | 89 | #[Object] 90 | impl CoffeeChanged { 91 | async fn id(&self) -> &ID { 92 | &self.id 93 | } 94 | 95 | async fn mutation_type(&self) -> MutationType { 96 | self.mutation_type 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /acpm-service/src/config.rs: -------------------------------------------------------------------------------- 1 | use config::{Config, Environment, File}; 2 | use lazy_static::lazy_static; 3 | use serde::{Deserialize, Serialize}; 4 | use std::env; 5 | 6 | lazy_static! { 7 | pub static ref APP_CONFIG: Settings = Settings::init_config(); 8 | } 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct PoolConfig { 13 | pub size: u32, 14 | } 15 | 16 | #[derive(Debug, Serialize, Deserialize)] 17 | #[serde(rename_all = "camelCase")] 18 | pub struct PostgresConfig { 19 | pub url: String, 20 | pub poolsize: u32, 21 | } 22 | 23 | #[derive(Debug, Serialize, Deserialize)] 24 | #[serde(rename_all = "camelCase")] 25 | pub struct ServerConfig { 26 | pub port: u16, 27 | } 28 | 29 | #[derive(Debug, Serialize, Deserialize)] 30 | #[serde(rename_all = "camelCase")] 31 | pub struct Settings { 32 | pub debug: bool, 33 | pub accessmodelpath: String, 34 | pub database: PostgresConfig, 35 | pub server: ServerConfig, 36 | } 37 | 38 | impl Settings { 39 | fn init_config() -> Self { 40 | let mut s = Config::default(); 41 | let mut config_file_path = env::current_dir().expect("Cannot get current path"); 42 | 43 | // println!("{:?}", config_file_path); 44 | 45 | // Get current RUN_MODE, should be: development/production 46 | let current_env = env::var("RUN_MODE").unwrap_or(String::from("development")); 47 | 48 | config_file_path.push("environments"); 49 | config_file_path.push(format!("{}.yaml", current_env)); 50 | 51 | // println!("{:?}", config_file_path); 52 | 53 | // Add in the current environment file 54 | // Default to 'development' env 55 | s.merge(File::from(config_file_path).required(false)) 56 | .expect("Could not read file"); 57 | 58 | // Add in settings from the environment 59 | // DEBUG=1 sets debug key, DATABASE_URL sets database.url key 60 | s.merge(Environment::new().prefix("APP").separator("_")).expect("Cannot get env"); 61 | 62 | // println!("{:?}", s); 63 | 64 | // Deserialize configuration 65 | let r: Settings = s.try_into().expect("Configuration error"); 66 | 67 | 68 | // Enable all logging 69 | // if r.debug { 70 | // env::set_var("RUST_BACKTRACE", "1"); 71 | // env::set_var("RUST_LOG", "actix_web=info,actix_redis=info"); 72 | // } 73 | 74 | // println!("{:?}", r); 75 | 76 | // Should not be necessary 77 | // if let Ok(database_url) = env::var("DATABASE_URL") { 78 | // r.database.url = database_url; 79 | // } 80 | 81 | r 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /identity-service/src/models/user.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{Context, Object, ID}; 2 | use paperclip::actix::Apiv2Schema; 3 | use serde::{Deserialize, Serialize}; 4 | use wither::prelude::*; 5 | use wither::{ 6 | bson::{doc, oid::ObjectId}, 7 | mongodb::Database, 8 | }; 9 | 10 | use super::Role; 11 | 12 | /// User representation 13 | #[derive(Debug, Model, Serialize, Deserialize)] 14 | #[model(index(keys = r#"doc!{"username": 1}"#, options = r#"doc!{"unique": true}"#))] 15 | pub struct User { 16 | /// The ID of the model. 17 | #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] 18 | pub id: Option, 19 | /// The username. 20 | pub username: String, 21 | /// The hashed password. 22 | pub password: String, 23 | // User email 24 | // email: String, 25 | /// User role 26 | pub role: String, 27 | } 28 | 29 | impl User { 30 | pub fn new_user(username: &str, password: &str) -> Self { 31 | User { 32 | id: None, 33 | username: String::from(username), 34 | password: String::from(password), 35 | role: String::from("customer"), 36 | } 37 | } 38 | 39 | pub fn to_user_info(&self) -> UserInfo { 40 | UserInfo { 41 | id: self.id.clone(), 42 | username: self.username.clone(), 43 | } 44 | } 45 | 46 | pub async fn find_by_id(db: &Database, id: &ObjectId) -> Option { 47 | User::find_one(&db, doc! { "_id": id }, None).await.unwrap() 48 | } 49 | 50 | pub async fn find_by_username(db: &Database, username: &str) -> Option { 51 | User::find_one(&db, doc! { "username": username }, None) 52 | .await 53 | .unwrap() 54 | } 55 | } 56 | 57 | /// Available User info 58 | #[derive(Debug, Serialize, Deserialize, Apiv2Schema)] 59 | pub struct UserInfo { 60 | /// The ID of the user. 61 | #[serde(rename = "_id", skip_serializing_if = "Option::is_none")] 62 | pub id: Option, 63 | /// The username. 64 | pub username: String, 65 | } 66 | 67 | #[Object] 68 | impl UserInfo { 69 | async fn id(&self) -> String { 70 | if let Some(id) = &self.id { 71 | id.clone().to_string() 72 | } else { 73 | String::from("") 74 | } 75 | } 76 | 77 | async fn username(&self) -> &str { 78 | &self.username 79 | } 80 | } 81 | 82 | /// New User Input 83 | #[derive(Debug, Serialize, Deserialize, Apiv2Schema)] 84 | pub struct UserInput { 85 | /// The new user username, must be unique. 86 | pub username: String, 87 | /// The new user password. 88 | pub password: String, 89 | // User email 90 | // email: String, 91 | } 92 | -------------------------------------------------------------------------------- /identity-service/src/config.rs: -------------------------------------------------------------------------------- 1 | use config::{Config, Environment, File}; 2 | use lazy_static::lazy_static; 3 | use serde::{Deserialize, Serialize}; 4 | use std::{env, net::IpAddr}; 5 | 6 | lazy_static! { 7 | pub static ref APP_CONFIG: Settings = Settings::init_config(); 8 | } 9 | 10 | #[derive(Debug, Serialize, Deserialize)] 11 | #[serde(rename_all = "camelCase")] 12 | pub struct MongoConfig { 13 | pub url: String, 14 | } 15 | 16 | #[derive(Debug, Serialize, Deserialize)] 17 | #[serde(rename_all = "camelCase")] 18 | pub struct ServerConfig { 19 | pub port: u16, 20 | } 21 | 22 | #[derive(Debug, Serialize, Deserialize)] 23 | #[serde(rename_all = "camelCase")] 24 | pub struct RedisConfig { 25 | pub url: String, 26 | } 27 | 28 | #[derive(Debug, Serialize, Deserialize)] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct SessionConfig { 31 | pub secret: String, 32 | } 33 | 34 | #[derive(Debug, Serialize, Deserialize)] 35 | #[serde(rename_all = "camelCase")] 36 | pub struct Settings { 37 | pub debug: bool, 38 | pub database: MongoConfig, 39 | pub server: ServerConfig, 40 | pub redis: RedisConfig, 41 | pub session: SessionConfig, 42 | } 43 | 44 | impl Settings { 45 | fn init_config() -> Self { 46 | println!("CONFIG INIT"); 47 | let mut s = Config::default(); 48 | let mut config_file_path = env::current_dir().expect("Cannot get current path"); 49 | 50 | // println!("{:?}", config_file_path); 51 | 52 | // Get current RUN_MODE, should be: development/production 53 | let current_env = env::var("RUN_MODE").unwrap_or(String::from("development")); 54 | 55 | config_file_path.push("environments"); 56 | config_file_path.push(format!("{}.yaml", current_env)); 57 | 58 | // println!("{:?}", config_file_path); 59 | 60 | // Add in the current environment file 61 | // Default to 'development' env 62 | s.merge(File::from(config_file_path).required(true)) 63 | .expect("Could not read file"); 64 | 65 | // Add in settings from the environment 66 | // DEBUG=1 sets debug key, DATABASE_URL sets database.url key 67 | s.merge(Environment::new().prefix("APP").separator("_")) 68 | .expect("Cannot get env"); 69 | 70 | // Deserialize configuration 71 | let mut r: Settings = s.try_into().expect("Configuration error"); 72 | 73 | // Enable all logging 74 | // if r.debug { 75 | // env::set_var("RUST_BACKTRACE", "1"); 76 | // env::set_var("RUST_LOG", "actix_web=info,actix_redis=info"); 77 | // } 78 | 79 | // Should not be necessary 80 | // if let Ok(database_url) = env::var("DATABASE_URL") { 81 | // r.database.url = database_url; 82 | // } 83 | 84 | r 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /products-service/src/authorization/guard.rs: -------------------------------------------------------------------------------- 1 | use crate::config::APP_CONFIG; 2 | use async_graphql::{async_trait, guard::Guard}; 3 | use async_graphql::{Context, Result}; 4 | use async_trait::async_trait; 5 | use log::info; 6 | use reqwest::StatusCode; 7 | use serde::{Deserialize, Serialize}; 8 | 9 | #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] 10 | pub enum Permission { 11 | CreateCoffee, 12 | ReadCoffee, 13 | UpdateCoffee, 14 | DeleteCoffee, 15 | // Other(String), 16 | } 17 | 18 | pub struct PermissionGuard { 19 | pub permission: Permission, 20 | } 21 | 22 | #[async_trait] 23 | impl Guard for PermissionGuard { 24 | async fn check(&self, ctx: &Context<'_>) -> Result<()> { 25 | if APP_CONFIG.authorization.skip { 26 | info!("Skipping authorization check"); 27 | Ok(()) 28 | } else { 29 | let client: &reqwest::Client = ctx.data().unwrap(); 30 | let mut user = "guest"; 31 | 32 | if let Some(logged_user) = ctx.data_opt::() { 33 | user = logged_user; 34 | } 35 | 36 | let subject = user; 37 | let action; 38 | let object; 39 | 40 | info!("{:?}", serde_json::to_string(&self.permission)); 41 | 42 | // TODO: I don't really like this 43 | match self.permission { 44 | Permission::CreateCoffee => { 45 | object = "coffee"; 46 | action = "create"; 47 | } 48 | Permission::ReadCoffee => { 49 | object = "coffee"; 50 | action = "read"; 51 | } 52 | Permission::UpdateCoffee => { 53 | object = "coffee"; 54 | action = "update"; 55 | } 56 | Permission::DeleteCoffee => { 57 | object = "coffee"; 58 | action = "delete"; 59 | } 60 | } 61 | 62 | info!("Requesting access to resource"); 63 | info!("{:?}::{:?}::{:?}", subject, action, object); 64 | 65 | // The permission query is formed as: 66 | // subject (user role, default to guest) 67 | // action (create, read, update, delete) 68 | // object (coffee) 69 | let request = client 70 | .get(&APP_CONFIG.authorization.url) 71 | .query(&[ 72 | ("subject", String::from(subject)), 73 | ("action", String::from(action)), 74 | ("object", String::from(object)), 75 | ]) 76 | .build()?; 77 | 78 | let res = client.execute(request).await?; 79 | 80 | // println!("Authorized response: {:?}", res); 81 | 82 | let status = res.status(); 83 | match status { 84 | StatusCode::OK => Ok(()), 85 | _ => Err("Cannot access resource".into()), 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /benchmark/config.query.yaml: -------------------------------------------------------------------------------- 1 | #- name: coffees 2 | # # response timeout 3 | # timeout: 1s 4 | # 5 | # # the benchmarks are first run for this duration and the results are ignored 6 | # warmup_duration: 60 7 | # 8 | # # the duration of each benchmark 9 | # duration: 300 10 | # 11 | # # number of open connections to the server 12 | # open_connections: 1000 13 | # 14 | # candidates: 15 | # - name: federation-mercurius-rust 16 | # url: https://gateway.graphql.simoneromano.eu/graphql 17 | # query: Coffees 18 | # queries_file: queries.graphql 19 | # rps: 20 | # - 200 21 | # - 400 22 | # - 600 23 | # - 800 24 | # - 1000 25 | 26 | url: 'https://gateway.graphql.simoneromano.eu/graphql' 27 | queries: 28 | # Name: Unique name for the query 29 | - name: CreateCoffee 30 | # Tools: List of benchmarking tools to run: ['autocannon', 'k6', 'wrk2'] 31 | tools: ['autocannon', 'k6'] 32 | # Execution Strategy: the type of the benchmark to run. Options are: 33 | # REQUESTS_PER_SECOND: Fixed duration, fixed rps. Example parameters: 34 | # duration: 10s 35 | # rps: 500 36 | # FIXED_REQUEST_NUMBER: Complete requests as fast as possible, no duration. Example parameters: 37 | # requests: 10000 38 | # MAX_REQUESTS_IN_DURATION: Make as many requests as possible in duration. Example parameters: 39 | # duration: 10s 40 | # MULTI_STAGE: (K6 only currently) Several stages of REQUESTS_PER_SECOND benchmark. Example parameters: 41 | # initial_rps: 0 42 | # stages: 43 | # - duration: 5s 44 | # target: 100 45 | # - duration: 10s 46 | # target: 1000 47 | # CUSTOM: Pass completely custom options to each tool (see full API spec for all supported options, very large) 48 | execution_strategy: FIXED_REQUEST_NUMBER 49 | requests: 100000 50 | connections: 10000 51 | query: createCoffee 52 | queries_file: createCoffee.mutation.graphql 53 | variables: 54 | name: { start: 0, end: 1000000 } 55 | # Name: Unique name for the query 56 | - name: AllCoffees 57 | # Tools: List of benchmarking tools to run: ['autocannon', 'k6', 'wrk2'] 58 | tools: ['autocannon', 'k6'] 59 | # Execution Strategy: the type of the benchmark to run. Options are: 60 | # REQUESTS_PER_SECOND: Fixed duration, fixed rps. Example parameters: 61 | # duration: 10s 62 | # rps: 500 63 | # FIXED_REQUEST_NUMBER: Complete requests as fast as possible, no duration. Example parameters: 64 | # requests: 10000 65 | # MAX_REQUESTS_IN_DURATION: Make as many requests as possible in duration. Example parameters: 66 | # duration: 10s 67 | # MULTI_STAGE: (K6 only currently) Several stages of REQUESTS_PER_SECOND benchmark. Example parameters: 68 | # initial_rps: 0 69 | # stages: 70 | # - duration: 5s 71 | # target: 100 72 | # - duration: 10s 73 | # target: 1000 74 | # CUSTOM: Pass completely custom options to each tool (see full API spec for all supported options, very large) 75 | # execution_strategy: REQUESTS_PER_SECOND 76 | # rps: 2000 77 | # duration: 10s 78 | execution_strategy: FIXED_REQUEST_NUMBER 79 | requests: 100000 80 | connections: 10000 81 | query: Coffees 82 | queries_file: coffee.query.graphql 83 | -------------------------------------------------------------------------------- /.drone.yml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: pipeline 3 | name: products-service 4 | 5 | steps: 6 | - name: build 7 | image: plugins/docker 8 | settings: 9 | registry: ghcr.io 10 | username: simoneromano96 11 | password: 12 | from_secret: token 13 | repo: ghcr.io/simoneromano96/federation-graphql-rust/products-service 14 | context: ./products-service 15 | dockerfile: ./products-service/.docker/products-service.dockerfile 16 | auto_tag: true 17 | target: production 18 | config: 19 | from_secret: docker_auth_config 20 | 21 | --- 22 | kind: pipeline 23 | name: acpm-service 24 | 25 | steps: 26 | - name: rust-build 27 | image: plugins/docker 28 | settings: 29 | registry: ghcr.io 30 | username: simoneromano96 31 | password: 32 | from_secret: token 33 | repo: ghcr.io/simoneromano96/federation-graphql-rust/acpm-service 34 | context: ./acpm-service 35 | dockerfile: ./acpm-service/.docker/acpm-service.dockerfile 36 | auto_tag: true 37 | target: production 38 | config: 39 | from_secret: docker_auth_config 40 | 41 | # - name: postgres-build 42 | # image: plugins/docker 43 | # settings: 44 | # registry: ghcr.io 45 | # username: simoneromano96 46 | # password: 47 | # from_secret: token 48 | # repo: ghcr.io/simoneromano96/federation-graphql-rust/acpm-postgres 49 | # context: ./acpm-service/.docker 50 | # dockerfile: ./acpm-service/.docker/postgres.dockerfile 51 | # auto_tag: true 52 | 53 | --- 54 | kind: pipeline 55 | name: identity-service 56 | 57 | steps: 58 | - name: build 59 | image: plugins/docker 60 | settings: 61 | registry: ghcr.io 62 | username: simoneromano96 63 | password: 64 | from_secret: token 65 | repo: ghcr.io/simoneromano96/federation-graphql-rust/identity-service 66 | context: ./identity-service 67 | dockerfile: ./identity-service/.docker/identity-service.dockerfile 68 | auto_tag: true 69 | target: production 70 | config: 71 | from_secret: docker_auth_config 72 | 73 | # --- 74 | # kind: pipeline 75 | # name: subscription-service 76 | # 77 | # steps: 78 | # - name: build 79 | # image: plugins/docker 80 | # settings: 81 | # registry: ghcr.io 82 | # username: simoneromano96 83 | # password: 84 | # from_secret: token 85 | # repo: ghcr.io/simoneromano96/federation-graphql-rust/subscription-service 86 | # context: ./subscription-service 87 | # dockerfile: ./subscription-service/.docker/subscription-service.dockerfile 88 | # auto_tag: true 89 | # target: production 90 | 91 | --- 92 | kind: pipeline 93 | name: graphql-gateway 94 | 95 | steps: 96 | - name: build 97 | image: plugins/docker 98 | settings: 99 | registry: ghcr.io 100 | username: simoneromano96 101 | password: 102 | from_secret: token 103 | repo: ghcr.io/simoneromano96/federation-graphql-rust/graphql-gateway 104 | context: ./gateway 105 | dockerfile: ./gateway/.docker/gateway.dockerfile 106 | auto_tag: true 107 | config: 108 | from_secret: docker_auth_config 109 | -------------------------------------------------------------------------------- /products-service/src/config.rs: -------------------------------------------------------------------------------- 1 | use config::{Config, Environment, File}; 2 | use lazy_static::lazy_static; 3 | use log::info; 4 | use serde::{Deserialize, Serialize}; 5 | use std::{ 6 | env, 7 | net::SocketAddr, 8 | }; 9 | 10 | lazy_static! { 11 | pub static ref APP_CONFIG: Settings = Settings::init_config(); 12 | } 13 | 14 | #[derive(Debug, Serialize, Deserialize)] 15 | #[serde(rename_all = "camelCase")] 16 | pub struct RedisConfig { 17 | pub url: String, 18 | } 19 | 20 | #[derive(Debug, Serialize, Deserialize)] 21 | #[serde(rename_all = "camelCase")] 22 | pub struct SessionConfig { 23 | pub secret: String, 24 | } 25 | 26 | #[derive(Debug, Serialize, Deserialize)] 27 | #[serde(rename_all = "camelCase")] 28 | pub struct MongoConfig { 29 | pub url: String, 30 | } 31 | 32 | #[derive(Debug, Serialize, Deserialize)] 33 | #[serde(rename_all = "camelCase")] 34 | pub struct ServerConfig { 35 | pub port: u16, 36 | } 37 | 38 | #[derive(Debug, Serialize, Deserialize)] 39 | #[serde(rename_all = "camelCase")] 40 | pub struct BasicAuthConfig { 41 | pub username: String, 42 | pub password: String, 43 | } 44 | 45 | #[derive(Debug, Serialize, Deserialize)] 46 | #[serde(rename_all = "camelCase")] 47 | pub struct AuthorizationServerConfig { 48 | pub auth: BasicAuthConfig, 49 | pub url: String, 50 | pub skip: bool 51 | } 52 | 53 | #[derive(Debug, Serialize, Deserialize)] 54 | #[serde(rename_all = "camelCase")] 55 | pub struct Settings { 56 | pub debug: bool, 57 | pub redis: RedisConfig, 58 | pub session: SessionConfig, 59 | pub database: MongoConfig, 60 | pub server: ServerConfig, 61 | pub authorization: AuthorizationServerConfig, 62 | } 63 | 64 | impl Settings { 65 | fn init_config() -> Self { 66 | println!("CONFIG INIT"); 67 | let mut s = Config::default(); 68 | let mut config_file_path = env::current_dir().expect("Cannot get current path"); 69 | 70 | // println!("{:?}", config_file_path); 71 | 72 | // Get current RUN_MODE, should be: development/production 73 | let current_env = env::var("RUN_MODE").unwrap_or(String::from("development")); 74 | 75 | config_file_path.push("environments"); 76 | config_file_path.push(format!("{}.yaml", current_env)); 77 | 78 | // println!("{:?}", config_file_path); 79 | 80 | // Add in the current environment file 81 | // Default to 'development' env 82 | s.merge(File::from(config_file_path).required(true)) 83 | .expect("Could not read file"); 84 | 85 | s.merge(Environment::new().prefix("APP").separator("_")).expect("Cannot merge env"); 86 | 87 | // Deserialize configuration 88 | let r: Settings = s.try_into().expect("Configuration error"); 89 | 90 | // Enable all logging 91 | // if r.debug { 92 | // info!("Adding debugging logging"); 93 | // env::set_var("RUST_BACKTRACE", "1"); 94 | // env::set_var("RUST_LOG", "info,actix_web=info,actix_redis=info"); 95 | // info!("Adding debugging logging 2"); 96 | // } 97 | 98 | // Should not be necessary 99 | // if let Ok(connection_string) = env::var("MONGO_CONNECTION_STRING") { 100 | // r.mongo.connection_string = connection_string; 101 | // } 102 | 103 | r 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /acpm-service/src/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod middlewares; 3 | mod models; 4 | mod routes; 5 | 6 | use crate::config::APP_CONFIG; 7 | use actix_web::{App, HttpServer, web::Data, middleware}; 8 | use actix_web_httpauth::middleware::HttpAuthentication; 9 | use log::info; 10 | use middlewares::basic_auth_validator; 11 | use paperclip::actix::{ 12 | web::{get, post, scope}, 13 | OpenApiExt, 14 | }; 15 | use routes::authorization::{add_policy, add_roles_for_user, is_authorized, remove_policy}; 16 | use sqlx::{postgres::PgPoolOptions, Pool, Postgres}; 17 | use sqlx_adapter::{ 18 | casbin::{self, prelude::*}, 19 | SqlxAdapter, 20 | }; 21 | use std::sync::{Arc, Mutex}; 22 | 23 | async fn init_db() -> sqlx::Result> { 24 | // Create a connection pool 25 | let pool: Pool = PgPoolOptions::new() 26 | .max_connections(APP_CONFIG.database.poolsize) 27 | .connect(&APP_CONFIG.database.url) 28 | .await?; 29 | 30 | info!("DB client initialised"); 31 | 32 | sqlx::migrate!("src/migrations") 33 | .run(&pool) 34 | .await?; 35 | 36 | Ok(pool) 37 | } 38 | 39 | async fn init_casbin() -> casbin::Result { 40 | let m = DefaultModel::from_file(&APP_CONFIG.accessmodelpath).await?; 41 | let a = SqlxAdapter::new(&APP_CONFIG.database.url, APP_CONFIG.database.poolsize).await?; 42 | let e = Enforcer::new(m, a).await?; 43 | 44 | info!("Casbin initialised"); 45 | Ok(e) 46 | } 47 | 48 | fn init_logger() { 49 | if APP_CONFIG.debug { 50 | std::env::set_var("RUST_BACKTRACE", "1"); 51 | std::env::set_var("RUST_LOG", "info,actix_web=info,actix_redis=info"); 52 | } 53 | 54 | pretty_env_logger::init(); 55 | info!("Logger initialised"); 56 | } 57 | 58 | #[actix_web::main] 59 | async fn main() -> std::io::Result<()> { 60 | println!("called main()"); 61 | init_logger(); 62 | 63 | let enforcer = Data::new(Mutex::new( 64 | init_casbin() 65 | .await 66 | .expect("could not create access policy enforcer"), 67 | )); 68 | 69 | let pool = init_db().await.expect("Could not init db"); 70 | info!("Initialisation finished, server will listen at port: {:?}", APP_CONFIG.server.port); 71 | 72 | HttpServer::new(move || { 73 | let auth = HttpAuthentication::basic(basic_auth_validator); 74 | 75 | App::new() 76 | .app_data(enforcer.clone()) 77 | .app_data(pool.clone()) 78 | // enable logger 79 | .wrap(middleware::Logger::default()) 80 | // .wrap(auth) 81 | .wrap_api() 82 | .service( 83 | scope("/api") 84 | // Protect the following routes with Basic Auth 85 | .wrap(auth) 86 | .service( 87 | scope("/v1").service( 88 | scope("/authorization") 89 | .route("/is-authorized", get().to(is_authorized)) 90 | .route("/add-policy", post().to(add_policy)) 91 | .route("/remove-policy", post().to(remove_policy)) 92 | .route("/add-roles-for-user", post().to(add_roles_for_user)), 93 | ), 94 | ), 95 | ) 96 | // Mount the JSON spec at this path. 97 | .with_json_spec_at("/openapi") 98 | // Build the app 99 | .build() 100 | }) 101 | .bind(format!("0.0.0.0:{}", APP_CONFIG.server.port))? 102 | .run() 103 | .await 104 | } 105 | -------------------------------------------------------------------------------- /identity-service/src/main.rs: -------------------------------------------------------------------------------- 1 | mod authentication; 2 | mod config; 3 | mod graphql; 4 | mod models; 5 | 6 | use crate::config::APP_CONFIG; 7 | use actix_redis::RedisSession; 8 | use actix_web::{cookie, middleware, App, HttpServer}; 9 | use async_graphql::{ 10 | extensions::{apollo_persisted_queries::ApolloPersistedQueries, ApolloTracing, Logger, apollo_persisted_queries::LruCacheStorage}, 11 | EmptyMutation, EmptySubscription, Schema, 12 | }; 13 | use authentication::routes::*; 14 | use graphql::{gql_playgound, index, IdentityServiceSchema, Query}; 15 | use models::User; 16 | use paperclip::actix::{ 17 | web::{get, post, scope}, 18 | OpenApiExt, 19 | }; 20 | use log::info; 21 | use wither::mongodb::{Client, Database}; 22 | use wither::Model; 23 | 24 | async fn init_db() -> Database { 25 | let db = Client::with_uri_str(&APP_CONFIG.database.url) 26 | .await 27 | .expect("Cannot connect to the db") 28 | .database("identity-service"); 29 | 30 | info!("Mongo database initialised"); 31 | 32 | User::sync(&db) 33 | .await 34 | .expect("Failed syncing indexes"); 35 | 36 | db 37 | } 38 | 39 | fn init_graphql(db: &Database) -> IdentityServiceSchema { 40 | let schema = Schema::build(Query, EmptyMutation, EmptySubscription) 41 | .data(db.clone()) 42 | // .extension(ApolloTracing) 43 | // .extension(ApolloPersistedQueries::new(LruCacheStorage::new(256))) 44 | .extension(Logger) 45 | .finish(); 46 | 47 | info!("Initialised graphql"); 48 | 49 | schema 50 | } 51 | 52 | fn init_logger() { 53 | if APP_CONFIG.debug { 54 | std::env::set_var("RUST_BACKTRACE", "1"); 55 | std::env::set_var("RUST_LOG", "info,actix_web=info,actix_redis=info"); 56 | } 57 | 58 | pretty_env_logger::init(); 59 | info!("Logger initialised"); 60 | } 61 | 62 | #[actix_web::main] 63 | async fn main() -> std::io::Result<()> { 64 | println!("called main()"); 65 | 66 | init_logger(); 67 | 68 | // Connect & sync indexes. 69 | let identity_database = init_db().await; 70 | let graphql_schema = init_graphql(&identity_database); 71 | 72 | // let db = std::sync::Arc::new(identity_database); 73 | 74 | // std::env::set_var("RUST_LOG", "actix_web=info,actix_redis=info"); 75 | // env_logger::init(); 76 | 77 | HttpServer::new(move || { 78 | App::new() 79 | // enable logger 80 | .wrap(middleware::Logger::default()) 81 | // cookie session middleware 82 | .wrap( 83 | RedisSession::new( 84 | &APP_CONFIG.redis.url, 85 | APP_CONFIG.session.secret.as_bytes(), 86 | ) 87 | // Don't allow the cookie to be accessed from javascript 88 | .cookie_http_only(true) 89 | // allow the cookie only from the current domain 90 | .cookie_same_site(cookie::SameSite::Lax), 91 | ) 92 | .data(identity_database.clone()) 93 | .data(graphql_schema.clone()) 94 | // GraphQL 95 | .route("/graphql", actix_web::web::post().to(index)) 96 | .route("/playground", actix_web::web::get().to(gql_playgound)) 97 | // Record services and routes from this line. 98 | .wrap_api() 99 | .service( 100 | scope("/api") 101 | .service( 102 | scope("/v1") 103 | .route("/signup", post().to(signup)) 104 | .route("/login", post().to(login)) 105 | .route("/user-info", get().to(user_info)) 106 | .route("/logout", get().to(logout)) 107 | // .service(signup) 108 | // .service(login) 109 | // .service(user_info) 110 | // .service(logout), 111 | ), 112 | ) 113 | // Mount the JSON spec at this path. 114 | .with_json_spec_at("/openapi") 115 | // Build the app 116 | .build() 117 | }) 118 | .bind(format!("0.0.0.0:{:?}", APP_CONFIG.server.port))? 119 | .run() 120 | .await 121 | } 122 | -------------------------------------------------------------------------------- /acpm-service/src/routes/authorization.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{AddRolesForUser, AddPermissionToRole, PermissionQuery}; 2 | use paperclip::actix::{ 3 | api_v2_operation, 4 | web::{Data, HttpResponse, Json, Query}, 5 | }; 6 | use sqlx_adapter::casbin::prelude::*; 7 | use std::sync::Mutex; 8 | 9 | // #[get("/is-authorized")] 10 | /// Is authorized 11 | /// 12 | /// Basic Auth protected route 13 | /// Gives back if a user (subject) can do something (action) to something else (object) 14 | #[api_v2_operation] 15 | pub async fn is_authorized( 16 | enforcer: Data>, 17 | permission_query: Query, 18 | ) -> std::result::Result { 19 | let sub = &permission_query.subject; 20 | let obj = &permission_query.object; 21 | let act = &permission_query.action; 22 | let e = enforcer.lock().unwrap(); 23 | 24 | let authorized = e.has_permission_for_user(sub, vec![obj.to_owned(), act.to_owned()]); 25 | 26 | if authorized { 27 | Ok(HttpResponse::Ok().body("Is authorized")) 28 | } else { 29 | Ok(HttpResponse::Unauthorized().body("Not authorized")) 30 | } 31 | } 32 | 33 | // #[post("/add-policy")] 34 | /// Add an access policy 35 | /// 36 | /// Basic Auth protected route 37 | /// Adds an access policy 38 | #[api_v2_operation] 39 | pub async fn add_policy( 40 | enforcer: Data>, 41 | permission_query: Json, 42 | ) -> std::result::Result { 43 | let sub = &permission_query.subject; 44 | let obj = &permission_query.object; 45 | let act = &permission_query.action; 46 | let mut e = enforcer.lock().unwrap(); 47 | 48 | let added = e 49 | .add_named_policy("p", vec![sub.clone(), obj.clone(), act.clone()]) 50 | .await 51 | .expect("Cannot add policy"); 52 | 53 | Ok(HttpResponse::Ok().body(format!("Added: {:?}", added))) 54 | } 55 | 56 | // #[post("/remove-policy")] 57 | /// Remove an access policy 58 | /// 59 | /// Basic Auth protected route 60 | /// Removes an access policy 61 | #[api_v2_operation] 62 | pub async fn remove_policy( 63 | enforcer: Data>, 64 | permission_query: Json, 65 | ) -> std::result::Result { 66 | let sub = &permission_query.subject; 67 | let obj = &permission_query.object; 68 | let act = &permission_query.action; 69 | let mut e = enforcer.lock().unwrap(); 70 | 71 | let removed = e 72 | .remove_named_policy("p", vec![sub.clone(), obj.clone(), act.clone()]) 73 | .await 74 | .expect("Cannot remove policy"); 75 | 76 | Ok(HttpResponse::Ok().body(format!("Removed: {:?}", removed))) 77 | } 78 | 79 | // #[post("/add-roles-for-user")] 80 | /// Add user to roles 81 | /// 82 | /// Basic Auth protected route 83 | /// Adds a user (user id) to the given roles array 84 | #[api_v2_operation] 85 | pub async fn add_roles_for_user( 86 | enforcer: Data>, 87 | add_user: Json, 88 | ) -> std::result::Result { 89 | let mut e = enforcer.lock().unwrap(); 90 | 91 | let added = e 92 | .add_roles_for_user(&add_user.user_id, add_user.roles.clone(), Some(add_user.domain.as_str())) 93 | .await 94 | .expect("Cannot add policy"); 95 | 96 | Ok(HttpResponse::Ok().body(format!("Added: {:?}", added))) 97 | } 98 | 99 | // #[post("/add-permissions-for-role")] 100 | /// Add permissions to role 101 | /// 102 | /// Basic Auth protected route 103 | /// Adds all the permissions in the array to a role 104 | #[api_v2_operation] 105 | pub async fn add_permissions_for_role( 106 | enforcer: Data>, 107 | add_permission: Json, 108 | ) -> std::result::Result { 109 | let mut e = enforcer.lock().unwrap(); 110 | 111 | let permissions: Vec> = 112 | add_permission 113 | .permissions 114 | .iter() 115 | .map(|permission| vec![permission.action.clone(), permission.object.clone()]) 116 | .collect(); 117 | 118 | let added = e 119 | .add_permissions_for_user(&add_permission.role, permissions) 120 | .await 121 | .expect("Cannot add policy"); 122 | 123 | Ok(HttpResponse::Ok().body(format!("Added: {:?}", added))) 124 | } 125 | -------------------------------------------------------------------------------- /identity-service/src/authentication/routes.rs: -------------------------------------------------------------------------------- 1 | use crate::authentication; 2 | use crate::models::{User, UserInfo, UserInput}; 3 | use actix_session::Session; 4 | use paperclip::actix::{ 5 | api_v2_operation, 6 | web::{Data, HttpResponse, Json}, 7 | }; 8 | use wither::bson::{doc, oid::ObjectId}; 9 | use wither::mongodb::Database as MongoDatabase; 10 | use wither::prelude::*; 11 | 12 | // #[post("/signup")] 13 | /// User signup 14 | /// 15 | /// Creates a new user but doesn't log in the user 16 | /// Currently like this because of future developements 17 | #[api_v2_operation] 18 | pub async fn signup( 19 | db: Data, 20 | new_user: Json, 21 | ) -> Result, HttpResponse> { 22 | let username = &new_user.username; 23 | let clear_password = &new_user.password; 24 | 25 | let password = authentication::hash_password(clear_password); 26 | 27 | // Create a user. 28 | let mut user = User::new_user(username, &password); 29 | 30 | if let Ok(_) = user.save(&db, None).await { 31 | Ok(Json(user.to_user_info())) 32 | } else { 33 | Err(HttpResponse::BadRequest().body("Username is already registered")) 34 | } 35 | } 36 | 37 | // #[post("/login")] 38 | /// User login 39 | /// 40 | /// Logs in the user via the provided credentials, will set a cookie session 41 | #[api_v2_operation] 42 | pub async fn login( 43 | credentials: Json, 44 | session: Session, 45 | db: Data, 46 | ) -> Result, HttpResponse> { 47 | let maybe_user: Option = session.get("user_id").unwrap(); 48 | if let Some(user_id) = maybe_user { 49 | // We can be sure that the user exists if there is a session 50 | let user = User::find_by_id(&db, &user_id).await.unwrap(); 51 | session.renew(); 52 | Ok(Json(user.to_user_info())) 53 | } else { 54 | // let find_user_result: Result, wither::WitherError> = 55 | // User::find_one(&db, doc! { "username": &credentials.username }, None).await; 56 | // if let Ok(find_user) = find_user_result { 57 | if let Some(user) = User::find_by_username(&db, &credentials.username).await { 58 | let clear_password = &credentials.password; 59 | let hashed_password = &user.password; 60 | 61 | let password_verified = authentication::verify_hash(hashed_password, clear_password); 62 | 63 | if password_verified { 64 | let info = user.to_user_info(); 65 | // If the user exists there is a user id 66 | session.set("user_id", user.id.unwrap()).unwrap(); 67 | session.set("user_role", user.role).unwrap(); 68 | Ok(Json(info)) 69 | } else { 70 | Err(HttpResponse::BadRequest().body("Wrong password")) 71 | } 72 | } else { 73 | Err(HttpResponse::NotFound().body("User not found")) 74 | } 75 | // } else { 76 | // Err(HttpResponse::InternalServerError().body("")) 77 | // } 78 | } 79 | } 80 | 81 | // #[get("/user-info")] 82 | /// User info 83 | /// 84 | /// Gets the current user info if he is logged in 85 | #[api_v2_operation] 86 | pub async fn user_info( 87 | session: Session, 88 | db: Data, 89 | ) -> Result, HttpResponse> { 90 | let maybe_id: Option = session.get("user_id").unwrap(); 91 | 92 | if let Some(id) = maybe_id { 93 | let maybe_user = User::find_by_id(&db, &id).await; 94 | if let Some(user) = maybe_user { 95 | session.renew(); 96 | Ok(Json(user.to_user_info())) 97 | } else { 98 | session.clear(); 99 | Err(HttpResponse::Unauthorized().body("Error")) 100 | } 101 | } else { 102 | Err(HttpResponse::Unauthorized().body("Not logged in")) 103 | } 104 | } 105 | 106 | // #[get("/logout")] 107 | /// Logout 108 | /// 109 | /// Logs out the current user invalidating the session if he is logged in 110 | #[api_v2_operation] 111 | pub async fn logout(session: Session) -> Result { 112 | let maybe_user: Option = session.get("user_id").unwrap(); 113 | 114 | if let Some(_) = maybe_user { 115 | session.clear(); 116 | Ok(HttpResponse::Ok().body("Logged out")) 117 | } else { 118 | Err(HttpResponse::BadRequest().body("Already logged out")) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /gateway/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 6 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 7 | // "lib": [], /* Specify library files to be included in the compilation. */ 8 | // "allowJs": true, /* Allow javascript files to be compiled. */ 9 | // "checkJs": true, /* Report errors in .js files. */ 10 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 11 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 12 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 13 | "sourceMap": true, /* Generates corresponding '.map' file. */ 14 | // "outFile": "./", /* Concatenate and emit output to single file. */ 15 | // "outDir": "./", /* Redirect output structure to the directory. */ 16 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 17 | // "composite": true, /* Enable project compilation */ 18 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 19 | // "removeComments": true, /* Do not emit comments to output. */ 20 | // "noEmit": true, /* Do not emit outputs. */ 21 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 22 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 23 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 24 | 25 | /* Strict Type-Checking Options */ 26 | "strict": false, /* Enable all strict type-checking options. */ 27 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 28 | // "strictNullChecks": true, /* Enable strict null checks. */ 29 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 30 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 34 | 35 | /* Additional Checks */ 36 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 37 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 38 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 40 | 41 | /* Module Resolution Options */ 42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 43 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 46 | // "typeRoots": [], /* List of folders to include type definitions from. */ 47 | // "types": [], /* Type declaration files to be included in compilation. */ 48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 49 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 50 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 51 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /products-service/src/main.rs: -------------------------------------------------------------------------------- 1 | mod authorization; 2 | mod config; 3 | mod graphql; 4 | mod models; 5 | 6 | use crate::config::APP_CONFIG; 7 | use actix_redis::RedisSession; 8 | use actix_web::{ 9 | cookie, guard, middleware, 10 | web::{self, post}, 11 | App, HttpServer, 12 | }; 13 | use async_graphql::{Schema, extensions::ApolloTracing, extensions::{Logger, apollo_persisted_queries::{ApolloPersistedQueries, LruCacheStorage}}}; 14 | use base64; 15 | use graphql::{index, index_ws, Mutation, ProductsServiceSchema, Query, Subscription}; 16 | use log::info; 17 | use models::Coffee; 18 | use pretty_env_logger; 19 | // use redis_async::client::{paired::PairedConnection, PubsubConnection}; 20 | use reqwest::{header, ClientBuilder}; 21 | use wither::prelude::*; 22 | 23 | /* 24 | pub struct AppData { 25 | mongo_database: wither::mongodb::Database, 26 | redis_publish: redis::aio::Connection, 27 | // redis_pubsub: redis::aio::PubSub, 28 | } 29 | */ 30 | 31 | async fn init_mongo() -> wither::mongodb::Database { 32 | use wither::mongodb::Client; 33 | 34 | // Connect to the database. 35 | let products_database = Client::with_uri_str(&APP_CONFIG.database.url) 36 | .await 37 | .expect("Cannot connect to the db") 38 | .database("products-service"); 39 | 40 | info!("Mongo database initialised"); 41 | 42 | Coffee::sync(&products_database) 43 | .await 44 | .expect("Failed syncing indexes"); 45 | 46 | info!("Models synced"); 47 | 48 | products_database 49 | } 50 | 51 | async fn init_redis() -> redis::Client { 52 | // use redis_async::client; 53 | 54 | /* 55 | let addr = format!("{}:{}", APP_CONFIG.redis.host, APP_CONFIG.redis.port) 56 | .parse() 57 | .expect("Cannot parse Redis connection string"); 58 | */ 59 | let addr = format!("redis://{}", APP_CONFIG.redis.url); 60 | 61 | let client: redis::Client = redis::Client::open(addr).unwrap(); 62 | 63 | info!("Redis client initialised"); 64 | 65 | client 66 | /* 67 | ( 68 | client::paired_connect(&addr) 69 | .await 70 | .expect("Cannot open connection"), 71 | client::pubsub_connect(&addr) 72 | .await 73 | .expect("Cannot connect to Redis"), 74 | ) 75 | */ 76 | } 77 | 78 | fn init_http_client() -> reqwest::Client { 79 | let mut headers = header::HeaderMap::new(); 80 | let basic_credentials = format!( 81 | "{}:{}", 82 | APP_CONFIG.authorization.auth.username, 83 | APP_CONFIG.authorization.auth.password 84 | ); 85 | let basic_auth_header_value = format!("Basic {}", base64::encode(basic_credentials)); 86 | 87 | headers.insert( 88 | header::AUTHORIZATION, 89 | header::HeaderValue::from_str(&basic_auth_header_value).expect("Invalid header value"), 90 | ); 91 | 92 | // println!("{:?}", headers); 93 | 94 | info!("HTTP Client initialised"); 95 | 96 | ClientBuilder::new() 97 | .default_headers(headers) 98 | .build() 99 | .expect("Could not create http client") 100 | } 101 | 102 | fn init_graphql( 103 | db: wither::mongodb::Database, 104 | redis_client: redis::Client, 105 | http_client: reqwest::Client, 106 | ) -> ProductsServiceSchema { 107 | let schema = Schema::build(Query, Mutation, Subscription) 108 | .data(db) 109 | .data(redis_client) 110 | .data(http_client) 111 | // .extension(ApolloTracing) 112 | // .extension(ApolloPersistedQueries::new(LruCacheStorage::new(256))) 113 | .extension(Logger) 114 | .finish(); 115 | 116 | info!("Initialised graphql"); 117 | 118 | schema 119 | } 120 | 121 | fn init_logger() { 122 | if APP_CONFIG.debug { 123 | std::env::set_var("RUST_BACKTRACE", "1"); 124 | std::env::set_var("RUST_LOG", "info,actix_web=info,actix_redis=info"); 125 | } 126 | 127 | pretty_env_logger::init(); 128 | info!("Logger initialised"); 129 | } 130 | 131 | #[actix_web::main] 132 | async fn main() -> std::io::Result<()> { 133 | println!("called main()"); 134 | 135 | init_logger(); 136 | let db = init_mongo().await; 137 | let redis_client = init_redis().await; 138 | let http_client = init_http_client(); 139 | let schema = init_graphql(db, redis_client, http_client); 140 | info!("Initialisation finished, server will listen at port: {:?}", APP_CONFIG.server.port); 141 | 142 | HttpServer::new(move || { 143 | App::new() 144 | // share GraphQL Schema 145 | .data(schema.clone()) 146 | // enable logger 147 | .wrap(middleware::Logger::default()) 148 | // cookie session middleware 149 | .wrap( 150 | RedisSession::new( 151 | &APP_CONFIG.redis.url, 152 | APP_CONFIG.session.secret.as_bytes(), 153 | ) 154 | // Don't allow the cookie to be accessed from javascript 155 | .cookie_http_only(true) 156 | // allow the cookie only from the current domain 157 | .cookie_same_site(cookie::SameSite::Strict), 158 | ) 159 | // CORS 160 | // .wrap(Cors::default()) 161 | // GraphQL 162 | .route("/graphql", post().to(index)) 163 | // GraphQL Subscriptions 164 | .service( 165 | web::resource("/graphql") 166 | .guard(guard::Get()) 167 | .guard(guard::Header("upgrade", "websocket")) 168 | .to(index_ws), 169 | ) 170 | // .service(web::resource("/playground").guard(guard::Get()).to(gql_playgound)) 171 | }) 172 | .bind(format!("0.0.0.0:{:?}", APP_CONFIG.server.port))? 173 | .run() 174 | .await 175 | } 176 | -------------------------------------------------------------------------------- /docker-compose.prod.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | 3 | services: 4 | redis: 5 | image: redis:alpine 6 | # ports: 7 | # - 6379:6379 8 | networks: 9 | - app-network 10 | 11 | mongo: 12 | image: mongo 13 | restart: always 14 | # ports: 15 | # - 27017:27017 16 | environment: 17 | MONGO_INITDB_ROOT_USERNAME: root 18 | MONGO_INITDB_ROOT_PASSWORD: example 19 | networks: 20 | - app-network 21 | 22 | mongo-express: 23 | image: mongo-express 24 | restart: always 25 | ports: 26 | - 8081:8081 27 | environment: 28 | ME_CONFIG_MONGODB_ADMINUSERNAME: root 29 | ME_CONFIG_MONGODB_ADMINPASSWORD: example 30 | networks: 31 | - app-network 32 | 33 | postgres: 34 | image: postgres 35 | restart: unless-stopped 36 | environment: 37 | POSTGRES_USER: casbin 38 | POSTGRES_PASSWORD: casbin 39 | POSTGRES_DB: casbin 40 | volumes: 41 | - postgres_data:/var/lib/postgresql/data 42 | command: ["postgres", "-c", "log_statement=all"] 43 | networks: 44 | - app-network 45 | 46 | adminer: 47 | image: adminer 48 | restart: unless-stopped 49 | #ports: 50 | # - 8080:8080 51 | labels: 52 | - "traefik.enable=true" 53 | - "traefik.docker.network=proxy" 54 | - "traefik.http.routers.graphql-adminer.rule=Host(`adminer.graphql.simoneromano.eu`)" 55 | - "traefik.http.routers.graphql-adminer.entrypoints=websecure" 56 | - "traefik.http.routers.graphql-adminer.tls=true" 57 | - "traefik.http.routers.graphql-adminer.tls.certresolver=leresolver" 58 | # Expose right ports 59 | - "traefik.http.services.graphql-adminer.loadbalancer.server.port=8080" 60 | networks: 61 | - proxy 62 | - app-network 63 | 64 | acpm-service: 65 | image: ghcr.io/simoneromano96/federation-graphql-rust/acpm-service 66 | restart: unless-stopped 67 | environment: 68 | APP_DATABASE_URL: "postgres://casbin:casbin@postgres:5432/casbin" 69 | APP_DATABASE_POOLSIZE: 16 70 | APP_SERVER_PORT: 80 71 | APP_DEBUG: "true" 72 | labels: 73 | - "traefik.enable=true" 74 | - "traefik.docker.network=proxy" 75 | - "traefik.http.routers.acpm-service.rule=Host(`acpm.graphql.simoneromano.eu`)" 76 | - "traefik.http.routers.acpm-service.entrypoints=websecure" 77 | - "traefik.http.routers.acpm-service.tls=true" 78 | - "traefik.http.routers.acpm-service.tls.certresolver=leresolver" 79 | # Expose right ports 80 | - "traefik.http.services.acpm-service.loadbalancer.server.port=80" 81 | #ports: 82 | # - 3999:80 83 | depends_on: 84 | - postgres 85 | networks: 86 | - proxy 87 | - app-network 88 | 89 | graphql-gateway: 90 | image: ghcr.io/simoneromano96/federation-graphql-rust/graphql-gateway 91 | restart: unless-stopped 92 | environment: 93 | PORT: 80 94 | IDENTITY_SERVICE_URL: "https://identity.graphql.simoneromano.eu/graphql" 95 | PRODUCTS_SERVICE_URL: "https://products.graphql.simoneromano.eu/graphql" 96 | labels: 97 | - "traefik.enable=true" 98 | - "traefik.docker.network=proxy" 99 | - "traefik.http.routers.graphql-gateway.rule=Host(`gateway.graphql.simoneromano.eu`)" 100 | - "traefik.http.routers.graphql-gateway.entrypoints=websecure" 101 | - "traefik.http.routers.graphql-gateway.tls=true" 102 | - "traefik.http.routers.graphql-gateway.tls.certresolver=leresolver" 103 | # Expose right ports 104 | - "traefik.http.services.graphql-gateway.loadbalancer.server.port=80" 105 | #ports: 106 | # - 4000:80 107 | depends_on: 108 | - identity-service 109 | - products-service 110 | networks: 111 | - proxy 112 | - app-network 113 | 114 | identity-service: 115 | image: ghcr.io/simoneromano96/federation-graphql-rust/identity-service 116 | restart: unless-stopped 117 | environment: 118 | APP_DEBUG: "true" 119 | APP_DATABASE_URL: "mongodb://root:example@mongo:27017/admin" 120 | APP_SERVER_PORT: 80 121 | APP_REDIS_URL: "redis:6379" 122 | labels: 123 | - "traefik.enable=true" 124 | - "traefik.docker.network=proxy" 125 | - "traefik.http.routers.identity-service.rule=Host(`identity.graphql.simoneromano.eu`)" 126 | - "traefik.http.routers.identity-service.entrypoints=websecure" 127 | - "traefik.http.routers.identity-service.tls=true" 128 | - "traefik.http.routers.identity-service.tls.certresolver=leresolver" 129 | # Expose right ports 130 | - "traefik.http.services.identity-service.loadbalancer.server.port=80" 131 | #ports: 132 | # - 4001:80 133 | depends_on: 134 | - redis 135 | - mongo 136 | networks: 137 | - proxy 138 | - app-network 139 | 140 | products-service: 141 | image: ghcr.io/simoneromano96/federation-graphql-rust/products-service 142 | restart: unless-stopped 143 | environment: 144 | APP_DEBUG: "true" 145 | APP_REDIS_URL: "redis:6379" 146 | APP_DATABASE_URL: "mongodb://root:example@mongo:27017/admin" 147 | APP_SERVER_PORT: 80 148 | APP_AUTHORIZATION_SKIP: "true" 149 | APP_AUTHORIZATION_URL: "https://acpm.graphql.simoneromano.eu/api/v1/authorization/is-authorized" 150 | APP_AUTHORIZATION_AUTH_USERNAME: "products-service" 151 | APP_AUTHORIZATION_AUTH_PASSWORD: "password" 152 | labels: 153 | - "traefik.enable=true" 154 | - "traefik.docker.network=proxy" 155 | - "traefik.http.routers.products-service.rule=Host(`products.graphql.simoneromano.eu`)" 156 | - "traefik.http.routers.products-service.entrypoints=websecure" 157 | - "traefik.http.routers.products-service.tls=true" 158 | - "traefik.http.routers.products-service.tls.certresolver=leresolver" 159 | # Expose right ports 160 | - "traefik.http.services.products-service.loadbalancer.server.port=80" 161 | #ports: 162 | # - 4002:80 163 | depends_on: 164 | - acpm-service 165 | - redis 166 | - mongo 167 | networks: 168 | - proxy 169 | - app-network 170 | 171 | #subscription-service: 172 | # image: ghcr.io/simoneromano96/federation-graphql-rust/subscription-service 173 | # restart: unless-stopped 174 | # environment: 175 | # APP_DATABASE_URL: mongodb://root:example@mongo:27017/admin 176 | # APP_SERVER_PORT: 80 177 | # ports: 178 | # - 4003:80 179 | 180 | networks: 181 | app-network: 182 | proxy: 183 | external: true 184 | 185 | volumes: 186 | postgres_data: 187 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://cloud.drone.io/api/badges/simoneromano96/federation-graphql-rust/status.svg)](https://cloud.drone.io/simoneromano96/federation-graphql-rust) 2 | 3 | # Complete GraphQL with Apollo Federation services in Rust 4 | 5 | ## Services 6 | 7 | The services are: 8 | 9 | * graphql gateway 10 | 11 | * user-management 12 | 13 | * (Access Control & Permission Management) acpm 14 | 15 | * subscriptions 16 | 17 | * products 18 | 19 | * reviews 20 | 21 | ## User Management Service 22 | 23 | ### Requirements 24 | 25 | This service must implement: login; signup; logout; and user-info, should implement mail verification. 26 | 27 | This service must be a REST service and should be a GraphQL service where possible, every service must be documented (REST with OpenAPI and GraphQL with the builtin schema docs). 28 | 29 | Routes details: 30 | 31 | * /signup must register a new user into the db 32 | 33 | * /login must accept user credentials and must set a session cookie 34 | 35 | * /user-info must give the user identity 36 | 37 | * /logout must destroy the user session 38 | 39 | ### Libraries 40 | 41 | * actix-web 42 | 43 | * actix-redis 44 | 45 | * actix-session 46 | 47 | * rust-argon2 48 | 49 | * wither 50 | 51 | * paperclip 52 | 53 | * casbin 54 | 55 | ## ACPM 56 | 57 | ### Requirements 58 | 59 | This service must implement CRUD operations related to policies and roles of users. 60 | 61 | The following routes must be protected: 62 | 63 | * /is-authorized must say if a user (can also be a guest) can do something to some resources 64 | 65 | * /add-policy must add the policy triplet (subject, action, object) 66 | 67 | * /delete-policy must remove the policy triplet (subject, action, object) 68 | 69 | * /add-role must add a role 70 | 71 | * /remove-role must remove a role 72 | 73 | * /add-user-to-role must add a user to a role 74 | 75 | * /remove-user-from-role must remove a user from a role 76 | 77 | ### Libraries 78 | 79 | * actix-web 80 | 81 | * paperclip 82 | 83 | * casbin 84 | 85 | ## Rust GraphQL services 86 | 87 | ### Libraries 88 | 89 | * actix-web 90 | 91 | * actix-redis 92 | 93 | * actix-session 94 | 95 | * async-graphql 96 | 97 | * paseto (TBH shouldn't be necessary) 98 | 99 | * wither 100 | 101 | ## Node GraphQL gateway 102 | 103 | ### Libraries 104 | 105 | * fastify 106 | 107 | * mercurius 108 | 109 | ## Decisions and issues 110 | 111 | I'd like to have a only GraphQL architecture but there is a problem: I can't handle stateful sessions in graphql resolvers; this would force me to handle sessions in a stateless way, which I personally don't like, cookie sessions are the most secure way to handle the user identity. 112 | 113 | I could use Paseto to generate some tokens but this don't solve the main problem of where I should store them. 114 | 115 | So the Identity/Accounts Service (they should be merged) exposes both "REST" (I wouldn't call them REST TBH) and GraphQL API standards. 116 | 117 | I believe that the tokens are as secure as where they are stored, so having them in the local storage or session storage in a untrusted client is bad, cookies, on the other hand, can be blocked from reading from JS (HTTP Only), are set automatically and they can be invalidated (with a real logout). 118 | 119 | Tokens are good/perfect for machine-to-machine communication where the parties are trusted. 120 | 121 | When creating an IAM service I believe the hardest decisions are about the DB and the hashing algorithm. 122 | 123 | I choose Mongo mostly because it is "cluster-able" easily, this will improve both the performance and the safety of the data at the cost of having some un-synced data, this is not a big deal though because I don't expect a lot of updates on the user model. 124 | 125 | Another thing I like about mongo for is how easily I can add/remove data from the collection being a "schema-less" database, I do expect a lot of changes in the User collection as my ecosystem grows. 126 | 127 | I know about cockroachDB which is a postgres-compatible scalable DB but for the preceding reason I'm not sure if it is a good choice, it will force me to handle migrations. 128 | 129 | Until now I've talked about Authentication mostly, now I should solve the problem of Authorization, I believe that the best Authorization mechanism is the RBAC (Role-Based access control) which defines some roles and actions where each role can execute some determinate actions. 130 | 131 | In my services I have the session, so I can get the current User before resolving the Graph query, but I'm still not sure on how to handle all this, I'd like to keep everything in my IAM service or I could also detatch it with an Access Control and Permission Management service, I'm not sure if detatching is a good idea right now, mostly because the only endpoint I can think of is the "/is-authorized" endpoint, that expects the user role and the action and gives back if he can execute the action, but this means that there will be an added latency that I'm not sure I like, if I could cache the roles and actions tough it might be ok. 132 | 133 | The chosen library for the access control is casbin, it is a multi-language library with an adapter for Rust and SQLx, it exposes an admin portal too. 134 | 135 | Now there is another issue, I don't like having a service using both mongo and postgres, using both the wither and SQLx dependencies, I think I should merge them into postgres and use SQLx. 136 | 137 | It's unclear to me currently how to enable federation in the services, if I put `graphql(entity)` to a query that doesn't have any input (a Read All for example), it says that it must have an input, if it has an input it is not exposed to the gateway, but if there is no `graphql(entity)` in the query schema nothing is exposed to the gateway, I had to put some fake queries/mutations that return the type that is needed for the other queries/mutations. 138 | 139 | Turns out I didn't understand at all the entity concept, it is internally used between the services to get particular informations, marking something with entity will not expose it externally but internally between the services, there is no need to put them in mutations too, just in a queries for each entity. 140 | 141 | If I need to expose both an entity and a query that fetches that entity the code must be sadly duplicated as for Federation design. 142 | 143 | To handle subscriptions, since I already have redis, I will use the pub-sub interface of redis avoiding more dependencies. 144 | 145 | Subscriptions are not supported by federation yet, but mercurius supports them, this means that I have to create a new service for the subscriptions or migrate everything to mercurius, since migrating from Rust to JS is fast I'll first try the create a new service and then I'll move all the services to JS and see if there is an appreciable difference in performances. 146 | 147 | I should also see why it does not work, it could just be a matter of changing the generated schema of `async-graphql` adding "extend". 148 | 149 | I decided to give schema-stitching a try after I saw that it is mantained and it works surprisingly well, subscriptions included, I've migrated the gateway to that. 150 | -------------------------------------------------------------------------------- /products-service/src/graphql/query.rs: -------------------------------------------------------------------------------- 1 | use crate::models::{Coffee, CreateCoffeeInput, UpdateCoffeeInput}; 2 | use crate::{ 3 | authorization::{Permission, PermissionGuard}, 4 | models::coffee::CoffeeChanged, 5 | models::coffee::MutationType, 6 | }; 7 | use async_graphql::guard::Guard; 8 | use async_graphql::{Context, Object, Result, ID, Subscription}; 9 | use futures::{Stream, stream::StreamExt}; 10 | use log::info; 11 | // use redis_async::{resp::FromResp, client::{PairedConnection, PubsubConnection}, resp_array}; 12 | use redis::AsyncCommands; 13 | use serde_json; 14 | use wither::prelude::*; 15 | use wither::{ 16 | bson::{doc, oid::ObjectId}, 17 | mongodb::Database, 18 | }; 19 | 20 | async fn fetch_all_coffees(db: &Database) -> Result> { 21 | info!("Fetching all coffees"); 22 | let mut coffees: Vec = Vec::new(); 23 | 24 | let mut cursor = Coffee::find(db, None, None).await?; 25 | 26 | while let Some(coffee) = cursor.next().await { 27 | coffees.push(coffee.unwrap()); 28 | } 29 | 30 | Ok(coffees) 31 | } 32 | 33 | async fn fetch_coffee_by_id(db: &Database, id: String) -> Result { 34 | info!("Fetching a coffee with id: {:?}", id); 35 | 36 | let query = doc! { 37 | "_id": ObjectId::with_string(&id)?, 38 | }; 39 | 40 | if let Some(coffee_model) = Coffee::find_one(db, Some(query), None).await? { 41 | Ok(coffee_model) 42 | } else { 43 | Err(format!("Coffee with ID {:?} not found", id).into()) 44 | } 45 | } 46 | 47 | async fn create_coffee( 48 | db: &Database, 49 | redis_client: &redis::Client, 50 | input: CreateCoffeeInput, 51 | ) -> Result { 52 | let mut coffee = Coffee { 53 | id: None, 54 | name: input.name, 55 | price: input.price, 56 | image_url: input.image_url.into_string(), 57 | description: input.description, 58 | }; 59 | 60 | coffee.save(db, None).await?; 61 | 62 | let message = CoffeeChanged { 63 | mutation_type: MutationType::Created, 64 | id: ID::from(coffee.id.clone().unwrap().to_string()), 65 | }; 66 | 67 | let json = serde_json::to_string(&message)?; 68 | 69 | let mut publish_conn = redis_client.get_async_connection().await?; 70 | publish_conn.publish("coffees", &json).await?; 71 | // redis_connection.send_and_forget(resp_array!["PUBLISH", "coffees", &json]); 72 | 73 | // SimpleBroker::publish(CoffeeChanged { 74 | // mutation_type: MutationType::Created, 75 | // id: ID::from(coffee.id.clone().unwrap().to_string()), 76 | // }); 77 | 78 | Ok(coffee) 79 | } 80 | 81 | async fn update_coffee(db: &Database, input: UpdateCoffeeInput) -> Result { 82 | let id = input.id; 83 | 84 | let query = doc! { 85 | "_id": ObjectId::with_string(&id)? 86 | }; 87 | 88 | if let Some(mut coffee) = Coffee::find_one(db, Some(query), None).await? { 89 | if let Some(name) = input.name { 90 | coffee.name = name; 91 | } 92 | 93 | if let Some(price) = input.price { 94 | coffee.price = price; 95 | } 96 | 97 | if let Some(description) = input.description { 98 | coffee.description = Some(description); 99 | } 100 | 101 | if let Some(image_url) = input.image_url { 102 | coffee.image_url = image_url.to_string(); 103 | } 104 | 105 | coffee.save(db, None).await?; 106 | 107 | Ok(coffee) 108 | } else { 109 | Err(format!("Coffee with id: {:?} not found", id).into()) 110 | } 111 | } 112 | 113 | async fn delete_coffee(db: &Database, id: String) -> Result { 114 | let query = doc! { 115 | "_id": ObjectId::with_string(&id)? 116 | }; 117 | 118 | let res: Option = Coffee::find_one_and_delete(db, query, None).await?; 119 | 120 | if let Some(coffee) = res { 121 | // SimpleBroker::publish(CoffeeChanged { 122 | // mutation_type: MutationType::Deleted, 123 | // id: ID::from(coffee.id.clone().unwrap().to_string()), 124 | // }); 125 | 126 | Ok(coffee) 127 | } else { 128 | Err(format!("Coffee with ID {:?} not found", id).into()) 129 | } 130 | } 131 | 132 | pub struct Query; 133 | 134 | #[Object(extends, cache_control(max_age = 60))] 135 | impl Query { 136 | /// Returns an array with all the coffees or an empty array 137 | #[graphql(guard(PermissionGuard(permission = "Permission::ReadCoffee")))] 138 | async fn coffees(&self, ctx: &Context<'_>) -> Result> { 139 | let db: &Database = ctx.data()?; 140 | fetch_all_coffees(db).await 141 | } 142 | 143 | /// Returns a coffee by its ID, will return error if none is present with the given ID 144 | #[graphql(guard(PermissionGuard(permission = "Permission::ReadCoffee")))] 145 | async fn coffee(&self, ctx: &Context<'_>, id: ID) -> Result { 146 | let db: &Database = ctx.data()?; 147 | fetch_coffee_by_id(db, id.to_string()).await 148 | } 149 | 150 | /// Returns a coffee by its ID, will return error if none is present with the given ID 151 | #[graphql(entity)] 152 | async fn find_coffee_by_id(&self, ctx: &Context<'_>, id: ID) -> Result { 153 | let db: &Database = ctx.data()?; 154 | fetch_coffee_by_id(db, id.to_string()).await 155 | } 156 | } 157 | 158 | pub struct Mutation; 159 | 160 | #[Object(extends, cache_control(max_age = 60))] 161 | impl Mutation { 162 | /// Creates a new coffee 163 | #[graphql(guard(PermissionGuard(permission = "Permission::CreateCoffee")))] 164 | async fn create_coffee(&self, ctx: &Context<'_>, input: CreateCoffeeInput) -> Result { 165 | // let redis_pubsub_connection: &PubsubConnection = ctx.data()?; 166 | // let (redis_connection, _): &(PairedConnection, PubsubConnection) = ctx.data()?; 167 | let redis_client: &redis::Client = ctx.data()?; 168 | 169 | let db: &Database = ctx.data()?; 170 | 171 | create_coffee(db, redis_client, input).await 172 | } 173 | 174 | /// Updates a coffee 175 | #[graphql(guard(PermissionGuard(permission = "Permission::UpdateCoffee")))] 176 | async fn update_coffee(&self, ctx: &Context<'_>, input: UpdateCoffeeInput) -> Result { 177 | let db: &Database = ctx.data()?; 178 | update_coffee(db, input).await 179 | } 180 | 181 | /// Deletes a coffeee 182 | #[graphql(guard(PermissionGuard(permission = "Permission::DeleteCoffee")))] 183 | async fn delete_coffee(&self, ctx: &Context<'_>, id: String) -> Result { 184 | let db: &Database = ctx.data()?; 185 | delete_coffee(db, id).await 186 | } 187 | } 188 | 189 | pub struct Subscription; 190 | 191 | #[Subscription] 192 | impl Subscription { 193 | async fn coffees( 194 | &self, 195 | ctx: &Context<'_>, 196 | mutation_type: Option, 197 | ) -> impl Stream { 198 | // let (_, pubsub_connection): &(PairedConnection, PubsubConnection) = ctx.data().unwrap(); 199 | let redis_client: &redis::Client = ctx.data().unwrap(); 200 | 201 | let mut pubsub_conn = redis_client.get_async_connection().await.unwrap().into_pubsub(); 202 | 203 | pubsub_conn.subscribe("coffees").await.expect("Cannot subscribe to topic"); 204 | let pubsub_stream = pubsub_conn.into_on_message(); 205 | 206 | pubsub_stream.filter_map(move |msg| { 207 | let mut res = None; 208 | if let Some(mutation_type) = mutation_type { 209 | if let Ok(payload) = msg.get_payload::() { 210 | let coffee_changed: CoffeeChanged = serde_json::from_str(&payload).unwrap(); 211 | if coffee_changed.mutation_type == mutation_type { 212 | res = Some(coffee_changed) 213 | } 214 | } 215 | } 216 | async { res } 217 | }) 218 | 219 | /* 220 | let msgs = pubsub_connection 221 | .subscribe("coffees") 222 | .await 223 | .expect("Cannot subscribe to topic"); 224 | 225 | msgs.filter_map(move |e| { 226 | let mut res = None; 227 | if let Ok(resp) = e { 228 | if let Some(mutation_type) = mutation_type { 229 | let msg: CoffeeChanged = 230 | serde_json::from_str(&(String::from_resp(resp).unwrap())).unwrap(); 231 | if msg.mutation_type == mutation_type { 232 | res = Some(msg) 233 | } 234 | } 235 | } 236 | async move { res } 237 | }) 238 | */ 239 | // SimpleBroker::::subscribe().filter(move |event| { 240 | // let res = if let Some(mutation_type) = mutation_type { 241 | // event.mutation_type == mutation_type 242 | // } else { 243 | // true 244 | // }; 245 | // async move { res } 246 | // }) 247 | } 248 | } 249 | -------------------------------------------------------------------------------- /workspace.json: -------------------------------------------------------------------------------- 1 | {"_type":"export","__export_format":4,"__export_date":"2020-10-18T10:56:05.677Z","__export_source":"insomnia.desktop.app:v2020.4.1","resources":[{"_id":"req_263adc298250431f8eac4589a1abb87a","parentId":"fld_1b52e2e9e9b1400d9a07a3996fa18e41","modified":1602953089713,"created":1601720190509,"url":"{{baseUrl}}:4000/graphql","name":"Me","description":"","method":"POST","body":{"mimeType":"application/graphql","text":"{\"query\":\"{\\n me {\\n id, \\n username\\n }\\n}\"}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_48bb6b0186ab4d32bc688c4ebf34e82c"}],"authentication":{},"metaSortKey":-1601827883143,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_1b52e2e9e9b1400d9a07a3996fa18e41","parentId":"wrk_1f91f2c3145d4b9d9742676d5c7092ac","modified":1601720179060,"created":1601720179060,"name":"Gateway","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1601720179060,"_type":"request_group"},{"_id":"wrk_1f91f2c3145d4b9d9742676d5c7092ac","parentId":null,"modified":1601659480455,"created":1601659480455,"name":"Federation GraphQL","description":"","scope":null,"_type":"workspace"},{"_id":"req_3914baff2d8a4b73b8a06d4a8a3aabdb","parentId":"fld_1b52e2e9e9b1400d9a07a3996fa18e41","modified":1601833567434,"created":1601827883093,"url":"{{baseUrl}}:4000/graphql","name":"Coffee","description":"","method":"POST","body":{"mimeType":"application/graphql","text":"{\"query\":\"{\\n coffee(id: \\\"5f7a0120007afe46000a2598\\\") {\\n name,\\n description\\n }\\n}\"}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_31fa72b4589b423481c833d95f7dcfb5"}],"authentication":{},"metaSortKey":-1601827883093,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_7495b94bc71a4a409f008b7f7c265677","parentId":"fld_1b52e2e9e9b1400d9a07a3996fa18e41","modified":1601833574737,"created":1601827852438,"url":"{{baseUrl}}:4000/graphql","name":"Coffees","description":"","method":"POST","body":{"mimeType":"application/graphql","text":"{\"query\":\"{\\n coffees {\\n id,\\n name,\\n description\\n }\\n}\"}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_1a9d880d63ee4f998f23af83817b2c49"}],"authentication":{},"metaSortKey":-1601827852438,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_eebae85260884fdc9ece9666c7240d24","parentId":"fld_1b52e2e9e9b1400d9a07a3996fa18e41","modified":1601838201768,"created":1601831226204,"url":"{{baseUrl}}:4000/graphql","name":"Coffees Mutation","description":"","method":"POST","body":{"mimeType":"application/graphql","text":"{\"query\":\"mutation {\\n createCoffee(input: {name: \\\"test1\\\", price: 1.5, imageUrl: \\\"http://google.com\\\"}) {\\n id\\n },\\n updateCoffee(input: {id: \\\"5f7a0120007afe46000a2598\\\", price: 10}) {\\n id, \\n price\\n },\\n deleteCoffee(id: \\\"5f7a014b002ccafd000a259a\\\") {\\n id\\n }\\n}\"}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_e5d283de6b834c85be9429b6a0803ebd"}],"authentication":{},"metaSortKey":-1601827852388,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_7d0f760538f042bca2669c01b3c7fb20","parentId":"fld_ceeaaa6d2f7c41b2943b3136804ea390","modified":1603017713935,"created":1603017625525,"url":"{{baseUrl}}:4001{{basePath}}/add-policy","name":"Add Policy","description":"","method":"GET","body":{},"parameters":[{"name":"subject","value":"customer","description":"","id":"pair_61cd7b7d48a54bc2b2a0825859bda37b"},{"name":"action","value":"read","description":"","id":"pair_e72b4d3ceda84fc580af6efa33a6e0fa"},{"name":"object","value":"coffee","description":"","id":"pair_314fe9aef690465d869d3a1aa3b9ce4d"}],"headers":[],"authentication":{},"metaSortKey":-1603017625525,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_ceeaaa6d2f7c41b2943b3136804ea390","parentId":"wrk_1f91f2c3145d4b9d9742676d5c7092ac","modified":1601818914643,"created":1601659499072,"name":"Accounts Service","description":"","environment":{"basePath":"/api/v1"},"environmentPropertyOrder":{"&":["basePath"]},"metaSortKey":-1601659499072,"_type":"request_group"},{"_id":"req_93ae620a795b4f0f87ccdcfde092c804","parentId":"fld_ceeaaa6d2f7c41b2943b3136804ea390","modified":1602953241562,"created":1602953176606,"url":"{{baseUrl}}:4001{{basePath}}/is-authorized","name":"Is Authorized","description":"","method":"GET","body":{},"parameters":[{"name":"subject","value":"5f8b1fc800df4c8c005edd7b","description":"","id":"pair_16e3329e4edf43698dd02a2eb163b3c9"},{"name":"action","value":"read","description":"","id":"pair_2a5dda44a31c423e90b584c98f4f32bb"},{"name":"object","value":"coffee","description":"","id":"pair_fa2f56d5a4e141efa58d7595177a9447"}],"headers":[],"authentication":{},"metaSortKey":-1602953176606,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_bb175c0f503d4f77893e25ba5e47ad59","parentId":"fld_ceeaaa6d2f7c41b2943b3136804ea390","modified":1601922472022,"created":1601822452133,"url":"{{baseUrl}}:4001/graphql","name":"User Info","description":"","method":"POST","body":{"mimeType":"application/graphql","text":"{\"query\":\"{\\n me {\\n id,\\n username\\n },\\n \\n _service {\\n sdl\\n }\\n}\"}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_9cb181ccb3cd4cf58ed42681a89d24bd"}],"authentication":{},"metaSortKey":-1601822452133,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_112edb4f37c943dea67b90e9a6b44d66","parentId":"fld_ceeaaa6d2f7c41b2943b3136804ea390","modified":1601822421324,"created":1601814866913,"url":"{{baseUrl}}:4001/openapi","name":"OpenAPI Spec","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1601814866913,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_9277e6678b0545b2ad9071ee10247b95","parentId":"fld_ceeaaa6d2f7c41b2943b3136804ea390","modified":1601822425556,"created":1601741506646,"url":"{{baseUrl}}:4001{{basePath}}/signup","name":"Signup","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"username\": \"test\",\n\t\"password\": \"test\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_14a62cbc58904edf9b8b59e3749c7ac7"}],"authentication":{},"metaSortKey":-1601741506646,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_51a1b06e4b924d1c935a43b7ef56657a","parentId":"fld_ceeaaa6d2f7c41b2943b3136804ea390","modified":1601822433253,"created":1601659493213,"url":"{{baseUrl}}:4001{{basePath}}/login","name":"Login","description":"","method":"POST","body":{"mimeType":"application/json","text":"{\n\t\"username\": \"test\",\n\t\"password\": \"test\"\n}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_c1ba0cc12bc3440fae884377177ddad6"}],"authentication":{},"metaSortKey":-1601659496142.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_2b178389248444fbbd42e7dccb6e68e5","parentId":"fld_ceeaaa6d2f7c41b2943b3136804ea390","modified":1601823930807,"created":1601823919047,"url":"{{baseUrl}}:4001{{basePath}}/logout","name":"Logout","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1601659496117.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_b512cb5435b24d36a8721ad3295bc7d8","parentId":"fld_ceeaaa6d2f7c41b2943b3136804ea390","modified":1601822437075,"created":1601744654720,"url":"{{baseUrl}}:4001{{basePath}}/user-info","name":"User Info","description":"","method":"GET","body":{},"parameters":[],"headers":[],"authentication":{},"metaSortKey":-1601659496092.5,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"req_6b77d1d49a4446a7a2b51717f591651e","parentId":"fld_7d1d59cbc6374309930349f5d035e358","modified":1601921268711,"created":1601829233587,"url":"{{baseUrl}}:4002/graphql","name":"Coffees Mutation","description":"","method":"POST","body":{"mimeType":"application/graphql","text":"{\"query\":\"mutation {\\n createCoffee(input: {name: \\\"test\\\", price: 1.5, imageUrl: \\\"http://google.com\\\"}) {\\n id\\n }\\n}\"}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_e5d283de6b834c85be9429b6a0803ebd"}],"authentication":{},"metaSortKey":-1601829233587,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"fld_7d1d59cbc6374309930349f5d035e358","parentId":"wrk_1f91f2c3145d4b9d9742676d5c7092ac","modified":1601829004227,"created":1601828992867,"name":"Products Service","description":"","environment":{},"environmentPropertyOrder":null,"metaSortKey":-1601659499022,"_type":"request_group"},{"_id":"req_e713adbc31d64cbc9734d6e26c9f1082","parentId":"fld_7d1d59cbc6374309930349f5d035e358","modified":1601839331772,"created":1601829020292,"url":"{{baseUrl}}:4002/graphql","name":"Coffees","description":"","method":"POST","body":{"mimeType":"application/graphql","text":"{\"query\":\"{\\n coffees {\\n name,\\n description\\n }\\n}\\n\"}"},"parameters":[],"headers":[{"name":"Content-Type","value":"application/json","id":"pair_690fe69ed7834f728a6b76a90fd7b2a7"}],"authentication":{},"metaSortKey":-1601829020292,"isPrivate":false,"settingStoreCookies":true,"settingSendCookies":true,"settingDisableRenderRequestBody":false,"settingEncodeUrl":true,"settingRebuildPath":true,"settingFollowRedirects":"global","_type":"request"},{"_id":"env_bb8eddb16d8535f250078defc387397fa4bd6023","parentId":"wrk_1f91f2c3145d4b9d9742676d5c7092ac","modified":1601659480682,"created":1601659480682,"name":"Base Environment","data":{},"dataPropertyOrder":null,"color":null,"isPrivate":false,"metaSortKey":1601659480682,"_type":"environment"},{"_id":"jar_bb8eddb16d8535f250078defc387397fa4bd6023","parentId":"wrk_1f91f2c3145d4b9d9742676d5c7092ac","modified":1602953166450,"created":1601659480685,"name":"Default Jar","cookies":[{"key":"actix-session","value":"hUlIzxePxpZFsxJ8b7Qh5QljuvWT2leWRNZgdwB5sBk=jo64W5qk1oxZNxTWgJleytemUfSiQRAM","maxAge":604800,"domain":"localhost","path":"/","httpOnly":true,"extensions":["SameSite=Lax"],"hostOnly":true,"creation":"2020-10-02T17:26:22.887Z","lastAccessed":"2020-10-17T16:46:06.449Z","id":"7822620620019536"}],"_type":"cookie_jar"},{"_id":"spc_598584891c1e4fc287d5067e6c9da12c","parentId":"wrk_1f91f2c3145d4b9d9742676d5c7092ac","modified":1601659480467,"created":1601659480467,"fileName":"Federation GraphQL","contents":"","contentType":"yaml","_type":"api_spec"},{"_id":"env_39e9c50eedff475190c8695882df0da5","parentId":"env_bb8eddb16d8535f250078defc387397fa4bd6023","modified":1601662315813,"created":1601662248891,"name":"Local Dev","data":{"baseUrl":"http://localhost"},"dataPropertyOrder":{"&":["baseUrl"]},"color":null,"isPrivate":false,"metaSortKey":1601662248891,"_type":"environment"}]} --------------------------------------------------------------------------------