├── .dockershell.sh ├── .editorconfig ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ ├── book.yml │ └── ci.yml ├── .gitignore ├── .versionrc.json ├── .vscode └── settings.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Cargo.toml ├── Dockerfile ├── LICENSE ├── README.md ├── bonnie.toml ├── docker-compose.yml ├── docs ├── .gitignore ├── book.toml └── src │ ├── SUMMARY.md │ ├── auth.md │ ├── config.md │ ├── core │ ├── auth.md │ ├── getting_started.md │ └── queries_mutations.md │ ├── getting_started.md │ ├── intro.md │ ├── serverless.md │ └── writing_schemas.md ├── examples ├── .env ├── create_jwt.rs └── schema │ ├── main.rs │ └── schema.rs ├── integrations ├── serverful │ └── actix-web │ │ ├── Cargo.toml │ │ ├── LICENSE │ │ ├── README.md │ │ ├── examples │ │ ├── server.rs │ │ └── subscriptions_server.rs │ │ ├── src │ │ ├── auth_middleware.rs │ │ ├── create_graphql_server.rs │ │ ├── create_subscriptions_server.rs │ │ ├── lib.rs │ │ └── routes.rs │ │ └── tests │ │ └── e2e.rs └── serverless │ └── aws-lambda │ ├── Cargo.toml │ ├── LICENSE │ ├── README.md │ ├── examples │ └── netlify │ │ ├── .gitignore │ │ ├── README.md │ │ ├── build.sh │ │ ├── functions │ │ └── netlify │ │ ├── main.rs │ │ ├── netlify.toml │ │ ├── public │ │ └── index.html │ │ └── rust-toolchain │ ├── src │ ├── lib.rs │ └── run_aws_req.rs │ └── tests │ ├── .gitignore │ └── e2e.rs ├── src ├── auth │ ├── auth_state.rs │ ├── core.rs │ ├── jwt.rs │ └── mod.rs ├── diana_handler.rs ├── errors.rs ├── graphql.rs ├── graphql_utils.rs ├── lib.rs ├── options.rs └── pubsub.rs └── tests ├── diana_handler.rs ├── jwt.rs └── options.rs /.dockershell.sh: -------------------------------------------------------------------------------- 1 | # Note: for this to do anything, use my starter Dockerfile config (https://gist.github.com/arctic-hen7/10987790b86360820e2790650e289f0b) 2 | 3 | # This file contains ZSH configuration for your shell when you interact with a container 4 | # (we wouldn't want any boring `sh` now would we?) 5 | # Please feel free to set up your own ZSH config in here! 6 | # It gets mapped to your `.zshrc` for the root user in the container 7 | 8 | # Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc. 9 | # Initialization code that may require console input (password prompts, [y/n] 10 | # confirmations, etc.) must go above this block; everything else may go below. 11 | if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then 12 | source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" 13 | fi 14 | 15 | # Source Antigen 16 | source ~/.antigen/antigen.zsh 17 | autoload -U colors && colors 18 | setopt promptsubst 19 | # Set up oh-my-zsh 20 | antigen use oh-my-zsh 21 | # Set up plugins 22 | antigen bundle git 23 | antigen bundle docker 24 | # Set up our preferred theme 25 | antigen theme cloud 26 | # Run all that config 27 | antigen apply 28 | 29 | # Set up Ctrl + Backspace and Ctrl + Del so you can move around and backspace faster (try it!) 30 | bindkey '^H' backward-kill-word 31 | bindkey -M emacs '^[[3;5~' kill-word 32 | 33 | # Set up aliases 34 | alias cl="clear" 35 | alias x="exit" 36 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This ensures your code style preferences are enforced no matter what editor you and your contributors use! 2 | # They'll need to install the Editorconfig extension in their editr of choice first though. 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 4 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | end_of_line = lf 13 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @arctic_hen7 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Let us know about a bug you found! 4 | title: "" 5 | labels: bug, triage 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **Minimum reproducible configuration** 13 | The simplest configuration that reproduces the bug. If it's complex, you should link to a repository here. 14 | 15 | **Expected behavior** 16 | A clear and concise description of what you expected to happen. 17 | 18 | **Actual behavior** 19 | A clear and concise description of what actually happened instead. 20 | 21 | **Runtime information** 22 | 23 | - OS: your OS 24 | - Diana version: the version you're running 25 | - Integrations used: what integrations, if any, are you using? what versions? 26 | 27 | **Additional context** 28 | Add any other context about the problem here. 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a new feature! 4 | title: "" 5 | labels: enhancement, triage 6 | assignees: "" 7 | --- 8 | 9 | **Description** 10 | A clear and concise description of what you want to add. 11 | 12 | **Reasoning** 13 | A clear and concise explanation of why your proposal would make Diana better. 14 | 15 | **Are you willing to work on an implementation of this?** 16 | Please complete. Please do not being work until the issue has been triaged (and the `triage` label removed)! 17 | -------------------------------------------------------------------------------- /.github/workflows/book.yml: -------------------------------------------------------------------------------- 1 | name: Compile and Deploy Book 2 | 3 | on: 4 | push: 5 | paths: 6 | - "docs/**" 7 | - ".github/workflows/book.yml" # If we change this build script, it should rerun 8 | 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Setup mdBook 15 | uses: peaceiris/actions-mdbook@v1 16 | with: 17 | mdbook-version: "latest" 18 | - name: Build book 19 | run: mdbook build 20 | working-directory: docs 21 | - name: Deploy book to GitHub Pages 22 | uses: peaceiris/actions-gh-pages@v3 23 | if: github.ref == 'refs/heads/main' 24 | with: 25 | github_token: ${{ secrets.GITHUB_TOKEN }} 26 | publish_dir: docs/book 27 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v2 10 | - name: Test Library 11 | run: cargo check --all && cargo fmt --all -- --check && cargo clippy --all && cargo test --all 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | netlify-config.json 3 | Cargo.lock 4 | -------------------------------------------------------------------------------- /.versionrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "types": [ 3 | { "type": "feat", "section": "Features" }, 4 | { "type": "fix", "section": "Bug Fixes" }, 5 | { "type": "docs", "section": "Documentation Changes" }, 6 | { "type": "perf", "section": "Performance Improvements" }, 7 | { "type": "refactor", "section": "Code Refactorings" }, 8 | { "type": "test", "section": "Tests", "hidden": true }, 9 | { "type": "build", "section": "Build System", "hidden": true }, 10 | { "type": "ci", "hidden": true }, 11 | 12 | { 13 | "type": "feat", 14 | "section": "Features", 15 | "hidden": false 16 | }, 17 | { 18 | "type": "fix", 19 | "section": "Bug Fixes", 20 | "hidden": false 21 | }, 22 | { 23 | "type": "chore", 24 | "section": "Chores", 25 | "hidden": true 26 | }, 27 | { 28 | "type": "docs", 29 | "section": "Documentation Changes", 30 | "hidden": false 31 | }, 32 | { 33 | "type": "style", 34 | "section": "Code Style Changes", 35 | "hidden": true 36 | }, 37 | { 38 | "type": "refactor", 39 | "section": "Code Refactorings", 40 | "hidden": true 41 | }, 42 | { 43 | "type": "perf", 44 | "section": "Performance Improvements", 45 | "hidden": false 46 | }, 47 | { 48 | "type": "test", 49 | "section": "Code Testing Changes", 50 | "hidden": true 51 | } 52 | ], 53 | "releaseCommitMessageFormat": "chore(release): 🔖 {{currentTag}}" 54 | } 55 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "conventionalCommits.scopes": [ 3 | "docker", 4 | "auth", 5 | "deps", 6 | "examples", 7 | "book", 8 | "docs" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [0.2.9](https://github.com/arctic-hen7/diana/compare/v0.2.8...v0.2.9) (2021-08-03) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **docs:** ✏️ fixed typo book links ([6e2f9f0](https://github.com/arctic-hen7/diana/commit/6e2f9f000625f903a07c8696c25dde5c9ad6cb9d)) 11 | 12 | ### [0.2.8](https://github.com/diana-graphql/diana/compare/v0.2.7...v0.2.8) (2021-07-11) 13 | 14 | 15 | ### Features 16 | 17 | * ✨ exposed auth types and added method to get token claims ([e9f66f5](https://github.com/diana-graphql/diana/commit/e9f66f51d8988ebba8cc7deeb139793286b20984)) 18 | 19 | ### [0.2.7](https://github.com/diana-graphql/diana/compare/v0.2.6...v0.2.7) (2021-07-11) 20 | 21 | 22 | ### Features 23 | 24 | * 🗑 added `is_authed!` to replace `if_authed!` ([35e8a8f](https://github.com/diana-graphql/diana/commit/35e8a8fa872d1e2a624f95beb5c3c1caa1ca0d93)) 25 | 26 | ### [0.2.6](https://github.com/diana-graphql/diana/compare/v0.2.5...v0.2.6) (2021-07-10) 27 | 28 | 29 | ### Bug Fixes 30 | 31 | * 🐛 allowed `if_authed` to work regardless of return result ([9d2fe98](https://github.com/diana-graphql/diana/commit/9d2fe986af5d6f9614f7856c9fd3ee01bcaac2df)) 32 | 33 | ### [0.2.6](https://github.com/diana-graphql/diana/compare/v0.2.5...v0.2.6) (2021-07-10) 34 | 35 | 36 | ### Bug Fixes 37 | 38 | * 🐛 allowed `if_authed` to work regardless of return result ([9d2fe98](https://github.com/diana-graphql/diana/commit/9d2fe986af5d6f9614f7856c9fd3ee01bcaac2df)) 39 | 40 | ### [0.2.5](https://github.com/diana-graphql/diana/compare/v0.2.4...v0.2.5) (2021-07-10) 41 | 42 | 43 | ### Features 44 | 45 | * ✨ added `Options::builder()` method ([23454d9](https://github.com/diana-graphql/diana/commit/23454d922f21113facd0809e62dae4af765c9cc6)), closes [#1](https://github.com/diana-graphql/diana/issues/1) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * 🚑 made `JWTSecret` `Clone`able ([61b8de6](https://github.com/diana-graphql/diana/commit/61b8de60f375e0821f5d9474202315e6233737d4)) 51 | 52 | ### [0.2.4](https://github.com/diana-graphql/diana/compare/v0.2.3...v0.2.4) (2021-07-10) 53 | 54 | 55 | ### Bug Fixes 56 | 57 | * 🚑 exposed hidden jwt types ([ff4a766](https://github.com/diana-graphql/diana/commit/ff4a76607184d11b47c2eb8c3ee06e5333b9dfa5)) 58 | 59 | 60 | ### Documentation Changes 61 | 62 | * **book:** 📝 updated book intro ([8234ff0](https://github.com/diana-graphql/diana/commit/8234ff00db4b1eec26d28482d883451a819b8b28)) 63 | * **book:** 📝 updated book to require installation of `async_graphql` directly ([2577738](https://github.com/diana-graphql/diana/commit/2577738552be068abc5c5d5e8d2f57d43119465a)) 64 | 65 | ### [0.2.3](https://github.com/diana-graphql/diana/compare/v0.2.2...v0.2.3) (2021-07-09) 66 | 67 | 68 | ### Documentation Changes 69 | 70 | * **book:** 📝 wrote book ([10739d5](https://github.com/diana-graphql/diana/commit/10739d5cb609ead61bb2f720253225df3e64c73e)) 71 | * 📝 updated readme ([45cca35](https://github.com/diana-graphql/diana/commit/45cca3553f43c6af3d037c7bb93e5ab03f12fb87)) 72 | 73 | ### [0.2.2](https://github.com/diana-graphql/diana/compare/v0.2.1...v0.2.2) (2021-07-02) 74 | 75 | 76 | ### Bug Fixes 77 | 78 | * 🐛 fixed type inference failure after `DianaHandler` changes ([a8a854c](https://github.com/diana-graphql/diana/commit/a8a854c96e808080f3a0e3fce4cb193acb609e04)) 79 | * 🐛 support binary bodies that can be serialized to strings in lambda integration ([b55ce88](https://github.com/diana-graphql/diana/commit/b55ce88dbbd69d4856ef96ad517fa9e2f7110dc5)) 80 | 81 | 82 | ### Code Refactorings 83 | 84 | * ♻️ made `DianaHandler.is_authed()` accept `Option>` ([c76aee0](https://github.com/diana-graphql/diana/commit/c76aee08a220dcef51fac94c1561afd16b3de732)) 85 | 86 | ### [0.2.1](https://github.com/diana-graphql/diana/compare/v0.2.0...v0.2.1) (2021-06-30) 87 | 88 | 89 | ### Bug Fixes 90 | 91 | * 🔧 fixed bonnie publish script ([a522249](https://github.com/diana-graphql/diana/commit/a522249576f0f29e476b27c1cec537301178d9df)) 92 | * **cargo:** 🔧 added diana versions to integration crates ([1373667](https://github.com/diana-graphql/diana/commit/1373667d6855bbcdc86961e7766002cb2545432c)) 93 | 94 | 95 | ### Code Refactorings 96 | 97 | * 🔧 split up a few bonnie scripts ([3056b89](https://github.com/diana-graphql/diana/commit/3056b89e08a9b1b83b5bd475bcd9a3783bad9172)) 98 | 99 | 100 | ### Documentation Changes 101 | 102 | * 📝 added readmes for integration crates ([cfdabda](https://github.com/diana-graphql/diana/commit/cfdabda4edbd0fe5e359130643fb07bbf73beb56)) 103 | * 📝 added versioning docs ([e265015](https://github.com/diana-graphql/diana/commit/e265015a7f7d7798daf16b3afc5e414ce26384fd)) 104 | 105 | ## [0.2.0](https://github.com/diana-graphql/diana/compare/v0.1.1...v0.2.0) (2021-06-30) 106 | 107 | 108 | ### ⚠ BREAKING CHANGES 109 | 110 | * renamed `AuthCheckBlockState` to `AuthBlockLevel` 111 | * modules now fully re-exported rather than electively 112 | * original serverless interface no longer supported 113 | * radical changes with new integrations model (see the book) 114 | 115 | ### Features 116 | 117 | * ✨ added integration for aws lambda and derivatives ([6b6ef32](https://github.com/diana-graphql/diana/commit/6b6ef324d2423617b78163846e9f7b16cb640e01)) 118 | * ✨ switched to integrations model with core logic ([40721eb](https://github.com/diana-graphql/diana/commit/40721eb2938d9b887437a28f9498981266d97ba5)) 119 | 120 | 121 | ### Code Refactorings 122 | 123 | * 🚚 refactored re-exports ([5ede923](https://github.com/diana-graphql/diana/commit/5ede9236d80b362da28bfade7e7ce4121b23bd0a)) 124 | * 🚚 renamed `AuthCheckBlockState` to `AuthBlockLevel` ([d34bfdd](https://github.com/diana-graphql/diana/commit/d34bfdd0af5d7566c0677827aba678db7b6e749c)) 125 | 126 | 127 | ### Documentation Changes 128 | 129 | * 📝 added documentation for integration crates ([99608c6](https://github.com/diana-graphql/diana/commit/99608c6a9e3fe0347617dbd13d0815ab5ac2e3d5)) 130 | * 📝 removed useless section of core crate docs ([6aedfac](https://github.com/diana-graphql/diana/commit/6aedfacd0334b792d6e2629d37414505aa32c91a)) 131 | * 📝 updated docs ([0e734a8](https://github.com/diana-graphql/diana/commit/0e734a852a127feb1542cd84cf66e3efa23cebaa)) 132 | * 📝 updated readme ([fef8ba6](https://github.com/diana-graphql/diana/commit/fef8ba638805286b90ada9dd740025ced83cf890)) 133 | 134 | ### [0.1.1](https://github.com/diana-graphql/diana/compare/v0.1.0...v0.1.1) (2021-06-28) 135 | 136 | 137 | ### Bug Fixes 138 | 139 | * 🔧 fixed incorrect compose target in playground ([84d926e](https://github.com/diana-graphql/diana/commit/84d926ea95756a6f77390d8799e755e5ccde7812)) 140 | * 🔧 updated crate name ([18ac391](https://github.com/diana-graphql/diana/commit/18ac3912d48b31e5b49c4819fb618ea1ab940a16)) 141 | 142 | 143 | ### Code Refactorings 144 | 145 | * 🚚 switched to workspace structure for examples ([ad330f2](https://github.com/diana-graphql/diana/commit/ad330f2abf5d5f14ad99fb5be6c39b316ae725ec)) 146 | 147 | 148 | ### Documentation Changes 149 | 150 | * ✏️ fixed typo in `if_authed` docs ([43c2fe4](https://github.com/diana-graphql/diana/commit/43c2fe4ad52a33ffd29ccde4c0315eb6cb013c8e)) 151 | * 📝 added mdbook basics ([ee475cd](https://github.com/diana-graphql/diana/commit/ee475cd601b7df09917dfba8676b502bc5565e8c)) 152 | * 📝 made trivial docs change to test book deployment ([76260da](https://github.com/diana-graphql/diana/commit/76260da8abc4a606afb55330908ac412d3f4477b)) 153 | * 📝 updated documentation examples ([d3477a2](https://github.com/diana-graphql/diana/commit/d3477a2f21d6c2b8756e77cbd71deac9e21597d6)) 154 | 155 | ## 0.1.0 (2021-06-27) 156 | 157 | 158 | ### Features 159 | 160 | * ✨ added aws-specific serverless function invoker ([2616733](https://github.com/arctic-hen7/diana/commit/26167331bae4bfb7afcbf8fbb84b2092a253aad4)) 161 | * ✨ added full serverless system ([96825bb](https://github.com/arctic-hen7/diana/commit/96825bbd501738684abbf40cc5f7da11d55bb221)) 162 | * ✨ added option to disable subscriptions server entirely ([234cd10](https://github.com/arctic-hen7/diana/commit/234cd10b5083751330ddbaf7e142a2e44482a298)) 163 | * ✨ modularised the query/mutation systems ([2a50470](https://github.com/arctic-hen7/diana/commit/2a50470109132c1b2a960f2b5f579842091e879e)) 164 | * ✨ modularised the subscriptions server ([a508e81](https://github.com/arctic-hen7/diana/commit/a508e812d8ba2ac07d9dd5699ba2cd458c48df1b)) 165 | * 🎉 imported code from elm-rust-boilerplate ([a1835f0](https://github.com/arctic-hen7/diana/commit/a1835f08b48abcf13ee157e51670f22b6d76c819)) 166 | * 🚧 added hello world serverless function ([60e608b](https://github.com/arctic-hen7/diana/commit/60e608b2ae15fed2716562f0490bfc2452522138)) 167 | * 🚧 set up basics for serverless setup ([c600f36](https://github.com/arctic-hen7/diana/commit/c600f36ef1416f0588b96077209231ada02524a7)) 168 | 169 | 170 | ### Bug Fixes 171 | 172 | * 🐛 fixed publisher error lockout ([36e1df4](https://github.com/arctic-hen7/diana/commit/36e1df41175246511ea6f262c18c7bae74767c94)) 173 | * 🥅 added error handling for tokio broadcasts ([9cb0fe6](https://github.com/arctic-hen7/diana/commit/9cb0fe61411ee09988435296b7c654a048c3240e)) 174 | 175 | 176 | ### Code Refactorings 177 | 178 | * ♻️ broke out serverless handler into separate function ([7ffa3ac](https://github.com/arctic-hen7/diana/commit/7ffa3ac77a55c1e57762febe5f8d6539175db05a)) 179 | * ♻️ broke out serverless handler into separate function ([c1a08fc](https://github.com/arctic-hen7/diana/commit/c1a08fc3d107ec4b27fe9575bd8381962293b629)) 180 | * ♻️ fixed convoluted error management and made publishes clearer ([c53aa8b](https://github.com/arctic-hen7/diana/commit/c53aa8b16cc5a65af92b9d2b4d185913c509bc44)) 181 | * 🚚 made gigantic infrastructure changes ([4525f04](https://github.com/arctic-hen7/diana/commit/4525f04398181c8d1c9065e3f41348f22a7e334b)) 182 | * 🚚 moved dev utilities into a separate sub-crate ([915ed72](https://github.com/arctic-hen7/diana/commit/915ed7229b5c85baef4054d70b3cd043fa2df12e)) 183 | 184 | 185 | ### Documentation Changes 186 | 187 | * 📝 added crate-level rustdoc ([2b2d9d8](https://github.com/arctic-hen7/diana/commit/2b2d9d8314d506c79875fddd7b6bdde7bb67ce64)) 188 | * 📝 documented everything with rustdoc ([938b4bd](https://github.com/arctic-hen7/diana/commit/938b4bdea0640941149929f840d404cd23269513)) 189 | * 📝 fixed rustdoc links ([594c04a](https://github.com/arctic-hen7/diana/commit/594c04a8207cc8f228205873da1351d6605114d1)) 190 | * 🔥 removed erroneous examples readme ([62e8a93](https://github.com/arctic-hen7/diana/commit/62e8a93d474d68b306ddc504148c73cf67e4539e)) 191 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | 2 | # Contributor Covenant Code of Conduct 3 | 4 | ## Our Pledge 5 | 6 | We as members, contributors, and leaders pledge to make participation in our 7 | community a harassment-free experience for everyone, regardless of age, body 8 | size, visible or invisible disability, ethnicity, sex characteristics, gender 9 | identity and expression, level of experience, education, socio-economic status, 10 | nationality, personal appearance, race, caste, color, religion, or sexual identity 11 | and orientation. 12 | 13 | We pledge to act and interact in ways that contribute to an open, welcoming, 14 | diverse, inclusive, and healthy community. 15 | 16 | ## Our Standards 17 | 18 | Examples of behavior that contributes to a positive environment for our 19 | community include: 20 | 21 | * Demonstrating empathy and kindness toward other people 22 | * Being respectful of differing opinions, viewpoints, and experiences 23 | * Giving and gracefully accepting constructive feedback 24 | * Accepting responsibility and apologizing to those affected by our mistakes, 25 | and learning from the experience 26 | * Focusing on what is best not just for us as individuals, but for the 27 | overall community 28 | 29 | Examples of unacceptable behavior include: 30 | 31 | * The use of sexualized language or imagery, and sexual attention or 32 | advances of any kind 33 | * Trolling, insulting or derogatory comments, and personal or political attacks 34 | * Public or private harassment 35 | * Publishing others' private information, such as a physical or email 36 | address, without their explicit permission 37 | * Other conduct which could reasonably be considered inappropriate in a 38 | professional setting 39 | 40 | ## Enforcement Responsibilities 41 | 42 | Community leaders are responsible for clarifying and enforcing our standards of 43 | acceptable behavior and will take appropriate and fair corrective action in 44 | response to any behavior that they deem inappropriate, threatening, offensive, 45 | or harmful. 46 | 47 | Community leaders have the right and responsibility to remove, edit, or reject 48 | comments, commits, code, wiki edits, issues, and other contributions that are 49 | not aligned to this Code of Conduct, and will communicate reasons for moderation 50 | decisions when appropriate. 51 | 52 | ## Scope 53 | 54 | This Code of Conduct applies within all community spaces, and also applies when 55 | an individual is officially representing the community in public spaces. 56 | Examples of representing our community include using an official e-mail address, 57 | posting via an official social media account, or acting as an appointed 58 | representative at an online or offline event. 59 | 60 | ## Enforcement 61 | 62 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 63 | reported to the community leaders responsible for enforcement at 64 | [arctic_hen7@pm.me](mailto:arctic_hen7@pm.me). 65 | All complaints will be reviewed and investigated promptly and fairly. 66 | 67 | All community leaders are obligated to respect the privacy and security of the 68 | reporter of any incident. 69 | 70 | ## Enforcement Guidelines 71 | 72 | Community leaders will follow these Community Impact Guidelines in determining 73 | the consequences for any action they deem in violation of this Code of Conduct: 74 | 75 | ### 1. Correction 76 | 77 | **Community Impact**: Use of inappropriate language or other behavior deemed 78 | unprofessional or unwelcome in the community. 79 | 80 | **Consequence**: A private, written warning from community leaders, providing 81 | clarity around the nature of the violation and an explanation of why the 82 | behavior was inappropriate. A public apology may be requested. 83 | 84 | ### 2. Warning 85 | 86 | **Community Impact**: A violation through a single incident or series 87 | of actions. 88 | 89 | **Consequence**: A warning with consequences for continued behavior. No 90 | interaction with the people involved, including unsolicited interaction with 91 | those enforcing the Code of Conduct, for a specified period of time. This 92 | includes avoiding interactions in community spaces as well as external channels 93 | like social media. Violating these terms may lead to a temporary or 94 | permanent ban. 95 | 96 | ### 3. Temporary Ban 97 | 98 | **Community Impact**: A serious violation of community standards, including 99 | sustained inappropriate behavior. 100 | 101 | **Consequence**: A temporary ban from any sort of interaction or public 102 | communication with the community for a specified period of time. No public or 103 | private interaction with the people involved, including unsolicited interaction 104 | with those enforcing the Code of Conduct, is allowed during this period. 105 | Violating these terms may lead to a permanent ban. 106 | 107 | ### 4. Permanent Ban 108 | 109 | **Community Impact**: Demonstrating a pattern of violation of community 110 | standards, including sustained inappropriate behavior, harassment of an 111 | individual, or aggression toward or disparagement of classes of individuals. 112 | 113 | **Consequence**: A permanent ban from any sort of public interaction within 114 | the community. 115 | 116 | ## Attribution 117 | 118 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 119 | version 2.0, available at 120 | [https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0]. 121 | 122 | Community Impact Guidelines were inspired by 123 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 124 | 125 | For answers to common questions about this code of conduct, see the FAQ at 126 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available 127 | at [https://www.contributor-covenant.org/translations][translations]. 128 | 129 | [homepage]: https://www.contributor-covenant.org 130 | [v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html 131 | [Mozilla CoC]: https://github.com/mozilla/diversity 132 | [FAQ]: https://www.contributor-covenant.org/faq 133 | [translations]: https://www.contributor-covenant.org/translations 134 | 135 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First off, thanks so much for taking the time to contribute to Diana, it's greatly appreciated! 4 | 5 | ## I just want to propose something 6 | 7 | If you just want to let us know about a bug, or propose a new feature, please open an issue on this repository. We'll take a look as soon as possible! 8 | 9 | If you have a question about Diana, open a new issue with the `question` tag and we'll get to it as soon as possible! 10 | 11 | ## What should I work on? 12 | 13 | You can check out the [roadmap](./README.md#Roadmap) or the [issues](https://github.com/arctic-hen7/diana/issues) to see what's currently needing to be done. If you want to work on something that's not on there, please file an issue and we'll take a look it as soon as possible! 14 | 15 | ## How do I contribute? 16 | 17 | Contributing to a project on Github is pretty straight forward. If this is you're first time contributing to a project, all you need to do is fork this repository to your own GitHub account, add then change the code you want to (usually on your local machine, you'd pull your fork down). Commit your changes as necessary, and when you're done, submit a pull request on this repository and we'll review it as soon as possible! 18 | 19 | Make sure your code doesn't break anything existing, that all tests pass, and, if necessary, add tests so your code can be confirmed to work automatically. 20 | 21 | After you've submitted a pull request, a maintainer will review your changes. Unfortunately, not every pull request will be merged, but we'll try to request changes so that your pull request can best be integrated into the project. 22 | 23 | ## Building and Testing 24 | 25 | - `cargo build` 26 | - `cargo test` 27 | 28 | Diana exposes three major components -- the dedicated subscriptions server, the serverful GraphQL system, and the serverless GraphQL system. The first two are what you'll most likely be working with, and these are run from within Docker. To work on Diana, you'll need both Docker and Docker Compose installed. 29 | 30 | There's a series of Bonnie scripts in the root of the project, and `bonnie up` will allow you to bring up the whole system (including a MongoDB database for testing). That will make all systems react to your changes, though they can be a bit slow to do so (re-compiling Rust takes a while), so you may want to open two terminals and then run `bonnie sh-server` in one of them and `bonnie sh-subscriptions-server` in the other. Those will give you fully-fledged ZSH prompts in the containers from which you can work. `cargo watch -w . -w ../lib -x "run"` will build the containers reactively and let you see errors and the like. 31 | 32 | If you're building something that's likely to impact serverless functionality, you'll need to test the deployment of the system on Netlify. Right now, Diana's serverless systems haven't been built, so instructions on how to do this will come once that's been done. 33 | 34 | ## Documentation 35 | 36 | If the code you write needs to be documented in the help page, the README, or elsewhere, please do so! Also, please ensure your code is commented, it makes everything so much easier for everyone. 37 | 38 | ## Committing 39 | 40 | We use the Conventional Commits system, but you can commit however you want. Your pull request will be squashed and merged into a single compliant commit, so don't worry about this! 41 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diana" 3 | description = "Diana is an out-of-the-box GraphQL system with full support for use as a serverless function, as well as commonly-used features like subscriptions and authentication." 4 | version = "0.2.9" 5 | authors = ["arctic_hen7 "] 6 | edition = "2018" 7 | license = "MIT" 8 | repository = "https://github.com/arctic-hen7/diana" 9 | homepage = "https://arctic-hen7.github.io/diana" 10 | keywords = ["graphql", "serverless", "authentication"] 11 | categories = ["web-programming", "web-programming::http-server", "web-programming::websocket"] 12 | include = [ 13 | "src/*", 14 | "Cargo.toml", 15 | "LICENSE", 16 | "README.md" 17 | ] 18 | 19 | [dependencies] 20 | serde = "1.0.103" 21 | serde_json = "1.0.44" 22 | serde_derive = "1.0.103" 23 | tokio = { version = "1.0.1", features = ["full"] } 24 | async-graphql = "2.8.2" 25 | reqwest = { version = "0.10.10", default-features = false, features = ["rustls-tls", "json"] } 26 | async-stream = "0.3.1" 27 | tokio-stream = "0.1.5" 28 | error-chain = "0.12.4" 29 | jsonwebtoken = "7.2.0" 30 | chrono = "0.4.19" 31 | 32 | [dev-dependencies] 33 | dotenv = "0.15.0" 34 | 35 | [lib] 36 | name = "diana" 37 | path = "src/lib.rs" 38 | 39 | # We pull in the integrations as workspace members, they're published as separate packages 40 | # Users shouldn't have to add code they don't want/need 41 | [workspace] 42 | members = [ 43 | "integrations/serverful/actix-web", 44 | "integrations/serverless/aws-lambda" 45 | ] 46 | 47 | # This is all optimisation for the release binaries, mainly targeted at the serverless example 48 | # Netlify unfortunately doesn't like 20MB executables 49 | # These drive up release compilation time significantly, but decrease binary size hugely 50 | # Also decreases binary speed, but that's okay for Netlify because otherwise we'd be writing in JS (which would be atrocious) 51 | # Unfortunately, we can't apply the `lto = true` optimisation because then Netlify doesn't recognise the bianry as a function 52 | [profile.release] 53 | opt-level = "z" 54 | codegen-units = 1 55 | panic = "abort" 56 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Setup Stage - set up the ZSH environment for optimal developer experience 2 | FROM node:14-alpine AS setup 3 | # Let scripts know we're running in Docker (useful for containerised development) 4 | ENV RUNNING_IN_DOCKER true 5 | # Use the unprivileged `node` user (pre-created by the Node image) for safety (and because it has permission to install modules) 6 | RUN mkdir -p /app \ 7 | && chown -R node:node /app 8 | # Set up ZSH and our preferred terminal environment for containers 9 | RUN apk --no-cache add zsh curl git 10 | RUN mkdir -p /home/node/.antigen 11 | RUN curl -L git.io/antigen > /home/node/.antigen/antigen.zsh 12 | # Use my starter Docker ZSH config file for this, or your own ZSH configuration file (https://gist.github.com/arctic-hen7/bbfcc3021f7592d2013ee70470fee60b) 13 | COPY .dockershell.sh /home/node/.zshrc 14 | RUN chown -R node:node /home/node/.antigen /home/node/.zshrc 15 | # Set up ZSH as the unprivileged user (we just need to start it, it'll initialise our setup itself) 16 | USER node 17 | RUN /bin/zsh /home/node/.zshrc 18 | # Switch back to root for whatever else we're doing 19 | USER root 20 | 21 | # Rust Setup Stage - install and set up Rust for development (used for backend) 22 | FROM setup AS rust-setup 23 | # Install the necessary system dependencies 24 | RUN apk add --no-cache build-base clang llvm gcc 25 | # Download and run the Rust installer, using the default options (needs to be done as the unprivileged user) 26 | USER node 27 | RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y 28 | # Install Cargo plugins 29 | # We have to use the absolute path to the Cargo binary because it isn't aliased in the Docker build process 30 | RUN /home/node/.cargo/bin/cargo install cargo-watch 31 | # Switch back to root for the remaining stages 32 | USER root 33 | 34 | # Dependencies Stage - install all system-level dependencies that won't change (before Rust caching because that gets constantly re-executed) 35 | FROM rust-setup AS dependencies 36 | # Install system dependencies 37 | USER root 38 | RUN apk add --no-cache openssl-dev 39 | # Install global dependencies with NPM 40 | # See https://answers.netlify.com/t/netlify-cli-fails-to-install/34508/3 for why we use `--unsafe-perm` 41 | RUN npm install -g --unsafe-perm netlify-cli 42 | 43 | # Rust Cacher Stage - caches all dependencies in the Rust code with `cargo vendor` to speed up builds massively 44 | # When your dependencies change, this will be re-executed, otherwise you get super-speed caching performance! 45 | FROM dependencies AS rust-cacher 46 | USER node 47 | RUN mkdir -p /app \ 48 | && chown -R node:node /app 49 | # Copy the Cargo configuration files into the correct place in the container 50 | # Note that we need to be able to write to Cargo.lock 51 | WORKDIR /app 52 | COPY --chown=node:node ./Cargo.lock Cargo.lock 53 | COPY ./Cargo.toml Cargo.toml 54 | # We also copy over all the manifests of all the integrations (workspace structure) 55 | COPY ./integrations/serverful/actix-web/Cargo.toml integrations/serverful/actix-web/Cargo.toml 56 | COPY ./integrations/serverless/aws-lambda/Cargo.toml integrations/serverless/aws-lambda/Cargo.toml 57 | # Vendor all dependencies (stores them all locally, meaning they can be cached) 58 | RUN mkdir -p /app/.cargo 59 | RUN chown -Rh node:node /app/.cargo 60 | RUN /home/node/.cargo/bin/cargo vendor > .cargo/config 61 | # Switch back to root for the remaining stages 62 | USER root 63 | 64 | # Base Stage - install system-level dependencies, disable telemetry, and copy files 65 | FROM rust-cacher AS base 66 | WORKDIR /app 67 | # Disable telemetry of various tools for privacy 68 | RUN yarn config set --home enableTelemetry 0 69 | # Copy the Netlify config file into the correct location 70 | # See `CONTRIBUTING.md` for how to set this up for the first time 71 | COPY --chown=node:node ./netlify-config.json /home/node/.config/netlify/config.json 72 | # Copy our source code into the container 73 | COPY . . 74 | 75 | # Playground stage - simple ZSH entrypoint for us to shell into the container as the non-root user for developing the main library 76 | FROM base AS playground 77 | USER node 78 | ENTRYPOINT [ "/bin/zsh" ] 79 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 arctic_hen7 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Diana

2 | 3 | > **Pragmatic GraphQL that just works.** 4 | 5 | [Book][book] • [Crate Page][crate] • [API Documentation][docs] • [Contributing][contrib] 6 | 7 | Diana is a GraphQL system for Rust that's designed to work as simply as possible out of the box, without sacrificing configuration ability. Unlike other GraphQL systems, Diana **fully supports serverless functions and automatically integrates them with a serverful subscriptions system** as needed, and over an authenticated channel. GraphQL subscriptions are state*ful*, and so have to be run in a server*ful* way. Diana makes this process as simple as possible. 8 | 9 | Diana's documentation can be found in [the book][book]. 10 | 11 | ## Installation 12 | 13 | Getting started with Diana is really easy! Just install it by adding this to your `Cargo.toml` file: 14 | 15 | ``` 16 | diana = "0.2.9" 17 | ``` 18 | 19 | Due to the complexity of its components, Diana does have a lot of dependencies, so you may want to go and have a cup of tea while you wait for the installation and everything to be compiled for the first time! 20 | 21 | Because of its structure, Diana needs you to run two servers in development. While it may be tempting to just combine these into one, this will not work whatsoever and it will blow up in your face (schema collisions)! You can either have two binaries or, using our recommended method, create a monorepo-style crate with two binary crates and a library crate to store your common logic (example in the book). 22 | 23 | All further documentation can be found in [the book][book], which was made with [mdBook](https://rust-lang.github.io/mdBook/index.html). 24 | 25 | ## Versioning 26 | 27 | Each Diana integration depends on the core library, so any change of the core library will result in a version change for an integration. That is also applied backwards in that any version change in an integration also results in a version change of the core and all other integrations. Essentially, the whole of Diana will always be at a certain version, the latest tag of this repository. 28 | 29 | When a new version is added, it will begin in `v0.1.0`. Once it moves to a stable release, what would otherwise be `v1.0.0`, it is immediately bumped to the same version as the rest of the Diana ecosystem. 30 | 31 | ## Stability 32 | 33 | Diana is under active development, and still requires the particular addition of support for authentication over WebSockets. The project will hopefully move to v1.0.0 by 2022! 34 | 35 | ## Credit to `async_graphql` 36 | 37 | [`async_graphql`](https://github.com/async-graphql/async-graphql) must be acknowledged as the primary dependency of Diana, as well as the biggest inspiration for the project. It is a fantastic GraphQL library for Rust, and if you want to go beyond the scope of Diana (which is more high-level), this should be your first port of call. Without it, Diana would not be possible at all. 38 | 39 | ## Why the name? 40 | 41 | _Diana_ is the Roman name for the Greek goddess _Artemis_, the sister of the god _Apollo_. [Apollo GraphQL](https://www.apollographql.com/) is a company that builds excellent GraphQL products (with which Diana is NOT in any way affiliated), so we may as well be in the same nominal family (if that's a thing). 42 | 43 | ## Roadmap 44 | 45 | - [ ] Support GraphiQL in production 46 | 47 | * [ ] Support authentication over WebSockets for subscriptions 48 | * [ ] Support GraphiQL over serverless 49 | 50 | ## Contributing 51 | 52 | If you want to make a contribution to Diana, that's great! Thanks so much! Contributing guidelines can be found [here](contrib), and please make sure you follow our [code of conduct](CODE_OF_CONDUCT.md). 53 | 54 | ## License 55 | 56 | See [`LICENSE`](./LICENSE). 57 | 58 | [book]: https://arctic-hen7.github.io/diana 59 | [crate]: https://crates.io/crates/diana 60 | [docs]: https://docs.rs/diana 61 | [contrib]: ./CONTRIBUTING.md 62 | -------------------------------------------------------------------------------- /bonnie.toml: -------------------------------------------------------------------------------- 1 | version = "0.3.1" 2 | 3 | [scripts] 4 | # Gets the ID of a running Docker container by its name (utility script) 5 | getcontainerid.cmd = "docker ps | awk -v containername=\"%containername\" '$0 ~ containername{print $1}'" 6 | getcontainerid.args = [ 7 | "containername" 8 | ] 9 | 10 | # Docker scripts 11 | dc = "docker-compose %%" 12 | rebuild = "bonnie dc up --build -d && bonnie dc down" # Use this when you change any Docker configurations or update Rust dependencies 13 | up = "bonnie dc up -d" 14 | end = "bonnie dc down" 15 | sh = "bonnie dc run --entrypoint \"/bin/zsh\" --use-aliases --rm playground" 16 | 17 | # General Rust scripts 18 | doc = "cargo doc" 19 | test = "cargo watch -x \"test\"" 20 | dev = "cargo watch -x \"check\"" 21 | check = "cargo check --all && cargo fmt --all -- --check && cargo clippy --all && cargo test --all" # This will be run on CI as well (ignoring expensive tests) 22 | example = { cmd = "cargo watch -x \"run --example %example_name\"", args = ["example_name"] } 23 | serverless-compile = "cd examples/netlify && sh build.sh" 24 | serverless-deploy = "bonnie serverless compile && netlify deploy --prod" 25 | 26 | # Releases the project to GitHub (doesn't publish the crates) 27 | release = "standard-version --sign --commit-all && git push --follow-tags origin main" 28 | publish-core = "cargo publish" 29 | publish-integrations = "cd integrations/serverful/actix-web && cargo publish && cd ../../serverless/aws-lambda && cargo publish" 30 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | playground: 4 | image: diana.playground 5 | container_name: diana.playground 6 | build: 7 | context: ./ 8 | target: playground 9 | network_mode: host # So we don't have to deal with port management 10 | volumes: 11 | - type: bind 12 | source: ./ 13 | target: /app 14 | stdin_open: true 15 | tty: true 16 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | book 2 | -------------------------------------------------------------------------------- /docs/book.toml: -------------------------------------------------------------------------------- 1 | [book] 2 | authors = ["arctic_hen7"] 3 | language = "en" 4 | multilingual = false 5 | src = "src" 6 | title = "Diana Book" 7 | -------------------------------------------------------------------------------- /docs/src/SUMMARY.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [Introduction](./intro.md) 4 | - [Getting Started](./getting_started.md) 5 | - [Writing Schemas](./writing_schemas.md) 6 | - [Configuration](./config.md) 7 | - [Authentication](./auth.md) 8 | - [Going Serverless](./serverless.md) 9 | - [Core Library](./core/getting_started.md) 10 | - [Handling Queries and Mutations](./core/queries_mutations.md) 11 | - [Authentication](./core/auth.md) 12 | -------------------------------------------------------------------------------- /docs/src/auth.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | Authentication is built into Diana out of the box using JWTs. It's designed to be as intuitive as possible, but there are a few things you should know when working with it. 4 | 5 | > 🚧 Authentication is not yet supported over subscriptions, this will be added soon! 🚧 6 | 7 | ## Authentication Block Level 8 | 9 | In your configuration, you define a required level of authentication for your GraphQL endpoints using `.auth_block_state()`. The different levels are explained on [the configuration page](./config.md), so all that will be added now is that they apply to all GraphQL endpoints, from both the queries/mutations and the subscriptions systems. `BlockUnauthenticated` is vastly preferred and recommended in production. 10 | 11 | ## JWTs 12 | 13 | Diana has full support for JWTs out of the box, and uses them internally to allow connections between its two systems. That means that you will need to create a JWT to enable this communication, which can be done using `diana::create_jwt`! Diana provides a few function for managing JWTs: `create_jwt`, `validate_and_decode_jwt`, `get_jwt_secret`, and `decode_time_str`. Those are all pretty self-explanatory except perhaps the last one, which turns strings like `1w` into one week from the present datetime in seconds after January 1st 1970 (Unix epoch), allowing you to more conveniently define JWT expiries. This is based on Vercel's [ms](https://github.com/vercel/ms) module for JavaScript, though only implements a subset of its features. 14 | 15 | The documentation for those functions is best seen directly in raw form [here](https://docs.rs/diana). The most important thing to know is that the JWT for connecting to the subscriptions server MUST define the `role` property in its payload to be `graphql_server`. Otherwise authentication will fail for `BlockUnauthenticated` and `AllowMissing`. 16 | 17 | ## GraphiQL 18 | 19 | GraphiQL is currently only supported in development (it will be disabled by force in production), and so there is as yet no need for authenticating for access to it. If and when it is usable in production, this will come with an authentication system for it. 20 | 21 | In development, you may need to provide a JWT that you've generated in order to test authentication. You can do this by opening the *Headers* panel at the bottom of the screen and typing the following: 22 | 23 | ```json 24 | { 25 | "Authorization": "Bearer YOUR_TOKEN_HERE" 26 | } 27 | ``` 28 | 29 | Please note that authentication is not yet supported for subscriptions, and so this will have no effect on them (equivalent to a permanent `AllowAll`). 30 | -------------------------------------------------------------------------------- /docs/src/config.md: -------------------------------------------------------------------------------- 1 | # Configuration 2 | 3 | Diana is configured using the `Options` struct. This page will go through in detail what can be specified using that system. Here's an example options initialization that we'll work through: 4 | 5 | ```rust 6 | Options::builder() 7 | .ctx(Context("test".to_string())) 8 | .subscriptions_server_hostname("http://localhost") 9 | .subscriptions_server_port("9002") 10 | .subscriptions_server_endpoint("/graphql") 11 | .jwt_to_connect_to_subscriptions_server( 12 | &env::var("SUBSCRIPTIONS_SERVER_PUBLISH_JWT").unwrap(), 13 | ) 14 | .auth_block_state(AuthBlockLevel::AllowAll) 15 | .jwt_secret(&env::var("JWT_SECRET").unwrap()) 16 | .schema(Query {}, Mutation {}, Subscription {}) 17 | .graphql_endpoint("/graphql") 18 | .playground_endpoint("/graphiql") 19 | .finish() 20 | .expect("Failed to build options!") 21 | ``` 22 | 23 | ## Context 24 | 25 | You must provide a context struct to Diana by using the `.ctx()` function. This struct will be parsed to all resolvers, and can be trivially accessed by using this in your resolvers: 26 | 27 | ```rust 28 | let ctx = raw_ctx.data()?; 29 | ``` 30 | 31 | A common use of the context struct is for a database pool. 32 | 33 | ## Subscriptions server configuration 34 | 35 | You need to provide the details of the subscriptions server in your configuration so the queries/mutation system knows where it is on the internet. This is defined using these four functions: 36 | 37 | - `.subscriptions_server_hostname()` -- the hostname of the subscriptions server (e.g. `http://localhost` 38 | - `.subscriptions_server_port()` -- the port the subscriptions server is running on 39 | - `.subscriptions_server_endpoint()` -- the GraphQL endpoint to connect to on the subscriptions server (e.g. `/graphql`) 40 | - `.jwt_to_connect_to_subscriptions_server()` -- a JWT to use to authenticate against the subscriptions server, which must be signed with the secret define by `.jwt_secret()`; this JWT must have a payload which defines `role: "graphql_server"` (see [Authentication](./auth.md)) 41 | 42 | If you aren't using subscriptions at all in your setup, you don't have to use any of these functions. 43 | 44 | ## Authentication 45 | 46 | Two properties define authentication data for Diana: `.jwt_secret()` and `.auth_block_state()`. The former defines the string secret to use to sign all JWTs (internally used for the communication channel between the two systems of Diana, you can use it too for authenticating clients). The latter defines the level of authentication required to connect to the GraphQL endpoint. This can be one of the following: 47 | 48 | - `AuthBlockLevel::AllowAll` -- allows everything, only ever use this in development unless you have an excellent reason 49 | - `AuthBlockLevel::BlockUnauthenticated` -- blocks anything without a valid JWT 50 | - `AuthBlockLevel::AllowMissing` -- blocks invalid tokens, but allows requests without tokens; this is designed for development use to show authentication while also allowing GraphiQL introspection (the hints and error messages like an IDE); do NOT use this in production! 51 | 52 | ## Endpoints 53 | 54 | The two functions `.graphql_endpoint()` and `.playground_endpoint` define the locations of your GraphQL endpoint and the endpoint for the GraphiQL playground, though you probably won't use them unless you're using something novel, they are set to `/graphql` and `/graphiql` respectively by default. 55 | 56 | ## Schema 57 | 58 | The last function is `.schema()`, which defines the actual schema for your app. You'll need to provide your `Query`, `Mutation` and `Subscription` types here. If you're not using subscriptions, you can use `diana::async_graphql::EmptySubscription` instead. There's also an `EmptyMutation` type if you need it. At least one query is mandatory. You should initialize each of these structs for this function with this notation: 59 | 60 | ``` 61 | Query {} 62 | ``` 63 | 64 | ## Building it 65 | 66 | Once you've run all those functions, you can build the `Options` by using `.finish()`, which will return a `Result`. Because you can't do anything at all without the options defined properly, it's typical to run `.expect()` after this to `panic!` quickly if the configuration couldn't be built. 67 | -------------------------------------------------------------------------------- /docs/src/core/auth.md: -------------------------------------------------------------------------------- 1 | # Authentication 2 | 3 | If you're not using any middleware, you can entirely ignore this page and get on with building your custom system, but if you want to authenticate users more efficiently, this is for you. 4 | 5 | `DianaHandler` has the function `.is_authed()` that you can call in middleware, parsing in a raw authentication header just as you would if you were [handling queries and mutations](./queries_mutations.md) without middleware. That will return an [`AuthVerdict`](https://docs.rs/diana/0.2.9/diana/enum.AuthVerdict.html), which tells you if the client is allowed, blocked, or if an error occurred. Typically, you would continue the request on `Allow`, return a 403 on `Block`, and return a 500 on `Error` (though this could be caused by a bad request, it occurs in the context of the server). In future, a distinction may be made between server and client caused errors, which would allow reasonable returning of a 400 in some cases, but that's not yet implemented. 6 | 7 | After you have an `AuthVerdict`, you can send that to your final handler in some way (Actix Web uses request extensions) and then extract it there to provide to `run_stateless_without_subscriptions()` or `.run_stateless_for_subscriptions`. If you do that, you don't need to provide the raw authentication header, as it won't be used, but you still can. 8 | -------------------------------------------------------------------------------- /docs/src/core/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Diana Core 2 | 3 | Diana is built for use with integrations, but if you want to support a platform without an integration, you'll need to work with Diana core. This shouldn't be too daunting, as it's designed to work as well as possible with queries and mutations in particular. Subscriptions are not yet well supported in Diana Core, and we strongly advise using the [diana-actix-web](https://crates.io/crates/diana-actix-web) integration for your subscriptions server. 4 | 5 | Diana core is just the `diana` package, which you should already have installed from [Getting Started](../getting_started.md). 6 | 7 | This guide is designed to be as generic as possible, and it may be useful to have some perspective on how to actually build an integration, for which you should look to the [Actix Web integration](https://github.com/arctic-hen7/diana/tree/main/integrations/serverful/actix-web/src). That folder also contains examples of using `async_graphql` and its integrations more directly to support subscriptions (which is how you would probably do it if you were building your own integration). 8 | 9 | Finally, if you build a fully-fledged integration for a serverful or serverless platform, please [submit a pull request](https://github.com/arctic-hen7/diana/pulls/new) to get it into the Diana codebase! We'd really appreciate your contribution! You can see our contributing guidelines [here](https://github.com/arctic-hen7/diana/tree/main/CONTRIBUTING.md) 10 | -------------------------------------------------------------------------------- /docs/src/core/queries_mutations.md: -------------------------------------------------------------------------------- 1 | # Handling Queries and Mutations 2 | 3 | The main struct you'll be dealing with here is [`DianaHandler`](https://docs.rs/diana/0.2.9/diana/struct.DianaHandler.html), and the API documentation for Diana is your friend here. 4 | 5 | You can create a new `DianaHandler` by running `DianaHandler::new()` and providing it the `Options` you're using for your setup. That will automatically create schemas internally for queries/mutation and subscriptions. The two are mutually exclusive. 6 | 7 | ## Running a request 8 | 9 | There are two functions you can use for running queries and mutations: `.run_stateless_for_subscriptions()` and `.run_stateless_without_subscriptions()`. The first uses the schema for the subscriptions system, which would be used basically only for running the internally used `publish` mutation. The latter is used for running the user's queries. If you're building for an unsupported platform, you'll need to support both if you want to support subscriptions. 10 | 11 | Both functions take the same arguments because they do the same thing, just with different schemas. First, they both take a string request body, which is NOT the query the user wrote! Rather, that should be the stringified JSON body that contains fields for the `query`, `variables`, etc. If you make that mistake, you'll get some very strange errors about schema validity no matter what you do! 12 | 13 | The second argument is an `Option` of a string authentication header, which should be the raw value extracted from the HTTP `Authorization` header (which is where JWTs will be given). Do NOT try to pre-parse this in any way, even resolving it to a string, that will all be handled internally. 14 | 15 | The third and final argument is an optional authentication verdict, which can be given to force the handling process to not run any authentication checks on the given token, but rather to use a predetermined verdict. This allows the use of authentication middleware to arrive at a verdict before all the HTTP data has been streamed in (more efficient). You can learn more about this [here](./auth.md). If you're not using middleware (not recommended unless you really can't), you should provide `None` here. 16 | -------------------------------------------------------------------------------- /docs/src/getting_started.md: -------------------------------------------------------------------------------- 1 | # Getting Started 2 | 3 | Diana is a high-level wrapper around [async_graphql](https://crates.io/crates/async-graphql), and is designed to be as easy as possible to get started with! This page is a basic tutorial of how to get started with a full and working setup. 4 | 5 | ## Installation 6 | 7 | Assuming you have Rust already installed (if not, [here's](https://www.rust-lang.org/tools/install) a guide on how to do so), you can add Diana as a dependency to your project easily by adding the following to your project's `Cargo.toml` under `[dependencies]`: 8 | 9 | ``` 10 | diana = "0.2.9" 11 | async-graphql = "2.8.2" 12 | ``` 13 | 14 | We also install `async_graphql` directly to prevent errors with asynchronous usage. Now run `cargo build` to download all dependencies. Diana is large and complex, so this will take quite a while! 15 | 16 | If you're new to GraphQL, we highly recommend reading more about it before diving further into Diana. You can see more about it on the [official GraphQL website](https://graphql.org). 17 | 18 | ## Project Structure 19 | 20 | We recommend a specific project structure for new projects using Diana, but it's entirely optional! It is however designed to minimize code duplication and maximize efficiency by allowing you to run both the queries/mutations system and the subscriptions server simultaneously. 21 | 22 | Critically, we recommend having three binary crates for your two servers and the serverless function, as well as a library crate for your schemas and configurations. These should all be Cargo workspaces. 23 | 24 | ``` 25 | lib/ 26 | src/ 27 | lib.rs 28 | Cargo.toml 29 | server/ 30 | src/ 31 | main.rs 32 | Cargo.toml 33 | serverless/ 34 | src/ 35 | main.rs 36 | Cargo.toml 37 | subscriptions/ 38 | src/ 39 | main.rs 40 | Cargo.toml 41 | Cargo.lock 42 | Cargo.toml 43 | ``` 44 | 45 | Set this up for now if possible, and we'll add to it later across the book (it will be assumed that you're using this or something similar). 46 | 47 | You should also have the following in your root `Cargo.toml` to set up workspaces (which you can read more about [here](https://doc.rust-lang.org/book/ch14-03-cargo-workspaces.html)): 48 | 49 | ```toml 50 | [workspace] 51 | 52 | members = [ 53 | "lib", 54 | "server", 55 | "serverless", 56 | "subscriptions" 57 | ] 58 | ``` 59 | 60 | Then you can make all the binary crates (everything except `lib`) dependent on your shared logic by adding this to the `Cargo.toml` files in `server`, `serverless`, and `subscriptions` under `[dependencies]`: 61 | 62 | ```toml 63 | lib = { path = "../lib" } 64 | ``` 65 | 66 | You can then reference `lib` in those crates as if it were just another external module! 67 | 68 | ## Your first schema 69 | 70 | If you're familiar with GraphQL, then the first thing you'll need to do to set up Diana is to write a basic schema. Diana depends entirely on [async_graphql](https://crates.io/crates/async-graphql) for this, so [their documentation](https://async-graphql.github.io) may also help you (particularly in more advanced cases), though this book should be enough for the simple stuff. 71 | 72 | Your first schema can be really simple, we'll just make a simple query that reports the API version when queried (we won't add any mutations or subscriptions for now). Try adding this somewhere in your shared logic: 73 | 74 | ```rust 75 | use diana::{ 76 | async_graphql::{ 77 | Object as GQLObject 78 | } 79 | } 80 | 81 | #[derive(Default, Clone)] 82 | pub struct Query {} 83 | #[GQLObject] 84 | impl Query { 85 | async fn api_version(&self) -> &str { 86 | "0.1.0" 87 | } 88 | } 89 | ``` 90 | 91 | This is probably the simplest schema you'll ever create! Crucially though, you MUST derive the `Default` and `Clone` traits on it. The former is required by `async_graphql`, and the latter for Diana. 92 | 93 | Hopefully you can see that our `Query` object is simply defining one query, `api_version`, which just returns `0.1.0`, the version of our API! Conveniently, `async_graphql` automatically parses this into the more conventional `apiVersion` when we call this, so you can conform to Rust and GraphQL conventions at the same time! 94 | 95 | ## Your first options 96 | 97 | Every part of Diana is configured using the `Options` struct, which can be created with `Options::builder()`. For now, we'll set up a simple configuration without any subscriptions support. Add this to your shared logic: 98 | 99 | ```rust 100 | use diana::{Options, AuthBlockState}; 101 | use diana::async_graphql::{EmptyMutation, EmptySubscription}; 102 | use crate::Query; // Or wherever you put your `Query` object from the previous section 103 | 104 | #[derive(Clone)] 105 | pub struct Context(String); 106 | 107 | pub fn get_opts() -> Options { 108 | Options::builder() 109 | .ctx(Context("test".to_string())) 110 | .auth_block_state(AuthBlockLevel::AllowAll) 111 | .jwt_secret("this is a secret") 112 | .schema(Query {}, Mutation {}, Subscription {}) 113 | .finish() 114 | .expect("Failed to build options!") 115 | } 116 | ``` 117 | 118 | Notice that we define a `Context` struct here. This will get passed around to every GraphQL resolver and you'll always be able to access it. As long as it's `Clone`able, you can put anything in here safely. A common use-case of this in reality would be as a database connection pool. Here, we just define it with a random string inside. 119 | 120 | Next, we define a function `get_opts()` that initializes our `Options`. We set the context, define our schema, and do two other things that need some explaining. The first is `.auth_block_state()`, which sets the required authentication level to access our GraphQL endpoint. Diana has authentication built-in, so this is fundamental. Here, we allow anything, authenticated or not, for educational purposes. In a production app, set this to block everything! You can read more about authentication [here](./auth.md). The second thing that needs explaining is `.jwt_secret()`. Diana's authentication systems is based on JWTs, which are basically tokens that clients send to servers to prove their identity (the server signed them earlier, and so can verify them). JWTs need a secret to be based on, and we define a very silly one here. In a production app, you should read this from an environment variable and it should be randomly generated (more on that [here](./auth.md)). 121 | 122 | ## Your first server 123 | 124 | Let's try plugging this into a basic Diana server! Diana is based around integrations for different platforms, and it currently supports only Actix Web for serverful systems, so that's what we'll use! You should add this to your `Cargo.toml` in the `server` crate under `[dependencies]`: 125 | 126 | ```toml 127 | diana-actix-web = "0.2.9" 128 | ``` 129 | 130 | Now add the following to your `main.rs` in the `server` crate: 131 | 132 | ```rust 133 | use diana_actix_web::{ 134 | actix_web::{App, HttpServer}, 135 | create_graphql_server, 136 | }; 137 | use diana::async_graphql::{EmptyMutation, EmptySubscription}; 138 | use lib::{get_opts, Query} 139 | 140 | #[diana_actix_web::actix_web::main] 141 | async fn main() -> std::io::Result<()> { 142 | let configurer = create_graphql_server(get_opts()).expect("Failed to set up configurer!"); 143 | 144 | HttpServer::new(move || App::new().configure(configurer.clone())) 145 | .bind("0.0.0.0:9000")? 146 | .run() 147 | .await 148 | } 149 | ``` 150 | 151 | Firstly, we're pulling in the dependencies we need, including the schema and the function to get our `Options`. Then, we define an asynchronous `main` function marked as the entrypoint for `actix_web`, in which we set up our entire GraphQL server using `create_graphql-server()`, parsing in our `Options`. After that, we start up a new Actix Web server, using `.configure()` to configure the entire thing. Pretty convenient, huh? 152 | 153 | If you also have some REST endpoints or the like, you can easily add them to this server as well, `.configure()` is inbuilt into Actix Web to enable this kind of modularization. 154 | 155 | ## Firing it up 156 | 157 | The last step of all this is to actually run your server! Go into the `server` crate and run `cargo run` to see it in action! If all has gone well, you should see be able to see the GraphiQL playground (GUI for GraphQL development) in your browser at ! Try typing in the following and then run it! 158 | 159 | ``` 160 | query { 161 | apiVersion 162 | } 163 | ``` 164 | 165 | You should see `0.1.0` faithfully printed on the right-hand side of the screen. 166 | 167 | Congratulations! You've just set up your first GraphQL server with Diana! The rest of this book will help you to understand how to extend this setup to include mutations, subscriptions, and authentication, as well as helping you to deploy it all serverlessly! 168 | -------------------------------------------------------------------------------- /docs/src/intro.md: -------------------------------------------------------------------------------- 1 | # Introduction 2 | 3 | [Diana][repo] • [Crate Page][crate] • [API Documentation][docs] • [Contributing][contrib] 4 | 5 | Welcome to the Diana book, the central location for all Diana documentation! This is designed to be read in order, but each page should also be self-containing for later reference. If you find an issue with this documentation or you'd like to contribute in any way to the project, please [open an issue](https://github.com/arctic-hen7/diana/issues/new/choose) and let us know! 6 | 7 | If you're looking for a more general overview of the project, please see the [README](https://github.com/arctic-hen7/diana). 8 | 9 | [repo]: https://github.com/arctic-hen7/diana 10 | [crate]: https://crates.io/crates/diana 11 | [docs]: https://docs.rs/diana 12 | [contrib]: ./CONTRIBUTING.md 13 | -------------------------------------------------------------------------------- /docs/src/serverless.md: -------------------------------------------------------------------------------- 1 | # Going Serverless 2 | 3 | Diana's most unique feature is its ability to bridge the serverless and serverful gap all in one system, and it's about time we covered how to use the serverless system! As for serverful systems, Diana uses integrations to support different serverless platforms. Currently, the only integration is for AWS Lambda and its derivatives, like Netlify and Vercel, so that's what we'll use here! 4 | 5 | Crucially, **no part of your schemas or options** should have to change to go serverless, it should be simply a different way of using them. 6 | 7 | ## Coding it 8 | 9 | First off, install `diana-aws-lambda` by adding the following to your `Cargo.toml` for the `serverless` crate under `[dependencies]` (notice that versions of integrations and the core library are kept in sync deliberately): 10 | 11 | ```toml 12 | diana-aws-lambda = "0.2.9" 13 | ``` 14 | 15 | ```rust 16 | use diana_aws_lambda::{ 17 | netlify_lambda_http::{ 18 | lambda, lambda::Context as LambdaCtx, IntoResponse as IntoLambdaResponse, 19 | Request as LambdaRequest, 20 | }, 21 | run_aws_req, AwsError, 22 | }; 23 | use lib::get_opts; 24 | 25 | #[lambda(http)] 26 | #[tokio::main] 27 | async fn main(req: LambdaRequest, _: LambdaCtx) -> Result { 28 | let res = run_aws_req(req, get_opts()).await?; 29 | Ok(res) 30 | } 31 | ``` 32 | 33 | This example also expects you to have [tokio](https://crates.io/crates/tokio) installed, you'll need a version above v1.0.0 for the runtime to work. 34 | 35 | The serverless system is quite a bit simpler than the serverful system actually, because it just runs a query/mutation directly, without any need to run a server for a longer period. This handler is now entirely complete. 36 | 37 | One thing to remember that could easily stump you for a while is environment variables. if you're reading from an environment variable file in your configuration setup, don't do that when you're in the serverless environment! And don't forget to add your environment variables to the serverless provider so they're available to your code! 38 | 39 | ## Deploying it 40 | 41 | This page will only cover deploying this to [Netlify](https://netlify.com), since that's arguably the most convenient service to set up for Rust serverless functions quickly right now. The rest of this section will assume you have a Netlify account and that you've installed the Netlify CLI. The process is however relatively similar for other services. 42 | 43 | Firstly, you'll need to set up a few basic things for Netlify deployment to work. Create a file named `rust-toolchain` in the `serverless` crate at its root (next to `Cargo.toml`). Then put the following in that file: 44 | 45 | ```toml 46 | [toolchain] 47 | channel = "stable" 48 | components = ["rustfmt", "clippy"] 49 | targets = ["x86_64-unknown-linux-musl"] 50 | ``` 51 | 52 | This tells Netlify how to prepare the environment for Rust. Next, you'll need some static files to deploy as a website (you may already have a frontend to use, otherwise just a basic `inde.html` is fine). Put these in a new directory in the `serverless` crate called `public`. Also create another new empty directory called `functions` next to it. 53 | 54 | Now we'll create a basic Netlify configuration. Create a `netlify.toml` file in the root of the `serverless` crate and put the following in it: 55 | 56 | ```toml 57 | [build] 58 | publish = "public" 59 | functions = "functions" 60 | ``` 61 | 62 | This tells Netlify where your static files and functions are. But we haven't actually got any compiled functions yet, so we'll set those up now! Your final function will be the compiled executable of your code in `src/main.rs`. 63 | 64 | Now we'll create a build script to prepare your function automatically. Create a new file in the `serverless` crate called `build.sh` and fill it with the following: 65 | 66 | ```bash 67 | #!/bin/bash 68 | 69 | cargo build --release --target x86_64-unknown-linux-musl 70 | cp ./target/x86_64-unknown-linux-musl/release/serverless functions 71 | ``` 72 | 73 | This will compile your binary for production and copy it to the `functions` directory, where Netlify can access it. Note that we're compiling for the `x86_64-unknown-linux-musl` target triple, which is the environment on Netlify's servers. To be able to compile for that target (a variant of Linux), you'll need to add it with `rustup target add x86_64-unknown-linux-musl`, which will download what you need. 74 | 75 | There's one more thing we have to do before we can deploy though, and that's minimizing the size of the binary. Rust by default creates very large binaries, optimizing for speed instead. Diana is large and complex, which exacerbates this problem. Netlify does not like large binaries. At all. Which means we need to slim our release binary down significantly. However, because Netlify support for Rust is in beta, certain very powerful optimizations (like running `strip` to halve the size) will result in Netlify being unable to even detect your binary. Add the following to your _root_ `Cargo.toml` (the one for all your crates): 76 | 77 | ```toml 78 | [profile.release] 79 | opt-level = "z" 80 | codegen-units = 1 81 | panic = "abort" 82 | ``` 83 | 84 | This changes the compiler to optimize for size rather than speed, removes extra unnecessary optimization code, and removes the entire panic handling matrix. What this means is that your binary becomes smaller, which is great! However, if your program happens to `panic!` in production, it will just abort, so if you have _any_ custom panic handling logic, you'll need to play around with this a bit. Netlify will generally accept binaries under 15MB. Now there are more optimizations we could apply here to make the binary tiny, but then Netlify can't even detect it, so this is the best we can do (if you have something else that works better, please [open an issue](https://github.com/arctic-hen7/diana/issues/new)). 85 | 86 | Finally, you can run `sh build.sh` to build your function! Now we just need to send it to Netlify! 87 | 88 | 1. Log in to Netlify from the terminal with `netlify login`. 89 | 2. Create a new site for this project for manual deployment with `netlify init --manual` (run this in `serverless`). 90 | 3. Deploy to production with `netlify deploy --prod`! 91 | 92 | You should now be able to query your live production GraphQL serverless function with any GraphQL or HTTP client! If you're having problems, Netlify's docunmentation may help, and don't forget to look at your site's logs! 93 | -------------------------------------------------------------------------------- /docs/src/writing_schemas.md: -------------------------------------------------------------------------------- 1 | # Writing Schemas 2 | 3 | The most critical part of GraphQL is schemas, and they're easily the most complex part of Diana. This page will explain how to write schemas that work with Diana, and it will explain how to get subscriptions to work properly, however it will not explain how to write basic schemas from scratch. For that, please refer to [async_graphql's documentation](https://async-graphql.github.io). 4 | 5 | ## Subscriptions 6 | 7 | A basic Diana subscription would look something like this: 8 | 9 | ```rust 10 | use diana::async_graphql::{Subscription as GQLSubscription, Context as GQLCtx}; 11 | use diana::errors::GQLResult; 12 | use diana::stream; 13 | 14 | #[derive(Default, Clone)] 15 | pub struct Subscription; 16 | #[GQLSubscription] 17 | impl Subscription { 18 | async fn new_blahs( 19 | &self, 20 | raw_ctx: &GQLCtx<'_>, 21 | ) -> impl Stream> { 22 | let stream_result = get_stream_for_channel_from_ctx("channel_name", raw_ctx); 23 | 24 | stream! { 25 | let stream = stream_result?; 26 | for await message in stream { 27 | yield Ok(message); 28 | } 29 | } 30 | } 31 | } 32 | ``` 33 | 34 | All this does is sets up a subscription that will return the strings on a particular channel. And this shows perfectly how subscriptions in Diana work -- channels. You publish something on a channel from the queries/mutations system and then receive it as above. You can then use the re-exported `stream!` macro to return a stream for it. 35 | 36 | Note that if you're trying to send a struct across channels you'll need to serialize/deserialize it into/out of a string for transport. However, as subscriptions can return errors in their streams, this shouldn't be a problem! 37 | 38 | ## Mutations that link with subscriptions 39 | 40 | The most common thing to trigger a subscription is some kind of mutation on the queries/mutations system, and so Diana provides a simple programmatic way of publishing data on a particular channel: 41 | 42 | ```rust 43 | use diana::async_graphql::{Subscription as GQLSubscription, Context as GQLCtx}; 44 | use diana::errors::GQLResult; 45 | use diana::stream; 46 | use diana::Publisher; 47 | 48 | #[derive(Default, Clone)] 49 | pub struct Mutation {} 50 | #[GQLObject] 51 | impl Mutation { 52 | async fn update_blah( 53 | &self, 54 | raw_ctx: &async_graphql::Context<'_>, 55 | ) -> GQLResult { 56 | let publisher = raw_ctx.data::()?; 57 | publisher.publish("channel_name", "important message").await?; 58 | Ok(true) 59 | } 60 | } 61 | ``` 62 | 63 | In the above example, we get a `Publisher` out of the GraphQL context (it's automatically injected), and we use it to easily send a message to the subscriptions server on the `channel_name` channel. Our subscription from the previous example would pick this up and stream it to the client. 64 | 65 | ## Linking other services to subscriptions 66 | 67 | Of course, it's entirely possible that services well beyond GraphQL may need to trigger a subscription message, and so you can easily push a message from anywhere where you can execute a basic HTTP request. Diana's subscriptions server has an inbuilt mutation `publish`, which takes a channel to publish on and a string message to publish. This can be called over a simple HTTP request from anywhere. However, this endpoint requires authentication, and you must have a valid JWT signed with the secret you've provided to be able to access it. 68 | -------------------------------------------------------------------------------- /examples/.env: -------------------------------------------------------------------------------- 1 | # Environment variables for the examples 2 | # This is deliberately checked into version control because it's just for demonstration purposes 3 | # Obviously change secret values in production and don't check them into version control! 4 | 5 | JWT_SECRET=thisisaterriblesecretthatshouldberandomlygeneratedseethebook 6 | # This is valid for the above secret until June 28th 2121 (I'll update it then) 7 | SUBSCRIPTIONS_SERVER_PUBLISH_JWT=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJleHAiOjQ3Nzg0NDUxMDEsImNsYWltcyI6eyJyb2xlIjoiZ3JhcGhxbF9zZXJ2ZXIifX0.er6fyfVI-lZR4XZPxyvEoC-0gce0axoZSL99sk9ZObwDEt9YHiEtlBLne_sfYahoyRX0wApuUKjcmoLxFpcM8w 8 | -------------------------------------------------------------------------------- /examples/create_jwt.rs: -------------------------------------------------------------------------------- 1 | // This example illustrates how to generate a JWT 2 | // This method is the same to create a JWT to connect to the subscriptions server as it is to generate one for a user 3 | 4 | use diana::{create_jwt, decode_time_str, get_jwt_secret}; 5 | use std::collections::HashMap; 6 | use std::env; 7 | 8 | fn main() { 9 | dotenv::from_filename("examples/.env").expect("Failed to load environment variables!"); 10 | let secret = 11 | get_jwt_secret(env::var("JWT_SECRET").unwrap()).expect("Couldn't parse JWT secret!"); 12 | 13 | let mut claims: HashMap = HashMap::new(); 14 | claims.insert("role".to_string(), "graphql_server".to_string()); // Role must be 'graphql_server' for connecting to the subscriptions server 15 | let jwt = create_jwt( 16 | claims, 17 | &secret, 18 | decode_time_str("1w").unwrap(), // This token will be valid for one week 19 | ) 20 | .expect("Couldn't create JWT!"); 21 | 22 | println!("{}", jwt); 23 | } 24 | -------------------------------------------------------------------------------- /examples/schema/main.rs: -------------------------------------------------------------------------------- 1 | // The example is all in `schema.rs`, and that's what the other examples pull from, so there's nothing that needs to be in here 2 | 3 | fn main() {} 4 | -------------------------------------------------------------------------------- /examples/schema/schema.rs: -------------------------------------------------------------------------------- 1 | // This file just defines the common schema and options that everything else will use 2 | // All examples import it dirtily by using the `include!` macro, which you should never use unless you have a very good reason to! 3 | 4 | use diana::{ 5 | Options, AuthBlockLevel, 6 | async_graphql::{ 7 | Object as GQLObject, Subscription as GQLSubscription, 8 | SimpleObject as GQLSimpleObject, 9 | }, 10 | errors::GQLResult, 11 | Stream, stream, 12 | graphql_utils::get_stream_for_channel_from_ctx, 13 | Publisher 14 | }; 15 | use std::env; 16 | use serde::{Serialize, Deserialize}; 17 | 18 | #[derive(GQLSimpleObject, Serialize, Deserialize)] 19 | pub struct User { 20 | username: String 21 | } 22 | 23 | #[derive(Default, Clone)] 24 | pub struct Query {} 25 | #[GQLObject] 26 | impl Query { 27 | async fn api_version(&self) -> &str { 28 | "0.1.0" 29 | } 30 | } 31 | #[derive(Default, Clone)] 32 | pub struct Mutation {} 33 | #[GQLObject] 34 | impl Mutation { 35 | // This only exists to illustrate sending data to the subscriptions server 36 | async fn update_blah( 37 | &self, 38 | raw_ctx: &async_graphql::Context<'_>, 39 | ) -> GQLResult { 40 | // Imagine we've acquired this from the user's input 41 | let user = User { 42 | username: "This is a username".to_string() 43 | }; 44 | // Stringify and publish the data to the subscriptions server 45 | let publisher = raw_ctx.data::()?; 46 | let user_json = serde_json::to_string(&user)?; 47 | publisher.publish("new_blah", user_json).await?; 48 | Ok(true) 49 | } 50 | } 51 | #[derive(Default, Clone)] 52 | pub struct Subscription; 53 | #[GQLSubscription] 54 | impl Subscription { 55 | async fn new_blahs( 56 | &self, 57 | raw_ctx: &async_graphql::Context<'_>, 58 | ) -> impl Stream> { 59 | // Get a direct stream from the context on a certain channel 60 | let stream_result = get_stream_for_channel_from_ctx("new_blah", raw_ctx); 61 | 62 | // We can manipulate the stream using the stream macro from async-stream 63 | stream! { 64 | let stream = stream_result?; 65 | for await message in stream { 66 | // Serialise the data as a user 67 | let new_user: User = serde_json::from_str(&message).map_err(|_err| "couldn't serialize given data correctly".to_string())?; 68 | yield Ok(new_user); 69 | } 70 | } 71 | } 72 | } 73 | 74 | #[derive(Clone)] 75 | pub struct Context { 76 | pub pool: String // This might be an actual database pool 77 | } 78 | 79 | pub fn get_opts() -> Options { 80 | // Load the environment variable file if we're not in Netlify 81 | // If we are, the variables should be directly available 82 | if env::var("NETLIFY").is_err() { 83 | dotenv::from_filename("examples/.env").expect("Failed to load environment variables!"); 84 | } 85 | 86 | Options::builder() 87 | .ctx(Context { 88 | pool: "connection".to_string(), 89 | }) 90 | .subscriptions_server_hostname("http://localhost") 91 | .subscriptions_server_port("9002") 92 | .subscriptions_server_endpoint("/graphql") 93 | .jwt_to_connect_to_subscriptions_server( 94 | &env::var("SUBSCRIPTIONS_SERVER_PUBLISH_JWT").unwrap(), 95 | ) 96 | .auth_block_state(AuthBlockLevel::AllowAll) 97 | .jwt_secret(&env::var("JWT_SECRET").unwrap()) 98 | .schema(Query {}, Mutation {}, Subscription {}) 99 | // Endpoints are set up as `/graphql` and `/graphiql` automatically 100 | .finish() 101 | .expect("Failed to build options!") 102 | } 103 | -------------------------------------------------------------------------------- /integrations/serverful/actix-web/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diana-actix-web" 3 | description = "The integration between Diana GraphQL and Actix Web." 4 | version = "0.2.9" 5 | authors = ["arctic_hen7 "] 6 | edition = "2018" 7 | license = "MIT" 8 | repository = "https://github.com/arctic-hen7/diana" 9 | homepage = "https://arctic-hen7.github.io" 10 | keywords = ["graphql", "serverless", "authentication"] 11 | categories = ["web-programming", "web-programming::http-server", "web-programming::websocket"] 12 | include = [ 13 | "src/*", 14 | "Cargo.toml", 15 | "LICENSE", 16 | "README.md" 17 | ] 18 | 19 | [dependencies] 20 | diana = { path = "../../../", version = "=0.2.9" } 21 | serde = "1.0.103" 22 | serde_json = "1.0.44" 23 | futures = "0.3.14" 24 | async-graphql = "2.8.2" 25 | async-graphql-actix-web = "2.8.2" 26 | actix-web = "3.3.2" 27 | 28 | [dev-dependencies] 29 | dotenv = "0.15.0" 30 | tokio = "1.3.0" 31 | reqwest = { version = "0.10.10", default-features = false, features = ["rustls-tls", "json"] } 32 | tungstenite = { version = "0.13.0", features = ["rustls-tls"] } 33 | actix-rt = "1.1.1" 34 | 35 | [lib] 36 | name = "diana_actix_web" 37 | path = "src/lib.rs" 38 | -------------------------------------------------------------------------------- /integrations/serverful/actix-web/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 arctic_hen7 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. -------------------------------------------------------------------------------- /integrations/serverful/actix-web/README.md: -------------------------------------------------------------------------------- 1 | # Diana Integration for Actix Web 2 | 3 | This is [Diana's](https://arctic-hen7.github.io/diana) integration crate for Actix Web, which enables the easy deployment of a Diana system 4 | on that platform. For more information, see [the documentation for Diana](https://github.com/arctic-hen7/diana) and 5 | [the book](https://arctic-hen7.github.io/diana). 6 | -------------------------------------------------------------------------------- /integrations/serverful/actix-web/examples/server.rs: -------------------------------------------------------------------------------- 1 | // This example illustrates how to set up a queries/mutations server for development 2 | // Note that this is literally identical to the `subscriptions_server.rs` example, just using `create_graphql_server` instead and a different port 3 | 4 | #![forbid(unsafe_code)] 5 | 6 | use diana_actix_web::{ 7 | actix_web::{App, HttpServer}, 8 | create_graphql_server, 9 | }; 10 | 11 | // This 'dirty-imports' the code in `schema.in` 12 | // It will literally be interpolated here 13 | // Never use this in production unless you have a fantastic reason! Just import your code through Cargo! 14 | // We do this here though because you can't import from another example (which is annoying) 15 | include!("../../../../examples/schema/schema.rs"); 16 | 17 | #[actix_web::main] 18 | async fn main() -> std::io::Result<()> { 19 | let configurer = create_graphql_server(get_opts()).expect("Failed to set up configurer!"); 20 | 21 | HttpServer::new(move || App::new().configure(configurer.clone())) 22 | .bind("0.0.0.0:9001")? 23 | .run() 24 | .await 25 | } 26 | -------------------------------------------------------------------------------- /integrations/serverful/actix-web/examples/subscriptions_server.rs: -------------------------------------------------------------------------------- 1 | // This example illustrates how to set up a subscriptions server for production (no serverless functions for subscriptions) 2 | // Note that this is literally identical to the `server.rs` example, just using `create_subscriptions_server` instead and a different port 3 | 4 | #![forbid(unsafe_code)] 5 | 6 | use diana_actix_web::{ 7 | actix_web::{App, HttpServer}, 8 | create_subscriptions_server, 9 | }; 10 | 11 | // This 'dirty-imports' the code in `schema.in` 12 | // It will literally be interpolated here 13 | // Never use this in production unless you have a fantastic reason! Just import your code through Cargo! 14 | // We do this here though because you can't import from another example (which is annoying) 15 | include!("../../../../examples/schema/schema.rs"); 16 | 17 | #[actix_web::main] 18 | async fn main() -> std::io::Result<()> { 19 | let configurer = create_subscriptions_server(get_opts()).expect("Failed to set up configurer!"); 20 | 21 | HttpServer::new(move || App::new().configure(configurer.clone())) 22 | .bind("0.0.0.0:9002")? 23 | .run() 24 | .await 25 | } 26 | -------------------------------------------------------------------------------- /integrations/serverful/actix-web/src/auth_middleware.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | dev::{Service, ServiceRequest, ServiceResponse, Transform}, 3 | Error, HttpMessage, HttpResponse, 4 | }; 5 | use async_graphql::{ObjectType, SubscriptionType}; 6 | use diana::{AuthVerdict, DianaHandler}; 7 | use futures::{ 8 | future::{ok, Ready}, 9 | Future, 10 | }; 11 | use std::any::Any; 12 | use std::pin::Pin; 13 | use std::result::Result as StdResult; 14 | use std::task::{Context, Poll}; 15 | 16 | // Create a factory for authentication middleware 17 | #[derive(Clone)] 18 | pub struct AuthCheck 19 | where 20 | C: Any + Send + Sync + Clone, 21 | Q: Clone + ObjectType + 'static, 22 | M: Clone + ObjectType + 'static, 23 | S: Clone + SubscriptionType + 'static, 24 | { 25 | diana_handler: DianaHandler, 26 | } 27 | impl AuthCheck 28 | where 29 | C: Any + Send + Sync + Clone, 30 | Q: Clone + ObjectType + 'static, 31 | M: Clone + ObjectType + 'static, 32 | S: Clone + SubscriptionType + 'static, 33 | { 34 | // Initialises a new instance of the authentication middleware factory by cloning the given DianaHandler 35 | pub fn new(diana_handler: &DianaHandler) -> Self { 36 | Self { 37 | diana_handler: diana_handler.clone(), 38 | } 39 | } 40 | } 41 | 42 | // This is what we'll actually call, all it does is create the middleware and define all its properties 43 | impl Transform for AuthCheck 44 | where 45 | C: Any + Send + Sync + Clone, 46 | Q: Clone + ObjectType + 'static, 47 | M: Clone + ObjectType + 'static, 48 | Sb: Clone + SubscriptionType + 'static, 49 | S: Service, 50 | S::Future: 'static, 51 | { 52 | // All the properties of the middleware need to be defined here 53 | // We could do this with `wrap_fn` instead, but this approach gives far greater control 54 | type Request = ServiceRequest; 55 | type Response = ServiceResponse; 56 | type Error = Error; 57 | type InitError = (); 58 | type Transform = AuthCheckMiddleware; 59 | type Future = Ready>; 60 | 61 | // This will be called internally by Actix Web to create our middleware 62 | // All this really does is pass the service itself (handler basically) over to our middleware (along with additional metadata) 63 | fn new_transform(&self, service: S) -> Self::Future { 64 | ok(AuthCheckMiddleware { 65 | diana_handler: self.diana_handler.clone(), 66 | service, 67 | }) 68 | } 69 | } 70 | 71 | // The actual middleware 72 | #[derive(Clone)] 73 | pub struct AuthCheckMiddleware 74 | where 75 | C: Any + Send + Sync + Clone, 76 | Q: Clone + ObjectType + 'static, 77 | M: Clone + ObjectType + 'static, 78 | Sb: Clone + SubscriptionType + 'static, 79 | { 80 | diana_handler: DianaHandler, 81 | service: S, 82 | } 83 | 84 | impl Service for AuthCheckMiddleware 85 | where 86 | C: Any + Send + Sync + Clone, 87 | Q: Clone + ObjectType + 'static, 88 | M: Clone + ObjectType + 'static, 89 | Sb: Clone + SubscriptionType + 'static, 90 | S: Service, 91 | S::Future: 'static, 92 | { 93 | // More properties for Actix Web 94 | type Request = ServiceRequest; 95 | type Response = ServiceResponse; 96 | type Error = Error; 97 | type Future = Pin>>>; 98 | 99 | // Stock function for asynchronous operations 100 | // The context here has nothing to do with our app's internal context whatsoever! 101 | fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { 102 | self.service.poll_ready(cx) 103 | } 104 | 105 | fn call(&mut self, req: ServiceRequest) -> Self::Future { 106 | // Get the HTTP `Authorization` header 107 | let auth_header = req 108 | .headers() 109 | .get("AUTHORIZATION") 110 | .map(|auth_header| { 111 | // We convert to a string and handle the result, which gives us an Option inside an Option 112 | let header_str = auth_header.to_str(); 113 | match header_str { 114 | Ok(header_str) => Some(header_str), 115 | Err(_) => None, 116 | } 117 | }) 118 | .flatten(); // Then we flatten the two Options into one Option 119 | // Get a verdict and match that to a middleware outcome 120 | let verdict = self.diana_handler.is_authed(auth_header); 121 | match verdict { 122 | auth_verdict @ AuthVerdict::Allow(_) => { 123 | // Insert the authentication verdict into the request extensions for later retrieval 124 | req.extensions_mut().insert(auth_verdict); 125 | // Move on from this middleware to the handler 126 | let fut = self.service.call(req); 127 | Box::pin(async move { 128 | let res = fut.await?; 129 | Ok(res) 130 | }) 131 | } 132 | AuthVerdict::Block => { 133 | // Return a 403 134 | Box::pin(async move { 135 | Ok(ServiceResponse::new( 136 | req.into_parts().0, // Eliminates the payload of the request 137 | HttpResponse::Unauthorized().finish(), // In the playground this will come up as bad JSON, it's a direct HTTP response 138 | )) 139 | }) 140 | } 141 | AuthVerdict::Error(_) => { 142 | // Middleware failed, we shouldn't let this proceed to the request just in case 143 | // This error could be triggered by a failure in transforming the token from base64, meaning the error can be caused forcefully by an attacker 144 | // In that scenario, we can't allow the bypassing of this layer 145 | Box::pin(async move { 146 | Ok(ServiceResponse::new( 147 | req.into_parts().0, // Eliminates the payload of the request 148 | HttpResponse::InternalServerError().finish(), 149 | )) 150 | }) 151 | } 152 | } 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /integrations/serverful/actix-web/src/create_graphql_server.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | guard, 3 | web::{self, ServiceConfig}, 4 | HttpResponse, 5 | }; 6 | use async_graphql::{ 7 | http::{playground_source, GraphQLPlaygroundConfig}, 8 | ObjectType, SubscriptionType, 9 | }; 10 | use diana::{errors::*, AuthBlockLevel, DianaHandler, Options}; 11 | use std::any::Any; 12 | 13 | use crate::auth_middleware::AuthCheck; 14 | use crate::routes::graphql_without_subscriptions; 15 | 16 | /// Creates a new server for queries and mutations. This returns a closure that can be used with Actix Web's `.configure()` function to 17 | /// quickly configure a new or existing Actix Web server to use Diana. For examples, see the book. 18 | /// This function is designed for development only, Diana should be used serverlessly for queries and mutations in a production environment. 19 | /// See the book for more information on how to do that. 20 | pub fn create_graphql_server( 21 | opts: Options, 22 | ) -> Result 23 | where 24 | C: Any + Send + Sync + Clone, 25 | Q: Clone + ObjectType + 'static, 26 | M: Clone + ObjectType + 'static, 27 | S: Clone + SubscriptionType + 'static, 28 | { 29 | // Create a new Diana handler (core logic primitive) 30 | let diana_handler = DianaHandler::new(opts.clone())?; 31 | 32 | // Get the appropriate authentication middleware set up with the JWT secret 33 | // This will wrap the GraphQL endpoint itself 34 | let auth_middleware = match opts.authentication_block_state { 35 | AuthBlockLevel::AllowAll => AuthCheck::new(&diana_handler), 36 | AuthBlockLevel::AllowMissing => AuthCheck::new(&diana_handler), 37 | AuthBlockLevel::BlockUnauthenticated => AuthCheck::new(&diana_handler), 38 | }; 39 | 40 | let graphql_endpoint = opts.graphql_endpoint; 41 | let playground_endpoint = opts.playground_endpoint; 42 | 43 | // Actix Web allows us to configure apps with `.configure()`, which is what the user will do 44 | // Now we create the closure that will configure the user's app to support a GraphQL server 45 | let configurer = move |cfg: &mut ServiceConfig| { 46 | // Add everything except for the playground endpoint (which may not even exist) 47 | cfg.data(diana_handler.clone()) // Clone the full DianaHandler we got before and provide it here 48 | // The primary GraphQL endpoint for queries and mutations 49 | .service( 50 | web::resource(&graphql_endpoint) 51 | .guard(guard::Post()) // Should accept POST requests 52 | .wrap(auth_middleware.clone()) 53 | .to(graphql_without_subscriptions::), // The handler function it should use 54 | ); 55 | 56 | // Define the closure for the GraphiQL endpoint 57 | // We don't do this in `routes` because of annoying type annotations 58 | let graphql_endpoint_for_closure = graphql_endpoint; // We need this because `move` 59 | let graphiql_closure = move || { 60 | HttpResponse::Ok() 61 | .content_type("text/html; charset=utf-8") 62 | .body(playground_source( 63 | GraphQLPlaygroundConfig::new(&graphql_endpoint_for_closure) 64 | .subscription_endpoint(&graphql_endpoint_for_closure), 65 | )) 66 | }; 67 | 68 | // Set up the endpoint for the GraphQL playground 69 | match playground_endpoint { 70 | // If we're in development and it's enabled, set it up without authentication 71 | Some(playground_endpoint) if cfg!(debug_assertions) => { 72 | cfg.service( 73 | web::resource(playground_endpoint) 74 | .guard(guard::Get()) 75 | .to(graphiql_closure), // The playground needs to know where to send its queries 76 | ); 77 | } 78 | // This shouldn't be possible (playground in production), see `.finish()` in `options.rs` 79 | Some(_) => (), 80 | None => (), 81 | }; 82 | // This closure works entirely with side effects, so we don't need to return anything here 83 | }; 84 | 85 | Ok(configurer) 86 | } 87 | -------------------------------------------------------------------------------- /integrations/serverful/actix-web/src/create_subscriptions_server.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{ 2 | guard, 3 | web::{self, ServiceConfig}, 4 | HttpResponse, 5 | }; 6 | use async_graphql::{ 7 | http::{playground_source, GraphQLPlaygroundConfig}, 8 | ObjectType, SubscriptionType, 9 | }; 10 | use diana::{errors::*, AuthBlockLevel, DianaHandler, Options}; 11 | use std::any::Any; 12 | 13 | use crate::auth_middleware::AuthCheck; 14 | use crate::routes::{graphql_for_subscriptions, graphql_ws}; 15 | 16 | /// Creates a new subscriptions server. This returns a closure that can be used with Actix Web's `.configure()` function to quickly configure 17 | /// a new or existing Actix Web server to use Diana. For examples, see the book. This function should be used to create production servers. 18 | /// If your setup doesn't require subscriptions at all, don't configure anything in the [`Options`](diana::Options) and don't worry 19 | /// about this function, subscriptions will automatically be disabled. 20 | pub fn create_subscriptions_server( 21 | opts: Options, 22 | ) -> Result 23 | where 24 | C: Any + Send + Sync + Clone, 25 | Q: Clone + ObjectType + 'static, 26 | M: Clone + ObjectType + 'static, 27 | S: Clone + SubscriptionType + 'static, 28 | { 29 | // Create a new Diana handler (core logic primitive) 30 | let diana_handler = DianaHandler::new(opts.clone())?; 31 | 32 | // Get the appropriate authentication middleware set up with the JWT secret 33 | // This will wrap the GraphQL endpoint itself 34 | let auth_middleware = match opts.authentication_block_state { 35 | AuthBlockLevel::AllowAll => AuthCheck::new(&diana_handler), 36 | AuthBlockLevel::AllowMissing => AuthCheck::new(&diana_handler), 37 | AuthBlockLevel::BlockUnauthenticated => AuthCheck::new(&diana_handler), 38 | }; 39 | 40 | let graphql_endpoint = opts.graphql_endpoint; 41 | let playground_endpoint = opts.playground_endpoint; 42 | 43 | // Actix Web allows us to configure apps with `.configure()`, which is what the user will do 44 | // Now we create the closure that will configure the user's app to support a GraphQL server 45 | let configurer = move |cfg: &mut ServiceConfig| { 46 | // Add everything except for the playground endpoint (which may not even exist) 47 | cfg.data(diana_handler.clone()) // Clone the full DianaHandler we got before and provide it here 48 | // The primary GraphQL endpoint for queries and mutations 49 | .service( 50 | web::resource(&graphql_endpoint) 51 | .guard(guard::Post()) // Should accept POST requests 52 | .wrap(auth_middleware.clone()) 53 | .to(graphql_for_subscriptions::), // The handler function it should use 54 | ) 55 | // The GraphQL endpoint for subscriptions over WebSockets 56 | .service( 57 | web::resource(&graphql_endpoint) 58 | .guard(guard::Get()) 59 | .guard(guard::Header("upgrade", "websocket")) 60 | .to(graphql_ws::), 61 | ); 62 | 63 | // Define the closure for the GraphiQL endpoint 64 | // We don't do this in `routes` because of annoying type annotations 65 | let graphql_endpoint_for_closure = graphql_endpoint; // We need this because `move` 66 | let graphiql_closure = move || { 67 | HttpResponse::Ok() 68 | .content_type("text/html; charset=utf-8") 69 | .body(playground_source( 70 | GraphQLPlaygroundConfig::new(&graphql_endpoint_for_closure) 71 | .subscription_endpoint(&graphql_endpoint_for_closure), 72 | )) 73 | }; 74 | 75 | // Set up the endpoint for the GraphQL playground 76 | match playground_endpoint { 77 | // If we're in development and it's enabled, set it up without authentication 78 | Some(playground_endpoint) if cfg!(debug_assertions) => { 79 | cfg.service( 80 | web::resource(playground_endpoint) 81 | .guard(guard::Get()) 82 | .to(graphiql_closure), // The playground needs to know where to send its queries 83 | ); 84 | } 85 | // This shouldn't be possible (playground in production), see `.finish()` in `options.rs` 86 | Some(_) => (), 87 | None => (), 88 | }; 89 | // This closure works entirely with side effects, so we don't need to return anything here 90 | }; 91 | 92 | Ok(configurer) 93 | } 94 | -------------------------------------------------------------------------------- /integrations/serverful/actix-web/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | #![deny(missing_docs)] 3 | 4 | /*! 5 | This is [Diana's](https://arctic-hen7.github.io/diana) integration crate for Actix Web, which enables the easy deployment of a Diana system 6 | on that platform. For more information, see [the documentation for Diana](https://github.com/arctic-hen7/diana) and 7 | [the book](https://arctic-hen7.github.io/diana). 8 | */ 9 | 10 | mod auth_middleware; 11 | mod create_graphql_server; 12 | mod create_subscriptions_server; 13 | mod routes; 14 | 15 | pub use crate::create_graphql_server::create_graphql_server; 16 | pub use crate::create_subscriptions_server::create_subscriptions_server; 17 | 18 | // Users shouldn't have to install Actix Web themselves for basic usage 19 | #[doc(no_inline)] 20 | pub use actix_web; 21 | -------------------------------------------------------------------------------- /integrations/serverful/actix-web/src/routes.rs: -------------------------------------------------------------------------------- 1 | use actix_web::{web, HttpRequest, HttpResponse, Result as ActixResult}; 2 | use async_graphql::{ObjectType, SubscriptionType}; 3 | use async_graphql_actix_web::WSSubscription; // Pre-built WebSocket logic 4 | use std::any::Any; 5 | 6 | use diana::{AuthVerdict, DianaHandler, DianaResponse}; 7 | 8 | // TODO reduce code duplication here 9 | 10 | // The main GraphQL endpoint for queries and mutations with authentication support 11 | // This handler does not support subscriptions 12 | pub async fn graphql_without_subscriptions( 13 | diana_handler: web::Data>, 14 | http_req: HttpRequest, 15 | body: String, 16 | ) -> HttpResponse 17 | where 18 | C: Any + Send + Sync + Clone, 19 | Q: Clone + ObjectType + 'static, 20 | M: Clone + ObjectType + 'static, 21 | S: Clone + SubscriptionType + 'static, 22 | { 23 | // Get the authorisation verdict from the request extensions if it exists (it would be set by the middleware) 24 | let extensions = http_req.extensions(); 25 | let auth_verdict = extensions.get::().cloned(); 26 | 27 | // Run the query, stating that authentication checks don't need to be performed again 28 | let res = diana_handler 29 | .run_stateless_without_subscriptions(body, Option::::None, auth_verdict) 30 | .await; 31 | 32 | // Transform the DianaResponse into an HttpResponse 33 | match res { 34 | DianaResponse::Success(res) => res.into(), 35 | DianaResponse::Blocked => HttpResponse::Forbidden().finish(), 36 | DianaResponse::Error(_) => HttpResponse::InternalServerError().finish(), 37 | } 38 | } 39 | // The main GraphQL endpoint for queries and mutations with authentication support 40 | // This handler does not support subscriptions, but is for use in the subscriptions system (which also needs query/mutation support) 41 | pub async fn graphql_for_subscriptions( 42 | diana_handler: web::Data>, 43 | http_req: HttpRequest, 44 | body: String, 45 | ) -> HttpResponse 46 | where 47 | C: Any + Send + Sync + Clone, 48 | Q: Clone + ObjectType + 'static, 49 | M: Clone + ObjectType + 'static, 50 | S: Clone + SubscriptionType + 'static, 51 | { 52 | // Get the authorisation verdict from the request extensions if it exists (it would be set by the middleware) 53 | let extensions = http_req.extensions(); 54 | let auth_verdict = extensions.get::().cloned(); 55 | 56 | // Run the query, stating that authentication checks don't need to be performed again 57 | let res = diana_handler 58 | .run_stateless_for_subscriptions(body, Option::::None, auth_verdict) 59 | .await; 60 | 61 | // Transform the DianaResponse into an HttpResponse 62 | match res { 63 | DianaResponse::Success(res) => res.into(), 64 | DianaResponse::Blocked => HttpResponse::Forbidden().finish(), 65 | DianaResponse::Error(_) => HttpResponse::InternalServerError().finish(), 66 | } 67 | } 68 | 69 | // The endpoint for GraphQL subscriptions 70 | // This doesn't use DianaHandler at all (except to extract the needed schema) because `async_graphql` provides practically pre-built integration for this 71 | pub async fn graphql_ws( 72 | diana_handler: web::Data>, 73 | http_req: HttpRequest, 74 | payload: web::Payload, 75 | ) -> ActixResult 76 | where 77 | C: Any + Send + Sync + Clone, 78 | Q: Clone + ObjectType + 'static, 79 | M: Clone + ObjectType + 'static, 80 | S: Clone + SubscriptionType + 'static, 81 | { 82 | let schema = &diana_handler.schema_for_subscriptions; 83 | WSSubscription::start(schema.clone(), &http_req, payload) 84 | } 85 | -------------------------------------------------------------------------------- /integrations/serverful/actix-web/tests/e2e.rs: -------------------------------------------------------------------------------- 1 | // This file contains full end-to-end tests of Diana for Actix Web 2 | // This is the verbatim process of manual testing that would be gone through to tests these systems 3 | // This testing is brittle only to the schema, and tests the full E2E result of the system, removing the need to test many smaller components 4 | 5 | use diana::{create_jwt, decode_time_str, get_jwt_secret}; 6 | use diana_actix_web::{ 7 | actix_web::{App, HttpServer}, 8 | create_graphql_server, create_subscriptions_server, 9 | }; 10 | use reqwest::Client; 11 | use std::collections::HashMap; 12 | use tungstenite::{connect, Message}; 13 | 14 | // This 'dirty-imports' the code in `schema.in` 15 | // It will literally be interpolated here 16 | // Never use this in production unless you have a fantastic reason! Just import your code through Cargo! 17 | // We do this here though because you can't import from another example (which is annoying) 18 | include!("../../../../examples/schema/schema.rs"); 19 | 20 | fn get_valid_auth_header() -> Option { 21 | // We assume `get_opts` has been called, which will load the environment variable if necessary 22 | let secret = get_jwt_secret(env::var("JWT_SECRET").unwrap()).unwrap(); 23 | let mut claims = HashMap::new(); 24 | claims.insert("role".to_string(), "test".to_string()); 25 | let exp = decode_time_str("1m").unwrap(); // The created JWT will be valid for 1 minute 26 | let jwt = create_jwt(claims, &secret, exp).unwrap(); 27 | Some("Bearer ".to_string() + &jwt) 28 | } 29 | 30 | // A utility testing macro that handles boilerplate for tests requests 31 | macro_rules! req_test( 32 | ($client:expr, $req_body:expr, $expected_res:expr) => { 33 | { 34 | let res = $client 35 | .post("http://localhost:9001/graphql") 36 | .body($req_body) 37 | .header("Authorization", &get_valid_auth_header().unwrap()) // We don't offer an option to change the token validity because the auth system is integration tested for DianaHandler 38 | .send() 39 | .await 40 | .unwrap() 41 | .text() 42 | .await 43 | .unwrap(); 44 | if res != $expected_res { 45 | panic!("Invalid response to test request on queries/mutations system. Found '{}'", res) 46 | } 47 | } 48 | }; 49 | ); 50 | // A testing utility macro for expecting a certain GraphQL subscription response 51 | // This doesn't actuall send anything to the endpoint, it just checks on the given socket 52 | macro_rules! expect_subscription_res( 53 | ($socket:expr, $expected_res:expr) => { 54 | { 55 | let msg = $socket.read_message().unwrap(); 56 | // The expected response will have the following WS metadata around it 57 | // We don't have braces around the payload because the user should include those (see usage) 58 | let expectation_in_ctx = Message::Text("{\"type\":\"data\",\"id\":\"1\",\"payload\":".to_string() + $expected_res + "}"); 59 | if msg != expectation_in_ctx { 60 | panic!("Invalid subscription response. Expected '{}', found '{}'", expectation_in_ctx, msg); 61 | } 62 | } 63 | }; 64 | ); 65 | // A testing utility macro for confirming that the first confirmational response for a GraphQL subscription happens 66 | macro_rules! expect_subscription_to_connect( 67 | ($socket:expr) => { 68 | { 69 | let msg = $socket.read_message().unwrap(); 70 | let expectation = Message::Text("{\"type\":\"connection_ack\"}".to_string()); 71 | if msg != expectation { 72 | panic!("Expected initial connection acknowledgement message, found '{}'", msg); 73 | } 74 | } 75 | }; 76 | ); 77 | // A testing utility macro to close the given WS connection cleanly and shut down Actix Web servers 78 | macro_rules! shutdown_all( 79 | ($socket:expr, $graphql_server:expr, $subscriptions_server:expr) => { 80 | { 81 | $socket.close(None).unwrap(); 82 | loop { 83 | let msg = $socket.read_message(); 84 | if msg.is_err() { 85 | break; 86 | } 87 | } 88 | 89 | // Stop the Actix Web servers gracefully 90 | $graphql_server.stop(true).await; 91 | $subscriptions_server.stop(true).await; 92 | } 93 | }; 94 | ); 95 | 96 | #[actix_web::rt::test] 97 | async fn e2e() { 98 | // Get the configurations for the two servers 99 | let graphql_configurer = 100 | create_graphql_server(get_opts()).expect("Failed to set up queries/mutations configurer!"); 101 | let susbcriptions_configurer = create_subscriptions_server(get_opts()) 102 | .expect("Failed to set up subscriptions configurer!"); 103 | 104 | // Initialise both of them 105 | let graphql_server = HttpServer::new(move || App::new().configure(graphql_configurer.clone())) 106 | .bind("0.0.0.0:9001") 107 | .expect("Couldn't bind to port 9001 in test.") 108 | .run(); 109 | let subscriptions_server = 110 | HttpServer::new(move || App::new().configure(susbcriptions_configurer.clone())) 111 | .bind("0.0.0.0:9002") 112 | .expect("Couldn't bind to port 9002 in test.") 113 | .run(); 114 | 115 | // Testing code from here on 116 | // We establish a connection to the subscriptions server first so we can listen to the messages it receives after mutations 117 | let (mut socket, _) = connect("ws://localhost:9002/graphql").unwrap(); 118 | socket 119 | .write_message(Message::Text( 120 | "{\"type\": \"connection_init\", \"payload\": {}}".to_string(), 121 | )) 122 | .unwrap(); 123 | socket.write_message(Message::Text("{\"id\": \"1\", \"type\": \"start\", \"payload\": {\"query\": \"subscription { newBlahs { username } }\"}}".to_string())).unwrap(); 124 | expect_subscription_to_connect!(socket); 125 | 126 | let client = Client::new(); 127 | req_test!( 128 | client, 129 | "{\"query\": \"query { apiVersion }\"}", 130 | "{\"data\":{\"apiVersion\":\"0.1.0\"}}" 131 | ); 132 | req_test!( 133 | client, 134 | "{\"query\": \"mutation { updateBlah }\"}", 135 | "{\"data\":{\"updateBlah\":true}}" 136 | ); // This will also test communication with the subscriptions server (it would return an error on failure) 137 | expect_subscription_res!( 138 | socket, 139 | "{\"data\":{\"newBlahs\":{\"username\":\"This is a username\"}}}" 140 | ); 141 | 142 | // Close the WS connection 143 | // We need to continue reading messages until the server confirms because that's how WS handshakes work 144 | shutdown_all!(socket, graphql_server, subscriptions_server); 145 | 146 | println!("Connection closed successfully! All tests have passed!"); 147 | 148 | // The below two lines will keep this program open for further manual testing 149 | // This should only be uncommented in development, and must not be active on CI! 150 | // let never = futures::future::pending::<()>(); 151 | // never.await; 152 | } 153 | -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "diana-aws-lambda" 3 | description = "The integration between Diana GraphQL and AWS Lambda (including its derivatives, like Netlify)." 4 | version = "0.2.9" 5 | authors = ["arctic_hen7 "] 6 | edition = "2018" 7 | license = "MIT" 8 | repository = "https://github.com/arctic-hen7/diana" 9 | homepage = "https://arctic-hen7.github.io/diana" 10 | keywords = ["graphql", "serverless", "authentication"] 11 | categories = ["web-programming", "web-programming::http-server", "web-programming::websocket"] 12 | include = [ 13 | "src/*", 14 | "Cargo.toml", 15 | "LICENSE", 16 | "README.md" 17 | ] 18 | 19 | [dependencies] 20 | diana = { path = "../../../", version = "=0.2.9" } 21 | serde = "1.0.103" 22 | serde_json = "1.0.44" 23 | tokio = { version = "1.0.1", features = ["full"] } 24 | async-graphql = "2.8.2" 25 | netlify_lambda_http = "0.2.0" 26 | aws_lambda_events = "0.4.0" 27 | 28 | [dev-dependencies] 29 | dotenv = "0.15.0" 30 | reqwest = { version = "0.11.4", default-features = false, features = ["rustls-tls", "json"] } 31 | 32 | [lib] 33 | name = "diana_aws_lambda" 34 | path = "src/lib.rs" 35 | -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 arctic_hen7 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. -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/README.md: -------------------------------------------------------------------------------- 1 | # Diana Integration for AWS Lambda and Derivatives 2 | 3 | This is [Diana's](https://arctic-hen7.github.io/diana) integration crate for AWS Lambda and its derivatives (like Netlify), which enables the 4 | easy deployment of a Diana system on those platforms. For more information, see 5 | [the documentation for Diana](https://github.com/arctic-hen7/diana) and [the book](https://arctic-hen7.github.io/diana). 6 | 7 | This crate can be used to create handlers for AWS Lambda itself, or any system that wraps it, like Netlify. Handlers created with this crate 8 | will compile, but will not run without being deployed fully. In development, you should use something like Actix Web (and 9 | [its Diana integration](https://crates.io/crates/diana-actix-web)) to deploy a serverful system for queries and mutations, which you can more 10 | easily work with. When you're ready, you can switch to this crate without changing any part of your schema logic. Full examples are in the book. 11 | -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/examples/netlify/.gitignore: -------------------------------------------------------------------------------- 1 | # Local Netlify folder 2 | .netlify 3 | -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/examples/netlify/README.md: -------------------------------------------------------------------------------- 1 | # Netlify Example 2 | 3 | This example is not entirely intuitive. The Rust code will compile, but it shouldn't be run locally. Optimally, it would be run with `netlify dev` on your local machine, but that tool doesn't yet support Rust functions (hopefully it soon will!). For now, the function actually has to be deployed to see the results. There is a build script at `build.sh` which will compile the binary, optimising for size (explained below). You can then upload that directly to a Netlify site. You should configure that site using the Netlify CLI. 4 | 5 | If you're using another serverless platform that isn't based on AWS Lambda (this should work for it and all its derivatives), you'll need to consult the documentation for `run_serverless_req`, which takes a request body and a raw `Authorization` header and runs a request. That should work basically anywhere. See the book for an example of how to do that. 6 | 7 | ## Size Optimisations 8 | 9 | Netlify does not like large files, and Rust produces very large binaries by default. So, we need to optimise explicitly for size rather than speed, which means including these settings in `Cargo.toml` at the project root (applies to everything, but only needed for this example). 10 | 11 | ```toml 12 | [profile.release] 13 | opt-level = "z" 14 | codegen-units = 1 15 | panic = "abort" 16 | ``` 17 | 18 | Further optimisations can be applied, but as of right now Netlify won't actually detect a super-optimised Rust binary (go figure), so we just use this for now. 19 | 20 | ## What's in `public` and `functions`? 21 | 22 | `public` contains a bare-bones HTMl file because we have to serve some static content over Netlify. 23 | `functions` is where the compiled function will go. 24 | -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/examples/netlify/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # This is a simple build script that sets up the serverless function for deployment to Netlify 4 | 5 | cargo build --example netlify --release --target x86_64-unknown-linux-musl 6 | cp ../../../../../target/x86_64-unknown-linux-musl/release/examples/netlify functions 7 | 8 | # Unfortunately, we can't run `strip` on the final binary to reduce its size becuase then Netlify ignores it completely! 9 | -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/examples/netlify/functions/netlify: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/arctic-hen7/diana/62754467c77224f093c96f25680c29a469717c8f/integrations/serverless/aws-lambda/examples/netlify/functions/netlify -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/examples/netlify/main.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | 3 | use diana_aws_lambda::{ 4 | netlify_lambda_http::{ 5 | lambda, lambda::Context as LambdaCtx, IntoResponse as IntoLambdaResponse, 6 | Request as LambdaRequest, 7 | }, 8 | run_aws_req, AwsError, 9 | }; 10 | 11 | // This 'dirty-imports' the code in `schema.in` 12 | // It will literally be interpolated here 13 | // Never use this in production unless you have a fantastic reason! Just import your code through Cargo! 14 | // We do this here though because you can't import from another example (which is annoying) 15 | include!("../../../../../examples/schema/schema.rs"); 16 | 17 | // Make sure you don't forget to add the JWT_SECRET and SUBSCRIPTIONS_SERVER_PUBLISH_JWT to your Netlify (based on the options in `schema.rs`)! 18 | #[lambda(http)] 19 | #[tokio::main] 20 | async fn main(req: LambdaRequest, _: LambdaCtx) -> Result { 21 | let res = run_aws_req(req, get_opts()).await?; 22 | Ok(res) 23 | } 24 | -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/examples/netlify/netlify.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | publish = "public" 3 | functions = "functions" 4 | 5 | # We don't set up a `dev` block because Netlify Dev does not yet support Rust functions 6 | # We have to deploy live to the cloud to see changes (growl.) 7 | -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/examples/netlify/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Diana Netlify Example 7 | 8 | 9 |

10 | This is an example content page, though the real magic of this 11 | system is in the serverless functions! Ping a request via cURL to 12 | here! 13 |

14 | 15 | 16 | -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/examples/netlify/rust-toolchain: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "stable" 3 | components = ["rustfmt", "clippy"] 4 | targets = ["x86_64-unknown-linux-musl"] 5 | -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | #![deny(missing_docs)] 3 | 4 | /*! 5 | This is [Diana's](https://arctic-hen7.github.io/diana) integration crate for AWS Lambda and its derivatives (like Netlify), which enables the 6 | easy deployment of a Diana system on those platforms. For more information, see 7 | [the documentation for Diana](https://github.com/arctic-hen7/diana) and [the book](https://arctic-hen7.github.io/diana). 8 | 9 | This crate can be used to create handlers for AWS Lambda itself, or any system that wraps it, like Netlify. Handlers created with this crate 10 | will compile, but will not run without being deployed fully. In development, you should use something like Actix Web (and 11 | [its Diana integration](https://crates.io/crates/diana-actix-web)) to deploy a serverful system for queries and mutations, which you can more 12 | easily work with. When you're ready, you can switch to this crate without changing any part of your schema logic. Full examples are in the book. 13 | */ 14 | 15 | mod run_aws_req; 16 | 17 | pub use crate::run_aws_req::{run_aws_req, AwsError}; 18 | 19 | // Users also shouldn't have to install the Netlify stuff themselves for basic usage 20 | pub use netlify_lambda_http; 21 | -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/src/run_aws_req.rs: -------------------------------------------------------------------------------- 1 | // This file contains serverless logic unique to AWS Lambda and its derivatives (e.g. Netlify) 2 | 3 | use async_graphql::{ObjectType, SubscriptionType}; 4 | use aws_lambda_events::encodings::Body; 5 | use netlify_lambda_http::{Request, Response}; 6 | use std::any::Any; 7 | 8 | use diana::{DianaHandler, DianaResponse, Options}; 9 | 10 | /// A *very* generic error type that the deployment system will accept as a return type. 11 | pub type AwsError = Box; 12 | 13 | // This allows us to propagate error HTTP responses more easily 14 | enum AwsReqData { 15 | Valid((String, Option)), 16 | Invalid(Response), // For some reason 17 | } 18 | 19 | // Gets the stringified body and authentication header from an AWS request 20 | // We use a generic error type rather than the crate's `error_chain` logic here for AWS' benefit 21 | fn get_data_from_aws_req(req: Request) -> Result { 22 | // Get the request body (query/mutation) as a string 23 | // Any errors are returned gracefully as HTTP responses 24 | let body = req.body(); 25 | let body = match body { 26 | Body::Text(body_str) => body_str.to_string(), 27 | // Binary bodies are fine as long as we can serialise them into strings 28 | Body::Binary(body_binary) => { 29 | let body_str = std::str::from_utf8(&body_binary); 30 | match body_str { 31 | Ok(body_str) => body_str.to_string(), 32 | Err(_) => { 33 | let res = Response::builder() 34 | .status(400) // Invalid request 35 | .body( 36 | "Found binary body that couldn't be serialized to string".to_string(), 37 | )?; 38 | return Ok(AwsReqData::Invalid(res)); 39 | } 40 | } 41 | } 42 | Body::Empty => { 43 | let res = Response::builder() 44 | .status(400) // Invalid request 45 | .body("Found empty body, expected string".to_string())?; 46 | return Ok(AwsReqData::Invalid(res)); 47 | } 48 | }; 49 | // Get the authorisation header as a string 50 | // Any errors are returned gracefully as HTTP responses 51 | let auth_header = req.headers().get("Authorization"); 52 | let auth_header = match auth_header { 53 | Some(auth_header) => { 54 | let header_str = auth_header.to_str(); 55 | match header_str { 56 | Ok(header_str) => Some(header_str.to_string()), 57 | Err(_) => { 58 | let res = Response::builder() 59 | .status(400) // Invalid request 60 | .body("Couldn't parse authorization header as string".to_string())?; 61 | return Ok(AwsReqData::Invalid(res)); 62 | } 63 | } 64 | } 65 | None => None, 66 | }; 67 | 68 | Ok(AwsReqData::Valid((body, auth_header))) 69 | } 70 | 71 | // Parses the response from `DianaHandler` into HTTP responses that AWS Lambda (or derivatives) can handle 72 | fn parse_aws_res(res: DianaResponse) -> Result, AwsError> { 73 | let res = match res { 74 | DianaResponse::Success(gql_res_str) => Response::builder() 75 | .status(200) // GraphQL will handle any errors within it through JSON 76 | .body(gql_res_str)?, 77 | DianaResponse::Blocked => Response::builder() 78 | .status(403) // Unauthorised 79 | .body("Request blocked due to invalid or insufficient authentication".to_string())?, 80 | DianaResponse::Error(_) => Response::builder() 81 | .status(500) // Internal server error 82 | .body("An internal server error occurred".to_string())?, 83 | }; 84 | 85 | Ok(res) 86 | } 87 | 88 | /// Runs a request for AWS Lambda or its derivatives (e.g. Netlify). 89 | /// This just takes the entire Lambda request and does all the processing for you, but it's really just a wrapper around 90 | /// [`DianaHandler`](diana::DianaHandler). 91 | /// You should use this function in your Lambda handler as shown in the book. 92 | pub async fn run_aws_req( 93 | req: Request, 94 | opts: Options, 95 | ) -> Result, AwsError> 96 | where 97 | C: Any + Send + Sync + Clone, 98 | Q: Clone + ObjectType + 'static, 99 | M: Clone + ObjectType + 'static, 100 | S: Clone + SubscriptionType + 'static, 101 | { 102 | // TODO cache the DianaHandler instance 103 | 104 | // Create a new Diana handler (core logic primitive) 105 | let diana_handler = DianaHandler::new(opts.clone()).map_err(|err| err.to_string())?; 106 | // Process the request data into what's needed 107 | let req_data = get_data_from_aws_req(req)?; 108 | let (body, auth_header) = match req_data { 109 | AwsReqData::Valid(data) => data, 110 | AwsReqData::Invalid(http_res) => return Ok(http_res), // Propagate any HTTP responses for errors 111 | }; 112 | 113 | // Run the serverless request with the extracted data and the user's given options 114 | // We convert the Option to Option<&str> with `.as_deref()` 115 | // We explicitly state that authentication checks need to be run again 116 | let res = diana_handler 117 | .run_stateless_without_subscriptions(body, auth_header.as_deref(), None) 118 | .await; 119 | 120 | // Convert the result to an appropriate HTTP response 121 | let http_res = parse_aws_res(res)?; 122 | Ok(http_res) 123 | } 124 | -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/tests/.gitignore: -------------------------------------------------------------------------------- 1 | # The file where the URl of the local Netlify deployment is stored 2 | .env.local 3 | -------------------------------------------------------------------------------- /integrations/serverless/aws-lambda/tests/e2e.rs: -------------------------------------------------------------------------------- 1 | // This file contains the end-to-end tests for the AWS Lambda integration 2 | // These assume that the latest version of the function in `examples/netlify` has been built and deployed to the correct location 3 | // These tests should be run locally, but MUST NOT be run on CI 4 | 5 | use diana::{create_jwt, decode_time_str, get_jwt_secret}; 6 | use reqwest::Client; 7 | use std::collections::HashMap; 8 | use std::env; 9 | 10 | fn get_valid_auth_header() -> Option { 11 | dotenv::from_filename("../../../examples/.env").unwrap(); // For the JWT secret 12 | // We assume `get_opts` has been called, which will load the environment variable if necessary 13 | let secret = get_jwt_secret(env::var("JWT_SECRET").unwrap()).unwrap(); 14 | let mut claims = HashMap::new(); 15 | claims.insert("role".to_string(), "test".to_string()); 16 | let exp = decode_time_str("1m").unwrap(); // The created JWT will be valid for 1 minute 17 | let jwt = create_jwt(claims, &secret, exp).unwrap(); 18 | Some("Bearer ".to_string() + &jwt) 19 | } 20 | 21 | // A utility testing macro that handles boilerplate for tests requests 22 | macro_rules! req_test( 23 | ($client:expr, $req_body:expr, $expected_res:expr) => { 24 | { 25 | let res = $client 26 | .post(&env::var("NETLIFY_DEPLOYMENT").unwrap()) 27 | .body($req_body) 28 | .header("Authorization", &get_valid_auth_header().unwrap()) // We don't offer an option to change the token validity because the auth system is integration tested for DianaHandler 29 | .send() 30 | .await 31 | .unwrap() 32 | .text() 33 | .await 34 | .unwrap(); 35 | if res != $expected_res { 36 | panic!("Invalid response to test request on serverless queries/mutations system. Found '{}'", res) 37 | } 38 | } 39 | }; 40 | ); 41 | 42 | #[tokio::test] 43 | #[ignore] // This test connects to an active deployment on Netlify (not ideal for CI!) 44 | async fn e2e() { 45 | dotenv::from_filename("./tests/.env.local").unwrap(); // For the Netlify deployment URL (excluded from Git) 46 | let client = Client::new(); 47 | // We only run a single simple query test on the serverless system because otherwise we'd need to set up a subscriptions server etc. 48 | req_test!( 49 | client, 50 | "{\"query\": \"query { apiVersion }\"}", 51 | "{\"data\":{\"apiVersion\":\"0.1.0\"}}" 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /src/auth/auth_state.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::auth::jwt::Claims; 4 | use crate::errors::*; 5 | 6 | /// An authentication token with claims. 7 | #[derive(Debug, Clone)] 8 | pub struct AuthToken(pub Claims); 9 | 10 | /// The three states authentication can be in at the token level. 11 | #[derive(Debug, Clone)] 12 | pub enum AuthState { 13 | /// The user is authorized, authentication data is attached. 14 | Authorised(AuthToken), 15 | /// An invalid token was provided. 16 | InvalidToken, 17 | /// No token was provided. 18 | NoToken, 19 | } 20 | impl AuthState { 21 | /// Checks if the each key/value pair in the given `HashMap` is present in the token. This will return false if the token was invalid 22 | /// or not provided. 23 | pub fn has_claims(&self, test_claims: HashMap<&str, &str>) -> bool { 24 | if let Self::Authorised(AuthToken(Claims { claims, .. })) = self { 25 | for (key, val) in &test_claims { 26 | if claims.get(&key.to_string()) != Some(&val.to_string()) { 27 | return false; 28 | } 29 | } 30 | 31 | true // If we're here everything's passed 32 | } else { 33 | false 34 | } 35 | } 36 | /// Checks if the token is valid. 37 | pub fn is_valid(&self) -> bool { 38 | matches!(self, Self::Authorised(_)) 39 | } 40 | /// Checks if the token is invalid. 41 | pub fn is_invalid(&self) -> bool { 42 | matches!(self, Self::InvalidToken) 43 | } 44 | /// Checks if the token is not present. 45 | pub fn has_no_token(&self) -> bool { 46 | matches!(self, Self::NoToken) 47 | } 48 | /// Gets a reference to the claims of the token (including metadata like expiry). 49 | pub fn get_claims(&self) -> Result<&Claims> { 50 | match self { 51 | Self::Authorised(AuthToken(claims)) => Ok(claims), 52 | _ => bail!(ErrorKind::Unauthorised), 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/auth/core.rs: -------------------------------------------------------------------------------- 1 | // This file contains the core authentication logic that will be used regardless of integration 2 | 3 | use crate::auth::auth_state::{AuthState, AuthToken}; 4 | use crate::auth::jwt::{get_jwt_secret, validate_and_decode_jwt}; 5 | use crate::errors::*; 6 | 7 | /// An enum for the level of blocking imposed on a particular endpoint. 8 | /// Your choice on this should be carefully evaluated based on your threat model. Please choose wisely! 9 | #[derive(Debug, Clone, Copy)] 10 | pub enum AuthBlockLevel { 11 | /// Allows anything through. 12 | /// - Valid token -> allow 13 | /// - Invalid token -> allow 14 | /// - Missing token -> allow 15 | AllowAll, 16 | /// Blocks eveything except requests with valid tokens. 17 | /// Note that, with this setting, introspection will be impossible in the GraphiQL playground. You may want to use `AllowMissing` in development 18 | /// and then this in production (see the book). 19 | /// - Valid token -> allow 20 | /// - Invalid token -> block 21 | /// - Missing token -> block 22 | BlockUnauthenticated, 23 | /// Allows requests with valid tokens or no token at all. Only blocks requests that specify an invalid token. 24 | /// This is mostly useful for development to enable introspection in the GraphiQL playground (see the book). 25 | /// - Valid token -> allow 26 | /// - Invalid token -> block 27 | /// - Missing token -> allow 28 | AllowMissing, 29 | } 30 | 31 | // Extracts an authentication state from the given Option token 32 | // This is exposed as a primitive for serverful and serverless authentication logic 33 | pub fn get_token_state_from_header( 34 | auth_header: Option<&str>, 35 | secret_str: String, 36 | ) -> Result { 37 | // Get the bearer token from the header if it exists 38 | let bearer_token = match auth_header { 39 | Some(header) => header 40 | .split("Bearer") 41 | .collect::>() 42 | .get(1) // Get everything apart from that first element 43 | .map(|token| token.trim()), 44 | None => None, 45 | }; 46 | 47 | // Decode the bearer token into an authentication state 48 | match bearer_token { 49 | Some(token) => { 50 | let jwt_secret = get_jwt_secret(secret_str)?; 51 | let decoded_jwt = validate_and_decode_jwt(&token, &jwt_secret); 52 | 53 | match decoded_jwt { 54 | Some(claims) => Ok(AuthState::Authorised(AuthToken(claims))), 55 | None => Ok(AuthState::InvalidToken), // The token is invalid 56 | } 57 | } 58 | None => Ok(AuthState::NoToken), // No token exists 59 | } 60 | } 61 | 62 | /// This represents the decision as to whether or not a use is allowed through to an endpoint. You should only have to deal with this if you're 63 | /// developing middleware for a custom integration. 64 | #[derive(Clone, Debug)] 65 | pub enum AuthVerdict { 66 | /// The user should be allowed through, and their decoded authentication data (JWT payload without metadata) is attached. 67 | Allow(AuthState), 68 | /// The user should be blocked. 69 | Block, 70 | /// Some internal error occurred, the body of which is attached. 71 | Error(String), 72 | } 73 | 74 | // Compares the given token's authentication state (as a raw result) to a given block-level to arrive at a verdict 75 | pub fn get_auth_verdict( 76 | token_state: Result, 77 | block_state: AuthBlockLevel, 78 | ) -> AuthVerdict { 79 | match token_state { 80 | // We hold `token_state` as the AuthState variant so we don't pointlessly insert a Result into the request extensions 81 | Ok(token_state @ AuthState::Authorised(_)) => AuthVerdict::Allow(token_state), 82 | Ok(token_state @ AuthState::InvalidToken) => { 83 | if let AuthBlockLevel::AllowAll = block_state { 84 | AuthVerdict::Allow(token_state) 85 | } else { 86 | AuthVerdict::Block 87 | } 88 | } 89 | Ok(token_state @ AuthState::NoToken) => { 90 | if let AuthBlockLevel::AllowAll | AuthBlockLevel::AllowMissing = block_state { 91 | AuthVerdict::Allow(token_state) 92 | } else { 93 | AuthVerdict::Block 94 | } 95 | } 96 | Err(err) => AuthVerdict::Error(err.to_string()), 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/auth/jwt.rs: -------------------------------------------------------------------------------- 1 | use chrono::{prelude::Utc, Duration}; 2 | use jsonwebtoken::{decode, encode, Algorithm, DecodingKey, EncodingKey, Header, Validation}; 3 | use serde::{Deserialize, Serialize}; 4 | use std::collections::HashMap; 5 | 6 | use crate::errors::*; 7 | 8 | /// The claims made by a JWT, including metadata. 9 | #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] 10 | pub struct Claims { 11 | /// The expiry of the JWT as a datetime in seconds from Unix epoch. 12 | pub exp: u64, 13 | /// The claims made by the user (non-metadata). 14 | pub claims: HashMap, 15 | } 16 | 17 | /// A parsed JWT secret. This should be created once with `get_jwt_secret` and then reused as much as possible. You may want to 18 | /// place it in your context under [`Options`](crate::Options). 19 | #[derive(Debug, Clone)] 20 | pub struct JWTSecret<'a> { 21 | encoding_key: EncodingKey, 22 | decoding_key: DecodingKey<'a>, 23 | } 24 | 25 | /// Transforms a string JWT secret into a form in which it can be used for operations. 26 | pub fn get_jwt_secret<'a>(secret_str: String) -> Result> { 27 | let encoding_key = EncodingKey::from_base64_secret(&secret_str)?; 28 | let decoding_key = DecodingKey::from_base64_secret(&secret_str)?; 29 | 30 | Ok(JWTSecret { 31 | encoding_key, 32 | decoding_key, 33 | }) 34 | } 35 | 36 | /// Decodes time strings like '1w' into actual datetimes from the present moment. If you've ever used NodeJS's [`jsonwebtoken`](https://www.npmjs.com/package/jsonwebtoken) module, this is 37 | /// very similar (based on Vercel's [`ms`](https://github.com/vercel/ms) module for JavaScript). 38 | /// Accepts strings of the form 'xXyYzZ...', where the lower-case letters are numbers meaning a number of the intervals X/Y/Z (e.g. 1m4d -- one month four days). 39 | /// The available intervals are: 40 | /// 41 | /// - s: second, 42 | /// - m: minute, 43 | /// - h: hour, 44 | /// - d: day, 45 | /// - w: week, 46 | /// - M: month (30 days used here, 12M ≠ 1y!), 47 | /// - y: year (365 days always, leap years ignored, if you want them add them as days) 48 | pub fn decode_time_str(time_str: &str) -> Result { 49 | let mut duration_after_current = Duration::zero(); 50 | // Get the current datetime since Unix epoch, we'll add to that 51 | let current = Utc::now(); 52 | // A working variable to store the '123' part of an interval until we reach the idnicator and can do the full conversion 53 | let mut curr_duration_length = String::new(); 54 | // Iterate through the time string's characters to get each interval 55 | for c in time_str.chars() { 56 | // If we have a number, append it to the working cache 57 | // If we have an indicator character, we'll match it to a duration 58 | if c.is_numeric() { 59 | curr_duration_length.push(c); 60 | } else { 61 | // Parse the working variable into an actual number 62 | let interval_length = curr_duration_length.parse::().unwrap(); // It's just a string of numbers, we know more than the compiler 63 | let duration = match c { 64 | 's' => Duration::seconds(interval_length), 65 | 'm' => Duration::minutes(interval_length), 66 | 'h' => Duration::hours(interval_length), 67 | 'd' => Duration::days(interval_length), 68 | 'w' => Duration::weeks(interval_length), 69 | 'M' => Duration::days(interval_length * 30), // Multiplying the number of months by 30 days (assumed length of a month) 70 | 'y' => Duration::days(interval_length * 365), // Multiplying the number of years by 365 days (assumed length of a year) 71 | c => bail!(ErrorKind::InvalidDatetimeIntervalIndicator(c.to_string())), 72 | }; 73 | duration_after_current = duration_after_current + duration; 74 | // Reset that working variable 75 | curr_duration_length = String::new(); 76 | } 77 | } 78 | // Form the final duration by reducing the durations vector into one 79 | let datetime = current + duration_after_current; 80 | 81 | Ok(datetime.timestamp() as u64) // As Unix timestamp in u64 because that's what the JWT demands (we can't have expiries before January 1st 1970, let me know if that's a problem!) 82 | } 83 | 84 | /// Creates a new JWT. You should use this to issue all client JWTs and create the initial JWT for communication with the subscriptions 85 | /// server (more information in the book). 86 | pub fn create_jwt( 87 | user_claims: HashMap, 88 | secret: &JWTSecret, 89 | exp: u64, 90 | ) -> Result { 91 | // Create the claims 92 | let claims = Claims { 93 | exp, 94 | claims: user_claims, 95 | }; 96 | let token = encode( 97 | &Header::new(Algorithm::HS512), 98 | &claims, 99 | &secret.encoding_key, 100 | )?; 101 | 102 | Ok(token) 103 | } 104 | 105 | /// Validates a JWT and returns the payload. All client JWTs are automatically validated and their payloads are sent (parsed) to your resolvers, 106 | /// but if you have a system on top of that you'll want to use this function (not required for normal Diana usage though). 107 | pub fn validate_and_decode_jwt(jwt: &str, secret: &JWTSecret) -> Option { 108 | let decoded = decode::( 109 | jwt, 110 | &secret.decoding_key, 111 | &Validation::new(Algorithm::HS512), 112 | ); 113 | 114 | match decoded { 115 | Ok(decoded) => Some(decoded.claims), 116 | Err(_) => None, 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/auth/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod auth_state; 2 | pub mod core; 3 | pub mod jwt; 4 | -------------------------------------------------------------------------------- /src/diana_handler.rs: -------------------------------------------------------------------------------- 1 | // This file contains the core logic primitives that actually run a given request 2 | // This is depended on by serverful and serverless systems 3 | 4 | use async_graphql::{EmptySubscription, ObjectType, Request, Schema, SubscriptionType}; 5 | use std::any::Any; 6 | 7 | use crate::auth::core::{get_auth_verdict, get_token_state_from_header, AuthVerdict}; 8 | use crate::errors::*; 9 | use crate::graphql::{ 10 | get_schema_for_subscriptions, get_schema_without_subscriptions, PublishMutation, 11 | SubscriptionQuery, 12 | }; 13 | use crate::options::Options; 14 | 15 | /// The basic response from a given request. 16 | #[derive(Clone, Debug)] 17 | pub enum DianaResponse { 18 | /// The request was successful and the response is attached. 19 | /// Return a 200. 20 | Success(String), 21 | /// The request was blocked (unauthorized). 22 | /// Return a 403. 23 | Blocked, 24 | /// An error occurred on the server side and its body is encapsulated. Any GraphQL errors will be encapsulated in the `Success` variant's 25 | /// payload. 26 | /// Return a 500. 27 | Error(String), 28 | } 29 | 30 | // Represents the chice of the schema for/without subscriptions 31 | #[doc(hidden)] 32 | pub enum SysSchema { 33 | WithoutSubscriptions, 34 | ForSubscriptions, 35 | } 36 | 37 | /// The core logic primitive that underlies Diana's systems. You should only use this if you need to support a platform other than the ones 38 | /// Diana has pre-built systems for (see the book). 39 | /// This is a struct so as to allow the caching of data that stay the same across requests, like the parsed and built schemas. 40 | #[derive(Clone)] 41 | pub struct DianaHandler 42 | where 43 | C: Any + Send + Sync + Clone, 44 | Q: Clone + ObjectType + 'static, 45 | M: Clone + ObjectType + 'static, 46 | S: Clone + SubscriptionType + 'static, 47 | { 48 | /// The options parsed in to the handler in `::new()`.You should only need to touch this if you're building a custom integration. 49 | pub opts: Options, 50 | /// The schema created for the queries/mutations system. This has the user's given query and mutation roots and no subscriptions at all. 51 | /// You should only need to touch this if you're building a custom integration. 52 | pub schema_without_subscriptions: Schema, 53 | /// The schema created for the subscriptions server. This has the user's given subscription root and internally used query/mutation roots 54 | /// for communication with the query/mutation system. You should only need to touch this if you're building a custom integration. 55 | pub schema_for_subscriptions: Schema, 56 | } 57 | impl DianaHandler 58 | where 59 | C: Any + Send + Sync + Clone, 60 | Q: Clone + ObjectType + 'static, 61 | M: Clone + ObjectType + 'static, 62 | S: Clone + SubscriptionType + 'static, 63 | { 64 | /// Creates a new instance of the handler with the given options. 65 | pub fn new(opts: Options) -> Result { 66 | // TODO only create a schema for subscriptions if they're actually being used (will require broader logic changes) 67 | // Get the schema (this also creates a publisher to the subscriptions server and inserts context) 68 | // We deal with any errors directly with the serverless response enum 69 | let schema_without_subscriptions = get_schema_without_subscriptions( 70 | opts.schema.clone(), 71 | opts.subscriptions_server_data.clone(), 72 | opts.ctx.clone(), 73 | )?; 74 | let schema_for_subscriptions = 75 | get_schema_for_subscriptions(opts.schema.clone(), opts.ctx.clone()); 76 | 77 | Ok(DianaHandler { 78 | opts, 79 | schema_without_subscriptions, 80 | schema_for_subscriptions, 81 | }) 82 | } 83 | /// Determines ahead of time whether or not a request is authenticated. This should be used in middleware if possible so we can avoid 84 | /// sending full payloads if the auth token isn't even valid. 85 | /// This just takes the HTTP `Authorization` header and returns an [`AuthVerdict`]. 86 | pub fn is_authed + std::fmt::Display>( 87 | &self, 88 | raw_auth_header: Option, 89 | ) -> AuthVerdict { 90 | // This function accepts anything that can be turned into a string for convenience 91 | // Then we convert it into a definite Option 92 | let auth_header = raw_auth_header.map(|x| x.to_string()); 93 | // And then we get it as an Option<&str> (whic is what we need for slicing) 94 | let auth_header_str = auth_header.as_deref(); 95 | // Get a verdict on whether or not the user should be allowed through 96 | let token_state = 97 | get_token_state_from_header(auth_header_str, self.opts.jwt_secret.clone()); 98 | get_auth_verdict(token_state, self.opts.authentication_block_state) 99 | } 100 | /// Runs a query or mutation (stateless) given the request body and the value of the HTTP `Authorization` header. 101 | /// This performs authorisation checks and runs the actual request. If you've already used `.is_authed()` to obtain an [`AuthVerdict`], 102 | /// this can be provided as the third argument to avoid running auth checks twice. 103 | /// This will return a [`DianaResponse`] no matter what, which simplifies error handling significantly. 104 | /// This function is for the subscriptions system only. 105 | pub async fn run_stateless_for_subscriptions + std::fmt::Display>( 106 | &self, 107 | body: String, 108 | raw_auth_header: Option, 109 | given_auth_verdict: Option, 110 | ) -> DianaResponse { 111 | self.run_stateless_req( 112 | SysSchema::ForSubscriptions, 113 | body, 114 | raw_auth_header, 115 | given_auth_verdict, 116 | ) 117 | .await 118 | } 119 | /// Runs a query or mutation (stateless) given the request body and the value of the HTTP `Authorization` header. 120 | /// This performs authorisation checks and runs the actual request. If you've already used `.is_authed()` to obtain an [`AuthVerdict`], 121 | /// this can be provided as the third argument to avoid running auth checks twice. 122 | /// This will return a [`DianaResponse`] no matter what, which simplifies error handling significantly. 123 | /// This function is for the queries/mutations system only. 124 | pub async fn run_stateless_without_subscriptions + std::fmt::Display>( 125 | &self, 126 | body: String, 127 | raw_auth_header: Option, 128 | given_auth_verdict: Option, 129 | ) -> DianaResponse { 130 | self.run_stateless_req( 131 | SysSchema::WithoutSubscriptions, 132 | body, 133 | raw_auth_header, 134 | given_auth_verdict, 135 | ) 136 | .await 137 | } 138 | // This is used internally to provide query/mutation running functionality to the systems for/without subscriptions 139 | // It is exposed to make testing easier, though users should not use it! 140 | #[doc(hidden)] 141 | pub async fn run_stateless_req + std::fmt::Display>( 142 | &self, 143 | which_schema: SysSchema, 144 | body: String, 145 | raw_auth_header: Option, 146 | given_auth_verdict: Option, 147 | ) -> DianaResponse { 148 | // Run authentication checks if we need to (they may have already been run in middleware) 149 | let verdict = match given_auth_verdict { 150 | Some(verdict) => verdict, 151 | None => self.is_authed(raw_auth_header), 152 | }; 153 | 154 | // Based on that verdict, maybe run the request 155 | match verdict { 156 | AuthVerdict::Allow(auth_data) => { 157 | // Deserialise that raw JSON request into an actual request with variables etc. 158 | let gql_req = serde_json::from_str::(&body); 159 | let mut gql_req = match gql_req { 160 | Ok(gql_req) => gql_req, 161 | Err(err) => return DianaResponse::Error(err.to_string()), 162 | }; 163 | // Insert the authentication data directly into that 164 | gql_req = gql_req.data(auth_data); 165 | // Run the request with the correct schema 166 | let res = match which_schema { 167 | SysSchema::WithoutSubscriptions => { 168 | self.schema_without_subscriptions.execute(gql_req).await 169 | } 170 | SysSchema::ForSubscriptions => { 171 | self.schema_for_subscriptions.execute(gql_req).await 172 | } 173 | }; 174 | // Serialise that response into a string (the response bodies all have to be of the same type) 175 | let res_str = serde_json::to_string(&res); 176 | let res_str = match res_str { 177 | Ok(res_str) => res_str, 178 | Err(err) => return DianaResponse::Error(err.to_string()), 179 | }; 180 | 181 | DianaResponse::Success(res_str) 182 | } 183 | AuthVerdict::Block => DianaResponse::Blocked, 184 | AuthVerdict::Error(err) => DianaResponse::Error(err), 185 | } 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /src/errors.rs: -------------------------------------------------------------------------------- 1 | #![allow(missing_docs)] 2 | 3 | pub use error_chain::bail; 4 | use error_chain::error_chain; 5 | 6 | // TODO fix the integration errors 7 | 8 | // The `error_chain` setup for the whole crate 9 | // All systems use these errors, except for GraphQL resolvers, because they have to return a particular kind of error 10 | error_chain! { 11 | // The custom errors for this crate (very broad) 12 | errors { 13 | /// An environment variable had an invalid type. 14 | /// E.g. a port was given as a hex string for some reason. 15 | InvalidEnvVarType(var_name: String, expected: String) { 16 | description("invalid environment variable type") 17 | display( 18 | "invalid environment variable type for variable '{var_name}', expected '{expected}'", 19 | var_name=var_name, 20 | expected=expected 21 | ) 22 | } 23 | 24 | /// A required part of the GraphQL context was not found. 25 | GraphQLContextNotFound(elem_name: String) { 26 | description("required graphql context element not found") 27 | display("required graphql context element '{}' not found", elem_name) 28 | } 29 | 30 | /// A Mutex was poisoned (if `.lock()` failed). 31 | MutexPoisoned(mutex_name: String) { 32 | description("mutex poisoned") 33 | display("mutex '{}' poisoned", mutex_name) 34 | } 35 | 36 | /// The subscriptions server failed to publish data it was asked to. This error is usually caused by an authentication failure. 37 | SubscriptionDataPublishFailed { 38 | description("failed to publish data to the subscriptions server") 39 | display("failed to publish data to the subscriptions server, this is most likely due to an authentication failure") 40 | } 41 | 42 | /// An invalid indicator string was used when trying to convert a timestring into a datetime. 43 | InvalidDatetimeIntervalIndicator(indicator: String) { 44 | description("invalid indicator in timestring") 45 | display("invalid indicator '{}' in timestring, must be one of: s, m, h, d, w, M, y", indicator) 46 | } 47 | 48 | /// There was an unauthorised access attempt. 49 | Unauthorised { 50 | description("unauthorised access attempt") 51 | display("unable to comply with request due to lack of valid and sufficient authentication") 52 | } 53 | 54 | /// One or more required builder fields weren't set up. 55 | IncompleteBuilderFields { 56 | description("not all required builder fields were instantiated") 57 | display("some required builder fields haven't been instantiated") 58 | } 59 | 60 | /// The creation of an HTTP response for Lambda or its derivatives failed. 61 | HttpResponseBuilderFailed { 62 | description("the builder for an http response (netlify_lambda_http) returned an error") 63 | display("the builder for an http response (netlify_lambda_http) returned an error") 64 | } 65 | 66 | /// There was an attempt to create a subscriptions server without declaring its existence or configuration in the [Options]. 67 | InvokedSubscriptionsServerWithInvalidOptions { 68 | description("you tried to create a subscriptions server without configuring it in the options") 69 | display("you tried to create a subscriptions server without configuring it in the options") 70 | } 71 | 72 | /// There was an attempt to initialize the GraphiQL playground in a production environment. 73 | AttemptedPlaygroundInProduction { 74 | description("you tried to initialize the GraphQL playground in production, which is not supported due to authentication issues") 75 | display("you tried to initialize the GraphQL playground in production, which is not supported due to authentication issues") 76 | } 77 | 78 | /// There was an error in one of the integrations. 79 | IntegrationError(message: String, integration_name: String) { 80 | description("an error occurred in one of Diana's integration libraries") 81 | display( 82 | "the following error occurred in the '{integration_name}' integration library: {message}", 83 | integration_name=integration_name, 84 | message=message 85 | ) 86 | } 87 | } 88 | // We work with many external libraries, all of which have their own errors 89 | foreign_links { 90 | Io(::std::io::Error); 91 | EnvVar(::std::env::VarError); 92 | Reqwest(::reqwest::Error); 93 | Json(::serde_json::Error); 94 | JsonWebToken(::jsonwebtoken::errors::Error); 95 | } 96 | } 97 | 98 | /// A wrapper around [`async_graphql::Result`](async_graphql::Result). 99 | /// You should use this as the return type for any of your own schemas that might return errors. 100 | /// # Example 101 | /// ```rust 102 | /// use diana::errors::GQLResult; 103 | /// 104 | /// async fn api_version() -> GQLResult { 105 | /// // Your code here 106 | /// Ok("test".to_string()) 107 | /// } 108 | /// ``` 109 | pub type GQLResult = async_graphql::Result; 110 | /// A wrapper around [`async_graphql::Error`]. 111 | /// If any of your schemas need to explicitly create an error that only exists in them (and you're not using something like [mod@error_chain]), 112 | /// you should use this. 113 | /// # Example 114 | /// ```rust 115 | /// use diana::errors::{GQLResult, GQLError}; 116 | /// 117 | /// async fn api_version() -> GQLResult { 118 | /// let err = GQLError::new("Test error!"); 119 | /// // Your code here 120 | /// Err(err) 121 | /// } 122 | /// ``` 123 | pub type GQLError = async_graphql::Error; 124 | -------------------------------------------------------------------------------- /src/graphql.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{EmptySubscription, Object as GQLObject, ObjectType, Schema, SubscriptionType}; 2 | use std::any::Any; 3 | use std::sync::Mutex; 4 | 5 | use crate::errors::*; 6 | use crate::graphql_utils::{get_auth_data_from_ctx, get_pubsub_from_ctx}; 7 | use crate::is_authed; 8 | use crate::pubsub::{PubSub, Publisher}; 9 | 10 | // The base query type simply allows us to set up the subscriptions schema (has to have at least one query) 11 | #[derive(Default, Clone)] 12 | pub struct SubscriptionQuery; 13 | #[GQLObject] 14 | impl SubscriptionQuery { 15 | // TODO disable introspection on this endpoint 16 | async fn _query(&self) -> String { 17 | "This is a meaningless endpoint needed only for initialisation.".to_string() 18 | } 19 | } 20 | 21 | // This mutation type is utilised by the subscriptions server to allow the publishing of data 22 | // We pass around the PubSub state internally to that GraphQL system (see get_schema_for_subscriptions) 23 | #[derive(Default, Clone)] 24 | pub struct PublishMutation; 25 | #[GQLObject] 26 | impl PublishMutation { 27 | // We accept string data because this is a highly generic type that serialises in the subscriptions handler 28 | // That may seem to subvert some of the purpose of GraphQL, but this resolver is to be INTERNALLY ONLY! 29 | // That provides a system-level data integrity guarantee, as only full mutations will call this, and through a PubSub abstraction 30 | // There should be very little reason for users to implement it themselves, but this type could easily be extended with custom logic 31 | async fn publish( 32 | &self, 33 | raw_ctx: &async_graphql::Context<'_>, 34 | channel: String, 35 | data: String, 36 | ) -> Result { 37 | if is_authed!( 38 | get_auth_data_from_ctx(raw_ctx)?, 39 | { 40 | "role" => "graphql_server" 41 | } 42 | ) { 43 | let mut pubsub = get_pubsub_from_ctx(raw_ctx)?; 44 | pubsub.publish(&channel, data); 45 | Ok(true) 46 | } else { 47 | bail!(ErrorKind::Unauthorised) 48 | } 49 | } 50 | } 51 | 52 | // Information about the subscriptions server for the rest of the system 53 | #[derive(Clone)] 54 | pub struct SubscriptionsServerInformation { 55 | pub hostname: String, 56 | pub port: String, // It'll be mixed in to create a URL, may as well start as a string 57 | pub endpoint: String, 58 | pub jwt_to_connect: String, // This should be signed with the secret the subscriptions server knows 59 | } 60 | 61 | // A type for the schema that the user will submit 62 | #[derive(Clone)] 63 | pub struct UserSchema 64 | where 65 | Q: ObjectType + 'static, 66 | M: ObjectType + 'static, 67 | S: SubscriptionType + 'static, 68 | { 69 | pub query_root: Q, 70 | pub mutation_root: M, 71 | pub subscription_root: S, 72 | } 73 | 74 | pub fn get_schema_without_subscriptions( 75 | user_schema: UserSchema, 76 | subscription_server_info: Option, 77 | user_ctx: C, 78 | ) -> Result> 79 | where 80 | C: Any + Send + Sync, 81 | Q: ObjectType + 'static, 82 | M: ObjectType + 'static, 83 | S: SubscriptionType + 'static, 84 | { 85 | // We just use an empty subscription root here because subscriptions are handled by the dedicated subscriptions server 86 | let schema = Schema::build( 87 | user_schema.query_root, 88 | user_schema.mutation_root, 89 | EmptySubscription, 90 | ) 91 | // We add some custom user-defined context (e.g. a database connection pool) 92 | .data(user_ctx); 93 | 94 | // Conditionally extend that schema with a publisher if we're using a subscriptions server 95 | let schema = match subscription_server_info { 96 | Some(subscription_server_info) => schema 97 | .data(Publisher::new( 98 | subscription_server_info.hostname, 99 | subscription_server_info.port, 100 | subscription_server_info.endpoint, 101 | subscription_server_info.jwt_to_connect, 102 | )?) 103 | .finish(), 104 | None => schema.finish(), 105 | }; 106 | 107 | Ok(schema) 108 | } 109 | pub fn get_schema_for_subscriptions( 110 | user_schema: UserSchema, 111 | user_ctx: C, 112 | ) -> Schema 113 | where 114 | C: Any + Send + Sync, 115 | Q: ObjectType + 'static, 116 | M: ObjectType + 'static, 117 | S: SubscriptionType + 'static, 118 | { 119 | // The schema for the subscriptions server should only have subscriptions, and a mutation to allow publishing 120 | // Unfortunately, we have to have at least one query, so we implement a meaningless one that isn't introspected 121 | Schema::build( 122 | SubscriptionQuery, 123 | PublishMutation, 124 | user_schema.subscription_root, 125 | ) 126 | // We add some custom user-defined context (e.g. a database connection pool) 127 | .data(user_ctx) 128 | // We add a mutable PubSub instance for managing subscriptions internally 129 | .data(Mutex::new(PubSub::default())) // We add a PubSub instance to internally manage state in the serverful subscriptions system 130 | .finish() 131 | } 132 | -------------------------------------------------------------------------------- /src/graphql_utils.rs: -------------------------------------------------------------------------------- 1 | // Utility functions for GraphQL resolvers 2 | use std::sync::{Mutex, MutexGuard}; 3 | use tokio_stream::Stream; 4 | 5 | use crate::auth::auth_state::AuthState; 6 | use crate::errors::*; 7 | use crate::pubsub::PubSub; 8 | 9 | /// Checks to see if the given authentication state matches the series of given claims. This must be provided with the authentication state, 10 | /// a series of claims to check against, and code to execute if the user is authenticated. This will call [`bail!`] with an [`ErrorKind::Unauthorised`](crate::errors::ErrorKind::Unauthorised) 11 | /// error if the user is unauthenticated, so **that must be handled in your function's return type**! 12 | /// # Example 13 | /// This is a simplified version of the internal logic that publishes data to the subscriptions server. 14 | /// ``` 15 | /// use diana::{ 16 | /// errors::{Result, GQLResult}, 17 | /// graphql_utils::get_auth_data_from_ctx, 18 | /// async_graphql::{Object as GQLObject}, 19 | /// if_authed, 20 | /// }; 21 | /// 22 | /// #[derive(Default, Clone)] 23 | /// pub struct PublishMutation; 24 | /// #[GQLObject] 25 | /// impl PublishMutation { 26 | /// async fn publish( 27 | /// &self, 28 | /// raw_ctx: &async_graphql::Context<'_>, 29 | /// channel: String, 30 | /// data: String, 31 | /// ) -> Result { 32 | /// let auth_state = get_auth_data_from_ctx(raw_ctx)?; 33 | /// if_authed!( 34 | /// auth_state, 35 | /// { 36 | /// "role" => "graphql_server" 37 | /// }, 38 | /// { 39 | /// // Your code here 40 | /// Ok(true) 41 | /// } 42 | /// ) 43 | /// } 44 | /// } 45 | /// 46 | /// # fn main() {} 47 | /// ``` 48 | // TODO mark as deprecated 49 | #[macro_export] 50 | #[deprecated( 51 | since = "0.2.8", 52 | note = "please use `is_authed!` instead, it exposes a boolean and lets you use your own error logic" 53 | )] 54 | macro_rules! if_authed( 55 | ($auth_state:expr, { $($key:expr => $value:expr),+ }, $code:block) => { 56 | { 57 | // Create a HashMap out of the given test claims 58 | let mut test_claims: ::std::collections::HashMap<&str, &str> = ::std::collections::HashMap::new(); 59 | $( 60 | test_claims.insert($key, $value); 61 | )+ 62 | // Match the authentication state with those claims now 63 | if $auth_state.has_claims(test_claims) { 64 | $code 65 | } else { 66 | Err($crate::errors::ErrorKind::Unauthorised.into()) 67 | } 68 | } 69 | }; 70 | ); 71 | 72 | /// Checks to see if the given authentication state matches the series of given claims. This must be provided with the authentication state, 73 | /// a series of claims to check against. It will then return a boolean as to whether or not the user is authorized. 74 | /// This should be used instead of [`if_authed!`]. 75 | /// # Example 76 | /// This is a simplified version of the internal logic that publishes data to the subscriptions server. 77 | /// ``` 78 | /// use diana::{ 79 | /// errors::{Result, GQLResult, bail, ErrorKind}, 80 | /// graphql_utils::get_auth_data_from_ctx, 81 | /// async_graphql::{Object as GQLObject}, 82 | /// is_authed, 83 | /// }; 84 | /// 85 | /// #[derive(Default, Clone)] 86 | /// pub struct PublishMutation; 87 | /// #[GQLObject] 88 | /// impl PublishMutation { 89 | /// async fn publish( 90 | /// &self, 91 | /// raw_ctx: &async_graphql::Context<'_>, 92 | /// channel: String, 93 | /// data: String, 94 | /// ) -> Result { 95 | /// if is_authed!( 96 | /// get_auth_data_from_ctx(raw_ctx)?, 97 | /// { 98 | /// "role" => "graphql_server" 99 | /// } 100 | /// ) { 101 | /// // Your code here 102 | /// Ok(true) 103 | /// } else { 104 | /// // Your error handling code here 105 | /// bail!(ErrorKind::Unauthorised) 106 | /// } 107 | /// } 108 | /// } 109 | /// 110 | /// # fn main() {} 111 | /// ``` 112 | #[macro_export] 113 | macro_rules! is_authed( 114 | ($auth_state:expr, { $($key:expr => $value:expr),+ }) => { 115 | { 116 | // Create a HashMap out of the given test claims 117 | let mut test_claims: ::std::collections::HashMap<&str, &str> = ::std::collections::HashMap::new(); 118 | $( 119 | test_claims.insert($key, $value); 120 | )+ 121 | // Match the authentication state with those claims now 122 | $auth_state.has_claims(test_claims) 123 | } 124 | }; 125 | ); 126 | 127 | /// Gets a subscription stream to events published on a particular channel from the context of a GraphQL resolver. 128 | /// **This must only be used in subscriptions! It will not work anywhere else!** 129 | /// This returns a pre-created stream which you should manipulate if necessary. 130 | /// All data sent via the publisher from the queries/mutations system will land here **in string format**. Serialization is up to you. 131 | /// # Example 132 | /// ``` 133 | /// use diana::{ 134 | /// stream, 135 | /// graphql_utils::get_stream_for_channel_from_ctx, 136 | /// errors::GQLResult, 137 | /// async_graphql::{Subscription as GQLSubscription, SimpleObject as GQLSimpleObject}, 138 | /// }; 139 | /// use tokio_stream::{Stream, StreamExt}; 140 | /// use serde::Deserialize; 141 | /// 142 | /// #[derive(Deserialize, GQLSimpleObject)] 143 | /// struct User { 144 | /// username: String 145 | /// } 146 | /// 147 | /// #[derive(Default, Clone)] 148 | /// pub struct Subscription; 149 | /// #[GQLSubscription] 150 | /// impl Subscription { 151 | /// async fn new_users( 152 | /// &self, 153 | /// raw_ctx: &async_graphql::Context<'_>, 154 | /// ) -> impl Stream> { 155 | /// // Get a direct stream from the context on a certain channel 156 | /// let stream_result = get_stream_for_channel_from_ctx("new_user", raw_ctx); 157 | /// 158 | /// // We can manipulate the stream using the stream macro from async-stream 159 | /// stream! { 160 | /// let stream = stream_result?; 161 | /// for await message in stream { 162 | /// // Serialise the data as a user 163 | /// let new_user: User = serde_json::from_str(&message).map_err(|_err| "couldn't serialize given data correctly".to_string())?; 164 | /// yield Ok(new_user); 165 | /// } 166 | /// } 167 | /// } 168 | /// } 169 | /// # fn main() {} 170 | /// ``` 171 | /// 172 | pub fn get_stream_for_channel_from_ctx( 173 | channel: &str, 174 | raw_ctx: &async_graphql::Context<'_>, 175 | ) -> Result> { 176 | // Get the PubSub mutably 177 | let mut pubsub = get_pubsub_from_ctx(raw_ctx)?; 178 | // Return a stream on the given channel 179 | Ok(pubsub.subscribe(channel)) 180 | } 181 | 182 | /// Gets authentication data from the context of a GraphQL resolver. 183 | /// This should only fail if the server is constructed without authentication middleware (which shouldn't be possible with the exposed API 184 | /// surface of this crate). 185 | pub fn get_auth_data_from_ctx<'a>( 186 | raw_ctx: &'a async_graphql::Context<'_>, 187 | ) -> Result<&'a AuthState> { 188 | let auth_state = raw_ctx 189 | .data::() 190 | .map_err(|_err| ErrorKind::GraphQLContextNotFound("auth_state".to_string()))?; 191 | 192 | Ok(auth_state) 193 | } 194 | /// Gets the internal PubSub from the context of a GraphQL resolver. You should never need to use this. 195 | #[doc(hidden)] 196 | pub fn get_pubsub_from_ctx<'a>( 197 | raw_ctx: &'a async_graphql::Context<'_>, 198 | ) -> Result> { 199 | // We store the PubSub instance as a Mutex because we need it sent/synced between threads as a mutable 200 | let pubsub_mutex = raw_ctx 201 | .data::>() 202 | .map_err(|_err| ErrorKind::GraphQLContextNotFound("pubsub".to_string()))?; 203 | let pubsub = pubsub_mutex 204 | .lock() 205 | .map_err(|_err| ErrorKind::MutexPoisoned("pubsub".to_string()))?; 206 | 207 | Ok(pubsub) 208 | } 209 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![forbid(unsafe_code)] 2 | #![deny(missing_docs)] 3 | 4 | /*! 5 | Diana is an out-of-the-box fully-fledged GraphQL system with inbuilt support for commonly-used features like subscriptions and authentication. 6 | It was built to allow a simple but fully-featured GraphQL system to be very quickly created for systems that have complex data structures 7 | and no time for boilerplate. 8 | 9 | Diana builds on the fantastic work of [async_graphql](https://crates.io/crates/async_graphql) to provide an architecture that allows you 10 | to run queries and mutations **serverlessly**, with subscriptions running on serverful infrastructure. To achieve this, Diana uses an 11 | integrations system, whereby the core [`DianaHandler`] logic is used to create high-level wrappers for common deployment systems like 12 | Actix Web and AWS Lambda (including its derivatives, like Netlify). The communication between the serverless and serverful systems is done 13 | for you, exposing a simple, automatically authenticated publishing API. 14 | 15 | In development, Diana supports setting up one server for queries/mutations and another for subscriptions. When it comes time to go serverless, 16 | you just change one file! 17 | 18 | This documentation does not contain examples, which can be found in the [GitHub repository](https://github.com/arctic-hen7/diana). More 19 | detailed explanations and tutorials can be found in the [book](https://arctic-hen7.github.io/diana). 20 | */ 21 | 22 | mod auth; 23 | mod diana_handler; 24 | /// The module for errors and results. This uses [error_chain] behind the scenes. 25 | /// You'll also find [`GQLResult`](crate::errors::GQLResult) and [`GQLError`](crate::errors::Error) in here, which may be useful in working 26 | /// with your own resolvers. 27 | pub mod errors; 28 | mod graphql; 29 | /// The module for utility functions for schema development. 30 | pub mod graphql_utils; 31 | mod options; 32 | mod pubsub; 33 | 34 | // Public exports accessible from the root (everything the user will need) 35 | pub use crate::auth::auth_state::{AuthState, AuthToken}; 36 | pub use crate::auth::core::{AuthBlockLevel, AuthVerdict}; 37 | pub use crate::auth::jwt::{ 38 | create_jwt, decode_time_str, get_jwt_secret, validate_and_decode_jwt, Claims, JWTSecret, 39 | }; 40 | pub use crate::diana_handler::{DianaHandler, DianaResponse, SysSchema}; 41 | pub use crate::options::{Options, OptionsBuilder}; 42 | pub use crate::pubsub::Publisher; 43 | 44 | // Users shouldn't have to install `async_graphql` themselves for basic usage 45 | #[doc(no_inline)] 46 | pub use async_graphql; 47 | // Other stuff users shouldn't have to install for basic use 48 | #[doc(no_inline)] 49 | pub use async_stream::stream; // The `stream!` macro 50 | #[doc(no_inline)] 51 | pub use tokio_stream::{Stream, StreamExt}; // For subscriptions 52 | -------------------------------------------------------------------------------- /src/options.rs: -------------------------------------------------------------------------------- 1 | // Contains the logic to actually create the GraphQL server that the user will use 2 | // This file does not include any logic for the subscriptions server 3 | 4 | // TODO convert the JWT to a fully-fledged secret straight away from here for efficiency 5 | 6 | use async_graphql::{ObjectType, SubscriptionType}; 7 | use std::any::Any; 8 | 9 | use crate::auth::core::AuthBlockLevel; 10 | use crate::errors::*; 11 | pub use crate::graphql::{SubscriptionsServerInformation, UserSchema}; 12 | 13 | /// The options for creating the normal server, subscriptions server, and serverless function. 14 | /// You should define your options in one file and then import them everywhere you need them. 15 | /// You should use `::builder()` to construct this. 16 | #[derive(Clone)] 17 | pub struct Options 18 | where 19 | C: Any + Send + Sync + Clone, 20 | Q: Clone + ObjectType + 'static, 21 | M: Clone + ObjectType + 'static, 22 | S: Clone + SubscriptionType + 'static, 23 | { 24 | /// Your custom context, often a database connection pool 25 | pub ctx: C, 26 | /// Data about the subscriptions server 27 | /// If you're not using subscriptions at all, the mechanics to connect to such a server will be disabled automatically. 28 | pub subscriptions_server_data: Option, 29 | /// Your `async_graphql` schema. See the book for details on how to create a schema. 30 | pub schema: UserSchema, 31 | /// The JWT secret for authenticating and generating client tokens and communications with the subscriptions server. 32 | /// This should be stored in an environment variable and randomly generated (see the book). 33 | pub jwt_secret: String, 34 | /// The blocking level that will be used for the GraphQL endpoint. 35 | /// See [`AuthBlockLevel`] for available blocklevels and their meanings. 36 | /// The default here is to block anything that is not authenticated. 37 | pub authentication_block_state: AuthBlockLevel, 38 | /// The endpoint for the GraphiQL playground. 39 | /// If nothing is provided here, the playground will be disabled. 40 | /// Not supported in production 41 | pub playground_endpoint: Option, 42 | /// The GraphQL endpoint location. By default `/graphql`. 43 | pub graphql_endpoint: String, 44 | } 45 | impl Options 46 | where 47 | C: Any + Send + Sync + Clone, 48 | Q: Clone + ObjectType + 'static, 49 | M: Clone + ObjectType + 'static, 50 | S: Clone + SubscriptionType + 'static, 51 | { 52 | /// Creates a new builder-style struct to create the and instance of [`Options`]. 53 | pub fn builder() -> OptionsBuilder { 54 | OptionsBuilder::default() 55 | } 56 | } 57 | 58 | /// A builder-style struct to create an instance of [`Options`] idiomatically. 59 | #[derive(Clone)] 60 | pub struct OptionsBuilder 61 | where 62 | C: Any + Send + Sync + Clone, 63 | Q: Clone + ObjectType + 'static, 64 | M: Clone + ObjectType + 'static, 65 | S: Clone + SubscriptionType + 'static, 66 | { 67 | ctx: Option, 68 | use_subscriptions_server: bool, 69 | subscriptions_server_hostname: Option, // The real property actually does take an Option for this one 70 | subscriptions_server_port: Option, // The real property actually does take an Option for this one 71 | subscriptions_server_endpoint: Option, // The real property actually does take an Option for this one 72 | subscriptions_server_jwt_to_connect: Option, // The real property actually does take an Option for this one 73 | schema: Option>, 74 | jwt_secret: Option, 75 | authentication_block_state: Option, 76 | playground_endpoint: Option, // The real property actually does take an Option for this one 77 | graphql_endpoint: Option, 78 | } 79 | impl Default for OptionsBuilder 80 | where 81 | C: Any + Send + Sync + Clone, 82 | Q: Clone + ObjectType + 'static, 83 | M: Clone + ObjectType + 'static, 84 | S: Clone + SubscriptionType + 'static, 85 | { 86 | fn default() -> Self { 87 | // By default, if we're in development we'll have a playground, and not in production 88 | let playground_endpoint = match cfg!(debug_assertions) { 89 | true => Some("/graphiql".to_string()), 90 | false => None, 91 | }; 92 | 93 | Self { 94 | ctx: None, 95 | use_subscriptions_server: false, // Most systems won't actually use subscriptions 96 | subscriptions_server_hostname: None, 97 | subscriptions_server_port: None, 98 | subscriptions_server_endpoint: None, 99 | subscriptions_server_jwt_to_connect: None, 100 | schema: None, 101 | jwt_secret: None, 102 | authentication_block_state: None, 103 | playground_endpoint, 104 | graphql_endpoint: Some("/graphql".to_string()), 105 | } 106 | } 107 | } 108 | impl OptionsBuilder 109 | where 110 | C: Any + Send + Sync + Clone, 111 | Q: Clone + ObjectType + 'static, 112 | M: Clone + ObjectType + 'static, 113 | S: Clone + SubscriptionType + 'static, 114 | { 115 | /// Creates a new builder. You'll need to then call the other methods to set everything up. 116 | pub fn new() -> Self { 117 | Self::default() 118 | } 119 | 120 | // Here begin the functions to build the options 121 | /// Defines the context to pass to all GraphQL resolvers. This is typically a database pool, but it can really be anything that can 122 | /// be safely sent between threads. 123 | /// If you don't want to use context, you'll need to set something here anyway (for now). 124 | pub fn ctx(mut self, ctx: C) -> Self { 125 | self.ctx = Some(ctx); 126 | self 127 | } 128 | /// Defines the JWT secret that will be used to authenticate client tokens and communication with the subscriptions server. 129 | /// This should be randomly generated (see the book). 130 | pub fn jwt_secret(mut self, jwt_secret: &str) -> Self { 131 | self.jwt_secret = Some(jwt_secret.to_string()); 132 | self 133 | } 134 | /// Defines the blocklevel for the GraphQL endpoint. See [`AuthBlockLevel`] for more details. 135 | pub fn auth_block_state(mut self, authentication_block_state: AuthBlockLevel) -> Self { 136 | self.authentication_block_state = Some(authentication_block_state); 137 | self 138 | } 139 | /// Defines your custom schema. 140 | /// The query/mutation roots will be inserted into the queries/mutations server/function and the subscription root will be inserted 141 | /// into the subscriptions server. These should be specified using `async_graphql` as per the book. 142 | pub fn schema(mut self, query_root: Q, mutation_root: M, subscription_root: S) -> Self { 143 | self.schema = Some(UserSchema { 144 | query_root, 145 | mutation_root, 146 | subscription_root, 147 | }); 148 | self 149 | } 150 | /// Explicitly enables the subscriptions server. You shouldn't every need to call this, as calling any methods that define settings 151 | /// for the subscriptions server will automatically enable it. 152 | pub fn use_subscriptions_server(mut self) -> Self { 153 | self.use_subscriptions_server = true; 154 | self 155 | } 156 | /// Defines the hsotname on which the subscriptions server will be contacted. 157 | pub fn subscriptions_server_hostname(mut self, subscriptions_server_hostname: &str) -> Self { 158 | self.subscriptions_server_hostname = Some(subscriptions_server_hostname.to_string()); 159 | self.use_subscriptions_server = true; 160 | self 161 | } 162 | /// Defines the port on which the subscriptions server will be contacted. 163 | pub fn subscriptions_server_port(mut self, subscriptions_server_port: &str) -> Self { 164 | self.subscriptions_server_port = Some(subscriptions_server_port.to_string()); 165 | self.use_subscriptions_server = true; 166 | self 167 | } 168 | /// Defines the GraphQL endpoint for the subscriptions server. 169 | /// The GraphiQL playground endpoint inherits from the `.playground_endpoint()` setting. 170 | pub fn subscriptions_server_endpoint(mut self, subscriptions_server_endpoint: &str) -> Self { 171 | self.subscriptions_server_endpoint = Some(subscriptions_server_endpoint.to_string()); 172 | self.use_subscriptions_server = true; 173 | self 174 | } 175 | /// Specifies the JWT which will be used by the queries/mutations system to connect to the subscriptions server. 176 | /// This should be generated based off the same secret as you specify for the queries/mutations system (TODO security review of that architecture). 177 | pub fn jwt_to_connect_to_subscriptions_server( 178 | mut self, 179 | subscriptions_server_jwt_to_connect: &str, 180 | ) -> Self { 181 | self.subscriptions_server_jwt_to_connect = 182 | Some(subscriptions_server_jwt_to_connect.to_string()); 183 | self.use_subscriptions_server = true; 184 | self 185 | } 186 | /// Defines the GraphiQL playground endpoint. 187 | /// In development, this is not required and will default to `/graphiql`. 188 | /// In production, if this has been set we'll throw an error at `.finish()`. 189 | pub fn playground_endpoint(mut self, playground_endpoint: &str) -> Self { 190 | self.playground_endpoint = Some(playground_endpoint.to_string()); 191 | self 192 | } 193 | /// Defines the GraphQL endpoint. This is not required, and defaults to `/graphql`. 194 | pub fn graphql_endpoint(mut self, graphql_endpoint: &str) -> Self { 195 | self.graphql_endpoint = Some(graphql_endpoint.to_string()); 196 | self 197 | } 198 | // Here end the functions to build the options 199 | 200 | /// Builds the final options, consuming `self`. 201 | /// This will return an error if you haven't set something required up. 202 | pub fn finish(self) -> Result> { 203 | // If the playground has been enabled in production, throw 204 | if !cfg!(debug_assertions) && self.playground_endpoint.is_some() { 205 | bail!(ErrorKind::AttemptedPlaygroundInProduction); 206 | } 207 | 208 | let opts = Options { 209 | ctx: self.ctx.ok_or(ErrorKind::IncompleteBuilderFields)?, 210 | subscriptions_server_data: match self.use_subscriptions_server { 211 | true => Some(SubscriptionsServerInformation { 212 | hostname: self 213 | .subscriptions_server_hostname 214 | .ok_or(ErrorKind::IncompleteBuilderFields)?, 215 | port: self 216 | .subscriptions_server_port 217 | .ok_or(ErrorKind::IncompleteBuilderFields)?, 218 | endpoint: self 219 | .subscriptions_server_endpoint 220 | .ok_or(ErrorKind::IncompleteBuilderFields)?, 221 | jwt_to_connect: self 222 | .subscriptions_server_jwt_to_connect 223 | .ok_or(ErrorKind::IncompleteBuilderFields)?, 224 | }), 225 | false => None, 226 | }, 227 | schema: self.schema.ok_or(ErrorKind::IncompleteBuilderFields)?, 228 | jwt_secret: self.jwt_secret.ok_or(ErrorKind::IncompleteBuilderFields)?, 229 | authentication_block_state: self 230 | .authentication_block_state 231 | .ok_or(ErrorKind::IncompleteBuilderFields)?, 232 | playground_endpoint: self.playground_endpoint, // This can be an option (we may not have a playground at all) 233 | graphql_endpoint: self 234 | .graphql_endpoint 235 | .ok_or(ErrorKind::IncompleteBuilderFields)?, 236 | }; 237 | 238 | Ok(opts) 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/pubsub.rs: -------------------------------------------------------------------------------- 1 | // This module defines a simple publish-subscribe structure, though one designed to run across the web 2 | // The publishing and subscribing are done on different servers/functions 3 | 4 | use async_stream::stream; 5 | use reqwest::Client; 6 | use serde::{Deserialize, Serialize}; 7 | use std::collections::HashMap; 8 | use tokio::sync::broadcast::{channel as create_channel, Sender}; 9 | use tokio_stream::Stream; 10 | 11 | use crate::errors::*; 12 | 13 | const MESSAGES_TO_BE_RETAINED: usize = 5; 14 | 15 | #[derive(Serialize)] 16 | struct GQLQueryBody { 17 | query: String, 18 | variables: T, 19 | } 20 | 21 | #[derive(Deserialize)] 22 | struct GQLPublishResponse { 23 | data: PublishResponse, 24 | } 25 | #[derive(Deserialize)] 26 | struct PublishResponse { 27 | publish: bool, 28 | } 29 | 30 | /// The system that publishes data from the queries/mutations system to the subscriptions server. 31 | /// These communications are secured by a JWT specified in [`Options`](crate::Options). 32 | /// This is automatically created from the [`Options`](crate::Options) and passed to all resolvers. You should never need to manually create it. 33 | pub struct Publisher { 34 | client: Client, 35 | address: String, 36 | token: String, 37 | } 38 | impl Publisher { 39 | /// Creates a new publisher. This is done for you when you create the queries/mutations system, so you should never need to call this. 40 | pub fn new(hostname: String, port: String, endpoint: String, token: String) -> Result { 41 | let address = format!( 42 | "{hostname}:{port}{endpoint}", // The endpoint should start with '/' 43 | hostname = hostname, 44 | port = port, 45 | endpoint = endpoint 46 | ); 47 | 48 | let client = Client::new(); 49 | 50 | Ok(Self { 51 | client, 52 | address, 53 | token, 54 | }) 55 | } 56 | 57 | /// Sends the given data to the subscriptions server on the given channel. In-depth information about this process is available in the book. 58 | /// You should use [serde] to serialize anything sent here as a string (this won't be done for you). It should then be deserialized in the 59 | /// appropriate subscription (which will listen for messages from here indirectly). 60 | /// This function will return an error if the subscriptions server was unavailable or didn't correctly acknowledge the request. 61 | /// # Example 62 | /// ``` 63 | /// use diana::{ 64 | /// async_graphql::{Object as GQLObject, InputObject as GQLInputObject, SimpleObject as GQLSimpleObject}, 65 | /// errors::GQLResult, 66 | /// Publisher, 67 | /// }; 68 | /// use serde::Serialize; 69 | /// 70 | /// #[derive(Serialize, GQLSimpleObject)] 71 | /// struct User { 72 | /// username: String, 73 | /// } 74 | /// #[derive(Serialize, GQLInputObject)] 75 | /// struct UserInput { 76 | /// username: String, 77 | /// } 78 | /// 79 | /// #[derive(Default, Clone)] 80 | /// pub struct Mutation {} 81 | /// #[GQLObject] 82 | /// impl Mutation { 83 | /// async fn add_user( 84 | /// &self, 85 | /// ctx: &async_graphql::Context<'_>, 86 | /// new_user: UserInput, 87 | /// ) -> GQLResult { 88 | /// // Your code to add the new user 89 | /// 90 | /// // Notify the subscriptions server that a new user has been added 91 | /// let publisher = ctx.data::()?; 92 | /// let user_json = serde_json::to_string(&new_user).unwrap(); // GraphQL has already checked for ill-formation 93 | /// publisher.publish("new_user", user_json.to_string()).await?; 94 | /// 95 | /// Ok(User { 96 | /// username: new_user.username 97 | /// }) // In reality, you'd probably return the user that's just been created 98 | /// } 99 | /// } 100 | /// 101 | /// # fn main() {} 102 | /// ``` 103 | pub async fn publish(&self, channel: &str, data: String) -> Result<()> { 104 | let client = &self.client; 105 | 106 | // Create the query body with a HashMap of variables 107 | let mut variables = HashMap::new(); 108 | variables.insert("channel", channel.to_string()); 109 | variables.insert("data", data); 110 | 111 | let body = GQLQueryBody { 112 | query: " 113 | mutation PublishData($channel: String!, $data: String!) { 114 | publish( 115 | channel: $channel, 116 | data: $data 117 | ) 118 | } 119 | " 120 | .to_string(), 121 | variables, 122 | }; 123 | 124 | let res = client 125 | .post(&self.address) 126 | .json(&body) 127 | .header("Authorization", "Bearer ".to_string() + &self.token) 128 | .send() 129 | .await 130 | .map_err(|_| ErrorKind::SubscriptionDataPublishFailed)?; 131 | 132 | // Handle if the request wasn't successful on an HTTP level 133 | if res.status().to_string() != "200 OK" { 134 | bail!(ErrorKind::SubscriptionDataPublishFailed) 135 | } 136 | 137 | // Get the body out (data still stringified though, that's handled by resolvers) 138 | let body: GQLPublishResponse = serde_json::from_str( 139 | &res.text() 140 | .await 141 | .map_err(|_| ErrorKind::SubscriptionDataPublishFailed)?, 142 | ) 143 | .map_err(|_| ErrorKind::SubscriptionDataPublishFailed)?; 144 | 145 | // Confirm nothing's gone wrong on a GraphQL level (basically only if we got `false` instead of `true`) 146 | match body.data.publish { 147 | true => Ok(()), 148 | _ => bail!(ErrorKind::SubscriptionDataPublishFailed), 149 | } 150 | } 151 | } 152 | 153 | // Everything from here down operates solely on the subscriptions server, and is stateful! 154 | // Do NOT import these mechanisms in the serverless system! 155 | 156 | // This is a traditional PubSub implementation using Tokio's broadcast system 157 | // This doesn't need to be made available because it's entirely internal 158 | pub struct PubSub { 159 | // A hash map of channels to their Tokio broadcasters 160 | channels: HashMap>, 161 | } 162 | impl Default for PubSub { 163 | fn default() -> Self { 164 | Self { 165 | channels: HashMap::new(), 166 | } 167 | } 168 | } 169 | impl PubSub { 170 | // Gets a channel or creates a new one if needed 171 | fn get_channel(&mut self, channel: &str) -> Sender { 172 | let channel_sender = self.channels.get(channel); 173 | let channel_sender = match channel_sender { 174 | Some(sub) => sub, 175 | None => { 176 | let (channel_sender, _receiver) = create_channel(MESSAGES_TO_BE_RETAINED); 177 | self.channels.insert(channel.to_string(), channel_sender); // We put a clone into the HashMap because broadcast can be multi-producer 178 | self.channels.get(channel).unwrap() // We just added it, we know more than the compiler 179 | } 180 | }; 181 | 182 | channel_sender.clone() 183 | } 184 | 185 | pub fn subscribe(&mut self, channel: &str) -> impl Stream { 186 | let channel_sender = self.get_channel(channel); 187 | let mut receiver = channel_sender.subscribe(); 188 | 189 | stream! { 190 | loop { 191 | let message = receiver.recv().await; 192 | match message { 193 | Ok(message) => yield message, 194 | _ => continue 195 | } 196 | } 197 | } 198 | } 199 | 200 | // Creates a new sender for a given channel name if one doesn't exist and then sends a message using it 201 | pub fn publish(&mut self, channel: &str, data: String) { 202 | let channel_sender = self.get_channel(channel); 203 | // This will fail only if there are now receivers, but we don't care if that's the case 204 | let _ = channel_sender.send(data); 205 | } 206 | 207 | // Drops the handle to a sender for the given channel 208 | // All receiver calls after this point will result in a closed channel error 209 | // This doesn't need to be explicitly called normally 210 | pub fn close_channel(&mut self, channel: &str) { 211 | self.channels.remove(channel); 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /tests/diana_handler.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{EmptyMutation, EmptySubscription, Object as GQLObject}; 2 | use diana::{ 3 | create_jwt, decode_time_str, get_jwt_secret, AuthBlockLevel, AuthVerdict, DianaHandler, 4 | DianaResponse, Options, SysSchema, 5 | }; 6 | use std::collections::HashMap; 7 | 8 | #[derive(Clone)] 9 | struct Context { 10 | prop: String, 11 | } 12 | 13 | #[derive(Clone)] 14 | struct Query {} 15 | #[GQLObject] 16 | impl Query { 17 | async fn query(&self) -> bool { 18 | true 19 | } 20 | } 21 | 22 | const JWT_SECRET: &str = "thisisaterriblesecretthatshouldberandomlygeneratedseethebook"; 23 | const SIMPLE_QUERY: &str = "{\"query\": \"query { query }\"}"; 24 | const SIMPLE_QUERY_RES: &str = "{\"data\":{\"query\":true}}"; 25 | const SIMPLE_INVALID_QUERY: &str = "{\"query\": \"query { thisisnotaquery }\"}"; 26 | const SIMPLE_INVALID_QUERY_RES: &str = "{\"data\":null,\"errors\":[{\"message\":\"Unknown field \\\"thisisnotaquery\\\" on type \\\"Query\\\".\",\"locations\":[{\"line\":1,\"column\":9}]}]}"; 27 | 28 | fn get_opts( 29 | auth_block_level: AuthBlockLevel, 30 | ) -> Options { 31 | Options::builder() 32 | .ctx(Context { 33 | prop: "connection".to_string(), 34 | }) 35 | .subscriptions_server_hostname("http://localhost") 36 | .subscriptions_server_port("9002") 37 | .subscriptions_server_endpoint("/graphql") 38 | .jwt_to_connect_to_subscriptions_server("SUBSCRIPTIONS_SERVER_PUBLISH_JWT") 39 | .auth_block_state(auth_block_level) 40 | .jwt_secret(JWT_SECRET) 41 | .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 42 | .graphql_endpoint("/graphql") 43 | .playground_endpoint("/graphiql") 44 | .finish() 45 | .unwrap() 46 | } 47 | 48 | fn get_valid_auth_header() -> Option { 49 | let secret = get_jwt_secret(JWT_SECRET.to_string()).unwrap(); 50 | let mut claims = HashMap::new(); 51 | claims.insert("role".to_string(), "test".to_string()); 52 | let exp = decode_time_str("1m").unwrap(); // The created JWT will be valid for 1 minute 53 | let jwt = create_jwt(claims, &secret, exp).unwrap(); 54 | Some("Bearer ".to_string() + &jwt) 55 | } 56 | 57 | fn get_invalid_auth_header<'a>() -> Option<&'a str> { 58 | Some("Bearer thisisaninvalidjwt") 59 | } 60 | 61 | // Tests for `.new()` 62 | #[test] 63 | fn returns_valid_handler() { 64 | if !matches!( 65 | DianaHandler::new(get_opts(AuthBlockLevel::AllowAll)), 66 | Ok(DianaHandler { .. }) 67 | ) { 68 | panic!("Didn't return valid DianaHandler instance.") 69 | } 70 | if !matches!( 71 | DianaHandler::new(get_opts(AuthBlockLevel::AllowMissing)), 72 | Ok(DianaHandler { .. }) 73 | ) { 74 | panic!("Didn't return valid DianaHandler instance.") 75 | } 76 | if !matches!( 77 | DianaHandler::new(get_opts(AuthBlockLevel::BlockUnauthenticated)), 78 | Ok(DianaHandler { .. }) 79 | ) { 80 | panic!("Didn't return valid DianaHandler instance.") 81 | } 82 | } 83 | #[test] 84 | fn returns_valid_handler_with_no_subscriptions() { 85 | let opts = Options::builder() 86 | .ctx(Context { 87 | prop: "connection".to_string(), 88 | }) 89 | .auth_block_state(AuthBlockLevel::AllowAll) 90 | .jwt_secret(JWT_SECRET) 91 | .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 92 | .graphql_endpoint("/graphql") 93 | .playground_endpoint("/graphiql") 94 | .finish() 95 | .unwrap(); 96 | let diana_handler = DianaHandler::new(opts); 97 | if !matches!(diana_handler, Ok(DianaHandler { .. })) { 98 | panic!("Didn't return valid DianaHandler instance.") 99 | } 100 | } 101 | // Tests for `.is_authed()` 102 | #[test] 103 | fn allows_user_if_token_valid_for_block_unauthenticated_block_state() { 104 | let diana_handler = DianaHandler::new(get_opts(AuthBlockLevel::BlockUnauthenticated)).unwrap(); 105 | let verdict = diana_handler.is_authed(get_valid_auth_header()); 106 | if !matches!(verdict, AuthVerdict::Allow(_)) { 107 | panic!( 108 | "Didn't return correct AuthVerdict response. Expected AuthVerdict::Allow, got {:?}", 109 | verdict 110 | ) 111 | } 112 | } 113 | #[test] 114 | fn blocks_user_if_token_invalid_for_block_unauthenticated_block_state() { 115 | let diana_handler = DianaHandler::new(get_opts(AuthBlockLevel::BlockUnauthenticated)).unwrap(); 116 | let verdict = diana_handler.is_authed(get_invalid_auth_header()); 117 | if !matches!(verdict, AuthVerdict::Block) { 118 | panic!( 119 | "Didn't return correct AuthVerdict response. Expected AuthVerdict::Block, got {:?}", 120 | verdict 121 | ) 122 | } 123 | } 124 | #[test] 125 | fn blocks_user_if_token_missing_for_block_unauthenticated_block_state() { 126 | let diana_handler = DianaHandler::new(get_opts(AuthBlockLevel::BlockUnauthenticated)).unwrap(); 127 | let verdict = diana_handler.is_authed(Option::::None); 128 | if !matches!(verdict, AuthVerdict::Block) { 129 | panic!( 130 | "Didn't return correct AuthVerdict response. Expected AuthVerdict::Block, got {:?}", 131 | verdict 132 | ) 133 | } 134 | } 135 | #[test] 136 | fn allows_user_if_token_valid_for_allow_all_block_state() { 137 | let diana_handler = DianaHandler::new(get_opts(AuthBlockLevel::AllowAll)).unwrap(); 138 | let verdict = diana_handler.is_authed(get_valid_auth_header()); 139 | if !matches!(verdict, AuthVerdict::Allow(_)) { 140 | panic!( 141 | "Didn't return correct AuthVerdict response. Expected AuthVerdict::Allow, got {:?}", 142 | verdict 143 | ) 144 | } 145 | } 146 | #[test] 147 | fn allow_user_if_token_invalid_for_allow_all_block_state() { 148 | let diana_handler = DianaHandler::new(get_opts(AuthBlockLevel::AllowAll)).unwrap(); 149 | let verdict = diana_handler.is_authed(get_invalid_auth_header()); 150 | if !matches!(verdict, AuthVerdict::Allow(_)) { 151 | panic!( 152 | "Didn't return correct AuthVerdict response. Expected AuthVerdict::Allow, got {:?}", 153 | verdict 154 | ) 155 | } 156 | } 157 | #[test] 158 | fn allow_user_if_token_missing_for_allow_all_block_state() { 159 | let diana_handler = DianaHandler::new(get_opts(AuthBlockLevel::AllowAll)).unwrap(); 160 | let verdict = diana_handler.is_authed(Option::::None); 161 | if !matches!(verdict, AuthVerdict::Allow(_)) { 162 | panic!( 163 | "Didn't return correct AuthVerdict response. Expected AuthVerdict::Allow, got {:?}", 164 | verdict 165 | ) 166 | } 167 | } 168 | #[test] 169 | fn allows_user_if_token_valid_for_allow_missing_block_state() { 170 | let diana_handler = DianaHandler::new(get_opts(AuthBlockLevel::AllowMissing)).unwrap(); 171 | let verdict = diana_handler.is_authed(get_valid_auth_header()); 172 | if !matches!(verdict, AuthVerdict::Allow(_)) { 173 | panic!( 174 | "Didn't return correct AuthVerdict response. Expected AuthVerdict::Allow, got {:?}", 175 | verdict 176 | ) 177 | } 178 | } 179 | #[test] 180 | fn blocks_user_if_token_invalid_for_allow_missing_block_state() { 181 | let diana_handler = DianaHandler::new(get_opts(AuthBlockLevel::AllowMissing)).unwrap(); 182 | let verdict = diana_handler.is_authed(get_invalid_auth_header()); 183 | if !matches!(verdict, AuthVerdict::Block) { 184 | panic!( 185 | "Didn't return correct AuthVerdict response. Expected AuthVerdict::Block, got {:?}", 186 | verdict 187 | ) 188 | } 189 | } 190 | #[test] 191 | fn allows_user_if_token_missing_for_allow_missing_block_state() { 192 | let diana_handler = DianaHandler::new(get_opts(AuthBlockLevel::AllowMissing)).unwrap(); 193 | let verdict = diana_handler.is_authed(Option::::None); 194 | if !matches!(verdict, AuthVerdict::Allow(_)) { 195 | panic!( 196 | "Didn't return correct AuthVerdict response. Expected AuthVerdict::Allow, got {:?}", 197 | verdict 198 | ) 199 | } 200 | } 201 | // Tests for `.run_stateless_req()` (internal function that underlies other simpler querying logic) 202 | #[tokio::test] 203 | async fn returns_success_on_valid_auth_and_body() { 204 | let diana_handler = DianaHandler::new(get_opts(AuthBlockLevel::BlockUnauthenticated)).unwrap(); 205 | let res = diana_handler 206 | .run_stateless_req( 207 | SysSchema::WithoutSubscriptions, 208 | SIMPLE_QUERY.to_string(), 209 | get_valid_auth_header(), 210 | None, 211 | ) 212 | .await; 213 | if !matches!(res.clone(), DianaResponse::Success(val) if val == SIMPLE_QUERY_RES) { 214 | panic!("Didn't return correct DianaResponse variant. Expected DianaResponse::Success, got {:?}", res) 215 | } 216 | } 217 | #[tokio::test] 218 | async fn returns_success_with_error_embedded_on_valid_auth_and_invalid_body() { 219 | let diana_handler = DianaHandler::new(get_opts(AuthBlockLevel::BlockUnauthenticated)).unwrap(); 220 | let res = diana_handler 221 | .run_stateless_req( 222 | SysSchema::WithoutSubscriptions, 223 | SIMPLE_INVALID_QUERY.to_string(), 224 | get_valid_auth_header(), 225 | None, 226 | ) 227 | .await; 228 | if !matches!(res.clone(), DianaResponse::Success(val) if val == SIMPLE_INVALID_QUERY_RES) { 229 | panic!("Didn't return correct DianaResponse variant. Expected DianaResponse::Success, got {:?}", res) 230 | } 231 | } 232 | #[tokio::test] 233 | async fn returns_blocked_on_invalid_auth_and_valid_body() { 234 | let diana_handler = DianaHandler::new(get_opts(AuthBlockLevel::BlockUnauthenticated)).unwrap(); 235 | let res = diana_handler 236 | .run_stateless_req( 237 | SysSchema::WithoutSubscriptions, 238 | SIMPLE_QUERY.to_string(), 239 | get_invalid_auth_header(), 240 | None, 241 | ) 242 | .await; 243 | if !matches!(res.clone(), DianaResponse::Blocked) { 244 | panic!("Didn't return correct DianaResponse variant. Expected DianaResponse::Blocked, got {:?}", res) 245 | } 246 | } 247 | #[tokio::test] 248 | async fn returns_blocked_on_invalid_auth_and_invalid_body() { 249 | let diana_handler = DianaHandler::new(get_opts(AuthBlockLevel::BlockUnauthenticated)).unwrap(); 250 | let res = diana_handler 251 | .run_stateless_req( 252 | SysSchema::WithoutSubscriptions, 253 | SIMPLE_INVALID_QUERY.to_string(), 254 | get_invalid_auth_header(), 255 | None, 256 | ) 257 | .await; 258 | if !matches!(res.clone(), DianaResponse::Blocked) { 259 | panic!("Didn't return correct DianaResponse variant. Expected DianaResponse::Blocked, got {:?}", res) 260 | } 261 | } 262 | -------------------------------------------------------------------------------- /tests/jwt.rs: -------------------------------------------------------------------------------- 1 | use chrono::{Duration, Utc}; 2 | use diana::{create_jwt, decode_time_str, get_jwt_secret, validate_and_decode_jwt}; 3 | use std::collections::HashMap; 4 | 5 | const JWT_SECRET: &str = "thisisaterriblesecretthatshouldberandomlygeneratedseethebook"; 6 | const NUM_INTERVAL_LENGTHS_TO_TEST: i64 = 1000; 7 | 8 | // Tests for `get_jwt_secret` 9 | #[test] 10 | fn returns_secret_if_valid() { 11 | let secret_str = JWT_SECRET.to_string(); 12 | let secret = get_jwt_secret(secret_str); 13 | if !matches!(secret, Ok(_)) { 14 | panic!("Expected Ok, found {:?}", secret); 15 | } 16 | } 17 | #[test] 18 | fn returns_error_if_invalid() { 19 | let secret_str = "!@#$%^&*()".to_string(); // That's not valid base64... 20 | let secret = get_jwt_secret(secret_str); 21 | if !matches!(secret, Err(_)) { 22 | panic!("Expected Err, found {:?}", secret); 23 | } 24 | } 25 | // Tests for `decode_time_str` 26 | // A testing utility macro to test all the intervals with `decode_time_str` for a given length 27 | macro_rules! test_all_intervals_for_length( 28 | ($length:expr) => { 29 | { 30 | assert_eq!( 31 | decode_time_str(($length.to_string() + "s").as_str()).unwrap(), 32 | (Utc::now() + Duration::seconds($length)).timestamp() as u64 33 | ); 34 | assert_eq!( 35 | decode_time_str(($length.to_string() + "m").as_str()).unwrap(), 36 | (Utc::now() + Duration::minutes($length)).timestamp() as u64 37 | ); 38 | assert_eq!( 39 | decode_time_str(($length.to_string() + "h").as_str()).unwrap(), 40 | (Utc::now() + Duration::hours($length)).timestamp() as u64 41 | ); 42 | assert_eq!( 43 | decode_time_str(($length.to_string() + "d").as_str()).unwrap(), 44 | (Utc::now() + Duration::days($length)).timestamp() as u64 45 | ); 46 | assert_eq!( 47 | decode_time_str(($length.to_string() + "w").as_str()).unwrap(), 48 | (Utc::now() + Duration::weeks($length)).timestamp() as u64 49 | ); 50 | assert_eq!( 51 | decode_time_str(($length.to_string() + "M").as_str()).unwrap(), 52 | (Utc::now() + Duration::days($length * 30)).timestamp() as u64 53 | ); 54 | assert_eq!( 55 | decode_time_str(($length.to_string() + "y").as_str()).unwrap(), 56 | (Utc::now() + Duration::days($length * 365)).timestamp() as u64 57 | ); 58 | } 59 | }; 60 | ); 61 | #[test] 62 | fn returns_correct_datetime_for_each_interval_at_length_1() { 63 | // We test each interval directly with the above macro 64 | test_all_intervals_for_length!(1); 65 | } 66 | #[test] 67 | fn returns_correct_datetime_for_each_interval_at_many_length() { 68 | // We test each interval directly with the above macro 69 | // This tests runs for many intervals 70 | for length in 1..NUM_INTERVAL_LENGTHS_TO_TEST { 71 | test_all_intervals_for_length!(length); 72 | } 73 | } 74 | #[test] 75 | fn returns_error_on_invalid_interval() { 76 | let decoded = decode_time_str("1q"); 77 | if decoded.is_ok() { 78 | panic!("Didn't panic on time string with invalid interval."); 79 | } 80 | } 81 | // Tests for `create_jwt` 82 | #[test] 83 | fn returns_valid_jwt() { 84 | let mut claims = HashMap::new(); 85 | claims.insert("role".to_string(), "test".to_string()); 86 | let secret = get_jwt_secret(JWT_SECRET.to_string()).unwrap(); 87 | let exp = decode_time_str("1w").unwrap(); 88 | let jwt = create_jwt(claims.clone(), &secret, exp); 89 | if !matches!(jwt, Ok(_)) { 90 | panic!("Expected Ok, found {:?}", jwt); 91 | } 92 | 93 | let extracted_claims = validate_and_decode_jwt(&jwt.unwrap(), &secret); 94 | assert_eq!(extracted_claims.unwrap().claims, claims); 95 | } 96 | // Tests for `validate_and_decode_jwt` (the basic one is done with `create_jwt`) 97 | #[test] 98 | fn returns_error_if_jwt_invalid() { 99 | let secret = get_jwt_secret(JWT_SECRET.to_string()).unwrap(); 100 | 101 | let extracted_claims = validate_and_decode_jwt("thisisaninvalidjwt", &secret); 102 | if !matches!(extracted_claims, None) { 103 | panic!("Expected None, found {:?}", extracted_claims); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/options.rs: -------------------------------------------------------------------------------- 1 | use async_graphql::{EmptyMutation, EmptySubscription, Object as GQLObject}; 2 | use diana::{AuthBlockLevel, Options}; 3 | 4 | #[derive(Clone)] 5 | struct Context { 6 | prop: String, 7 | } 8 | 9 | #[derive(Clone)] 10 | struct Query {} 11 | #[GQLObject] 12 | impl Query { 13 | async fn query(&self) -> bool { 14 | true 15 | } 16 | } 17 | 18 | #[test] 19 | fn returns_valid_options() { 20 | let opts = Options::builder() 21 | .ctx(Context { 22 | prop: "connection".to_string(), 23 | }) 24 | .subscriptions_server_hostname("http://localhost") 25 | .subscriptions_server_port("9002") 26 | .subscriptions_server_endpoint("/graphql") 27 | .jwt_to_connect_to_subscriptions_server("SUBSCRIPTIONS_SERVER_PUBLISH_JWT") 28 | .auth_block_state(AuthBlockLevel::AllowAll) 29 | .jwt_secret("JWT_SECRET") 30 | .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 31 | .graphql_endpoint("/graphql") 32 | .playground_endpoint("/graphiql") 33 | .finish(); 34 | 35 | if !matches!(opts, Ok(Options { .. })) { 36 | panic!("Didn't return valid Options instance.") 37 | } 38 | } 39 | #[test] 40 | fn returns_valid_options_without_subscriptions() { 41 | let opts = Options::builder() 42 | .ctx(Context { 43 | prop: "connection".to_string(), 44 | }) 45 | .auth_block_state(AuthBlockLevel::AllowAll) 46 | .jwt_secret("JWT_SECRET") 47 | .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 48 | .graphql_endpoint("/graphql") 49 | .playground_endpoint("/graphiql") 50 | .finish(); 51 | 52 | if !matches!(opts, Ok(Options { .. })) { 53 | panic!("Didn't return valid Options instance.") 54 | } 55 | } 56 | #[test] 57 | fn uses_default_graphql_endpoint() { 58 | let opts = Options::builder() 59 | .ctx(Context { 60 | prop: "connection".to_string(), 61 | }) 62 | .subscriptions_server_hostname("http://localhost") 63 | .subscriptions_server_port("9002") 64 | .subscriptions_server_endpoint("/graphql") 65 | .jwt_to_connect_to_subscriptions_server("SUBSCRIPTIONS_SERVER_PUBLISH_JWT") 66 | .auth_block_state(AuthBlockLevel::AllowAll) 67 | .jwt_secret("JWT_SECRET") 68 | .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 69 | // .graphql_endpoint("/graphql") 70 | .playground_endpoint("/graphiql") 71 | .finish(); 72 | 73 | if !matches!(opts, Ok(Options { .. })) { 74 | panic!("Didn't return valid Options instance.") 75 | } 76 | assert_eq!(opts.unwrap().graphql_endpoint, "/graphql".to_string()) 77 | } 78 | #[test] 79 | fn uses_default_playground_endpoint() { 80 | let opts = Options::builder() 81 | .ctx(Context { 82 | prop: "connection".to_string(), 83 | }) 84 | .subscriptions_server_hostname("http://localhost") 85 | .subscriptions_server_port("9002") 86 | .subscriptions_server_endpoint("/graphql") 87 | .jwt_to_connect_to_subscriptions_server("SUBSCRIPTIONS_SERVER_PUBLISH_JWT") 88 | .auth_block_state(AuthBlockLevel::AllowAll) 89 | .jwt_secret("JWT_SECRET") 90 | .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 91 | .graphql_endpoint("/graphql") 92 | // .playground_endpoint("/graphiql") 93 | .finish(); 94 | 95 | if !matches!(opts, Ok(Options { .. })) { 96 | panic!("Didn't return valid Options instance.") 97 | } 98 | assert_eq!( 99 | opts.unwrap().playground_endpoint, 100 | Some("/graphiql".to_string()) 101 | ) 102 | } 103 | #[test] 104 | fn returns_error_on_playground_in_production() {} 105 | #[test] 106 | fn returns_error_on_missing_subscriptions_server_fields() { 107 | if matches!( 108 | Options::builder() 109 | .ctx(Context { 110 | prop: "connection".to_string(), 111 | }) 112 | // .subscriptions_server_hostname("http://localhost") 113 | .subscriptions_server_port("9002") 114 | .subscriptions_server_endpoint("/graphql") 115 | .jwt_to_connect_to_subscriptions_server("SUBSCRIPTIONS_SERVER_PUBLISH_JWT") 116 | .auth_block_state(AuthBlockLevel::AllowAll) 117 | .jwt_secret("JWT_SECRET") 118 | .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 119 | .graphql_endpoint("/graphql") 120 | .playground_endpoint("/graphiql") 121 | .finish(), 122 | Ok(Options { .. }) 123 | ) { 124 | panic!("Returned valid options instance, should've been invalid.") 125 | } 126 | if matches!( 127 | Options::builder() 128 | .ctx(Context { 129 | prop: "connection".to_string(), 130 | }) 131 | .subscriptions_server_hostname("http://localhost") 132 | // .subscriptions_server_port("9002") 133 | .subscriptions_server_endpoint("/graphql") 134 | .jwt_to_connect_to_subscriptions_server("SUBSCRIPTIONS_SERVER_PUBLISH_JWT") 135 | .auth_block_state(AuthBlockLevel::AllowAll) 136 | .jwt_secret("JWT_SECRET") 137 | .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 138 | .graphql_endpoint("/graphql") 139 | .playground_endpoint("/graphiql") 140 | .finish(), 141 | Ok(Options { .. }) 142 | ) { 143 | panic!("Returned valid options instance, should've been invalid.") 144 | } 145 | if matches!( 146 | Options::builder() 147 | .ctx(Context { 148 | prop: "connection".to_string(), 149 | }) 150 | .subscriptions_server_hostname("http://localhost") 151 | .subscriptions_server_port("9002") 152 | // .subscriptions_server_endpoint("/graphql") 153 | .jwt_to_connect_to_subscriptions_server("SUBSCRIPTIONS_SERVER_PUBLISH_JWT") 154 | .auth_block_state(AuthBlockLevel::AllowAll) 155 | .jwt_secret("JWT_SECRET") 156 | .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 157 | .graphql_endpoint("/graphql") 158 | .playground_endpoint("/graphiql") 159 | .finish(), 160 | Ok(Options { .. }) 161 | ) { 162 | panic!("Returned valid options instance, should've been invalid.") 163 | } 164 | if matches!( 165 | Options::builder() 166 | .ctx(Context { 167 | prop: "connection".to_string(), 168 | }) 169 | .subscriptions_server_hostname("http://localhost") 170 | .subscriptions_server_port("9002") 171 | .subscriptions_server_endpoint("/graphql") 172 | // .jwt_to_connect_to_subscriptions_server("SUBSCRIPTIONS_SERVER_PUBLISH_JWT") 173 | .auth_block_state(AuthBlockLevel::AllowAll) 174 | .jwt_secret("JWT_SECRET") 175 | .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 176 | .graphql_endpoint("/graphql") 177 | .playground_endpoint("/graphiql") 178 | .finish(), 179 | Ok(Options { .. }) 180 | ) { 181 | panic!("Returned valid options instance, should've been invalid.") 182 | } 183 | } 184 | #[test] 185 | fn returns_error_on_missing_required_fields() { 186 | if matches!( 187 | Options::::builder() 188 | // .ctx(Context { 189 | // prop: "connection".to_string(), 190 | // }) 191 | .auth_block_state(AuthBlockLevel::AllowAll) 192 | .jwt_secret("JWT_SECRET") 193 | .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 194 | .finish(), 195 | Ok(Options { .. }) 196 | ) { 197 | panic!("Returned valid options instance, should've been invalid.") 198 | } 199 | if matches!( 200 | Options::builder() 201 | .ctx(Context { 202 | prop: "connection".to_string(), 203 | }) 204 | // .auth_block_state(AuthBlockLevel::AllowAll) 205 | .jwt_secret("JWT_SECRET") 206 | .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 207 | .finish(), 208 | Ok(Options { .. }) 209 | ) { 210 | panic!("Returned valid options instance, should've been invalid.") 211 | } 212 | if matches!( 213 | Options::builder() 214 | .ctx(Context { 215 | prop: "connection".to_string(), 216 | }) 217 | .auth_block_state(AuthBlockLevel::AllowAll) 218 | // .jwt_secret("JWT_SECRET") 219 | .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 220 | .finish(), 221 | Ok(Options { .. }) 222 | ) { 223 | panic!("Returned valid options instance, should've been invalid.") 224 | } 225 | if matches!( 226 | Options::::builder() 227 | .ctx(Context { 228 | prop: "connection".to_string(), 229 | }) 230 | .auth_block_state(AuthBlockLevel::AllowAll) 231 | .jwt_secret("JWT_SECRET") 232 | // .schema(Query {}, EmptyMutation {}, EmptySubscription {}) 233 | .finish(), 234 | Ok(Options { .. }) 235 | ) { 236 | panic!("Returned valid options instance, should've been invalid.") 237 | } 238 | } 239 | --------------------------------------------------------------------------------