1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208
// Utility functions for GraphQL resolvers use std::sync::{Mutex, MutexGuard}; use tokio_stream::Stream; use crate::auth::auth_state::AuthState; use crate::errors::*; use crate::pubsub::PubSub; /// Checks to see if the given authentication state matches the series of given claims. This must be provided with the authentication state, /// 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) /// error if the user is unauthenticated, so **that must be handled in your function's return type**! /// # Example /// This is a simplified version of the internal logic that publishes data to the subscriptions server. /// ``` /// use diana::{ /// errors::{Result, GQLResult}, /// graphql_utils::get_auth_data_from_ctx, /// async_graphql::{Object as GQLObject}, /// if_authed, /// }; /// /// #[derive(Default, Clone)] /// pub struct PublishMutation; /// #[GQLObject] /// impl PublishMutation { /// async fn publish( /// &self, /// raw_ctx: &async_graphql::Context<'_>, /// channel: String, /// data: String, /// ) -> Result<bool> { /// let auth_state = get_auth_data_from_ctx(raw_ctx)?; /// if_authed!( /// auth_state, /// { /// "role" => "graphql_server" /// }, /// { /// // Your code here /// Ok(true) /// } /// ) /// } /// } /// /// # fn main() {} /// ``` // TODO mark as deprecated #[macro_export] #[deprecated( since = "0.2.8", note = "please use `is_authed!` instead, it exposes a boolean and lets you use your own error logic" )] macro_rules! if_authed( ($auth_state:expr, { $($key:expr => $value:expr),+ }, $code:block) => { { // Create a HashMap out of the given test claims let mut test_claims: ::std::collections::HashMap<&str, &str> = ::std::collections::HashMap::new(); $( test_claims.insert($key, $value); )+ // Match the authentication state with those claims now if $auth_state.has_claims(test_claims) { $code } else { Err($crate::errors::ErrorKind::Unauthorised.into()) } } }; ); /// Checks to see if the given authentication state matches the series of given claims. This must be provided with the authentication state, /// a series of claims to check against. It will then return a boolean as to whether or not the user is authorized. /// This should be used instead of [`if_authed!`]. /// # Example /// This is a simplified version of the internal logic that publishes data to the subscriptions server. /// ``` /// use diana::{ /// errors::{Result, GQLResult, bail, ErrorKind}, /// graphql_utils::get_auth_data_from_ctx, /// async_graphql::{Object as GQLObject}, /// is_authed, /// }; /// /// #[derive(Default, Clone)] /// pub struct PublishMutation; /// #[GQLObject] /// impl PublishMutation { /// async fn publish( /// &self, /// raw_ctx: &async_graphql::Context<'_>, /// channel: String, /// data: String, /// ) -> Result<bool> { /// if is_authed!( /// get_auth_data_from_ctx(raw_ctx)?, /// { /// "role" => "graphql_server" /// } /// ) { /// // Your code here /// Ok(true) /// } else { /// // Your error handling code here /// bail!(ErrorKind::Unauthorised) /// } /// } /// } /// /// # fn main() {} /// ``` #[macro_export] macro_rules! is_authed( ($auth_state:expr, { $($key:expr => $value:expr),+ }) => { { // Create a HashMap out of the given test claims let mut test_claims: ::std::collections::HashMap<&str, &str> = ::std::collections::HashMap::new(); $( test_claims.insert($key, $value); )+ // Match the authentication state with those claims now $auth_state.has_claims(test_claims) } }; ); /// Gets a subscription stream to events published on a particular channel from the context of a GraphQL resolver. /// **This must only be used in subscriptions! It will not work anywhere else!** /// This returns a pre-created stream which you should manipulate if necessary. /// All data sent via the publisher from the queries/mutations system will land here **in string format**. Serialization is up to you. /// # Example /// ``` /// use diana::{ /// stream, /// graphql_utils::get_stream_for_channel_from_ctx, /// errors::GQLResult, /// async_graphql::{Subscription as GQLSubscription, SimpleObject as GQLSimpleObject}, /// }; /// use tokio_stream::{Stream, StreamExt}; /// use serde::Deserialize; /// /// #[derive(Deserialize, GQLSimpleObject)] /// struct User { /// username: String /// } /// /// #[derive(Default, Clone)] /// pub struct Subscription; /// #[GQLSubscription] /// impl Subscription { /// async fn new_users( /// &self, /// raw_ctx: &async_graphql::Context<'_>, /// ) -> impl Stream<Item = GQLResult<User>> { /// // Get a direct stream from the context on a certain channel /// let stream_result = get_stream_for_channel_from_ctx("new_user", raw_ctx); /// /// // We can manipulate the stream using the stream macro from async-stream /// stream! { /// let stream = stream_result?; /// for await message in stream { /// // Serialise the data as a user /// let new_user: User = serde_json::from_str(&message).map_err(|_err| "couldn't serialize given data correctly".to_string())?; /// yield Ok(new_user); /// } /// } /// } /// } /// # fn main() {} /// ``` /// pub fn get_stream_for_channel_from_ctx( channel: &str, raw_ctx: &async_graphql::Context<'_>, ) -> Result<impl Stream<Item = String>> { // Get the PubSub mutably let mut pubsub = get_pubsub_from_ctx(raw_ctx)?; // Return a stream on the given channel Ok(pubsub.subscribe(channel)) } /// Gets authentication data from the context of a GraphQL resolver. /// This should only fail if the server is constructed without authentication middleware (which shouldn't be possible with the exposed API /// surface of this crate). pub fn get_auth_data_from_ctx<'a>( raw_ctx: &'a async_graphql::Context<'_>, ) -> Result<&'a AuthState> { let auth_state = raw_ctx .data::<AuthState>() .map_err(|_err| ErrorKind::GraphQLContextNotFound("auth_state".to_string()))?; Ok(auth_state) } /// Gets the internal PubSub from the context of a GraphQL resolver. You should never need to use this. #[doc(hidden)] pub fn get_pubsub_from_ctx<'a>( raw_ctx: &'a async_graphql::Context<'_>, ) -> Result<MutexGuard<'a, PubSub>> { // We store the PubSub instance as a Mutex because we need it sent/synced between threads as a mutable let pubsub_mutex = raw_ctx .data::<Mutex<PubSub>>() .map_err(|_err| ErrorKind::GraphQLContextNotFound("pubsub".to_string()))?; let pubsub = pubsub_mutex .lock() .map_err(|_err| ErrorKind::MutexPoisoned("pubsub".to_string()))?; Ok(pubsub) }