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
// 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 you function's return type**!
/// # Example
/// This is a simplified version of the internal logic that published data to the subscriptions server.
/// ```
/// use diana::{
///     errors::Result,
///     graphql_utils::get_auth_data_from_ctx
/// };
/// let auth_state = get_auth_data_from_ctx(raw_ctx)?;
/// 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)
///         }
///     )
/// }
/// ```
#[macro_export]
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
            match $auth_state {
                Some(auth_state) if auth_state.has_claims(test_claims) => {
                    $code
                },
                _ => crate::errors::bail!(crate::errors::ErrorKind::Unauthorised)
            }
        }
     };
);

/// 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
/// };
/// async fn new_users(
///     &self,
///     raw_ctx: &async_graphql::Context<'_>,
/// ) -> impl Stream<Item = Result<User, String>> {
///     // 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! {
///         // Unfortunately we can't get the stream in here, but we have to handle the error in here (try it if you want)
///         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);
///         }
///     }
/// }
/// ```
///
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 Option<AuthState>> {
    let auth_state = raw_ctx
        .data::<Option<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.
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)
}