Unverified Commit 3ab5a692 authored by david-perez's avatar david-perez Committed by GitHub
Browse files

Showcase a minimal operation-agnostic server model plugin (#3060)

Agnostic in that it's generic over the operation it's applied to.

Figuring out how to write such a plugin can be fun but is not trivial.
I'm mostly adding this as a reference to refresh my memory the next time
I have to implement one of these.

----

_By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice._
parent 068ad039
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -8,6 +8,7 @@ description = "A smithy Rust service to retrieve information about Pokémon."

[dependencies]
clap = { version = "4.1.11", features = ["derive"] }
http = "0.2"
hyper = { version = "0.14.26", features = ["server"] }
tokio = "1.26.0"
tower = "0.4"
+219 −0
Original line number Diff line number Diff line
/*
 * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
 * SPDX-License-Identifier: Apache-2.0
 */

//! This file showcases a rather minimal model plugin that is agnostic over the operation that it
//! is applied to.
//!
//! It is interesting because it is not trivial to figure out how to write one. As the
//! documentation for [`aws_smithy_http_server::plugin::ModelMarker`] calls out, most model
//! plugins' implementation are _operation-specific_, which are simpler.

use std::{marker::PhantomData, pin::Pin};

use aws_smithy_http_server::{
    body::BoxBody,
    operation::OperationShape,
    plugin::{ModelMarker, Plugin},
};
use pokemon_service_server_sdk::server::response::IntoResponse;
use tower::Service;

pub struct AuthorizationPlugin {
    // Private so that users are forced to use the `new` constructor.
    _private: (),
}

impl AuthorizationPlugin {
    pub fn new() -> Self {
        Self { _private: () }
    }
}

/// `T` is the inner service this plugin is applied to.
/// See the documentation for [`Plugin`] for details.
impl<Ser, Op, T> Plugin<Ser, Op, T> for AuthorizationPlugin {
    type Output = AuthorizeService<Op, T>;

    fn apply(&self, input: T) -> Self::Output {
        AuthorizeService {
            inner: input,
            authorizer: Authorizer::new(),
        }
    }
}

impl ModelMarker for AuthorizationPlugin {}

pub struct AuthorizeService<Op, S> {
    inner: S,
    authorizer: Authorizer<Op>,
}

/// We manually implement `Clone` instead of adding `#[derive(Clone)]` because we don't require
/// `Op` to be cloneable.
impl<Op, S> Clone for AuthorizeService<Op, S>
where
    S: Clone,
{
    fn clone(&self) -> Self {
        Self {
            inner: self.inner.clone(),
            authorizer: self.authorizer.clone(),
        }
    }
}

/// The error returned by [`AuthorizeService`].
pub enum AuthorizeServiceError<E> {
    /// Authorization was successful, but the inner service yielded an error.
    InnerServiceError(E),
    /// Authorization was not successful.
    AuthorizeError { message: String },
}

// Only the _outermost_ model plugin needs to apply a `Service` whose error type implements
// `IntoResponse` for the protocol the service uses (this requirement comes from the `Service`
// implementation of [`aws_smithy_http_server::operation::Upgrade`]). So if the model plugin is
// meant to be applied in any position, and to any Smithy service, one should implement
// `IntoResponse` for all protocols.
//
// Having model plugins apply a `Service` that has a `Service::Response` type or a `Service::Error`
// type that is different from those returned by the inner service hence diminishes the reusability
// of the plugin because it makes the plugin less composable. Most plugins should instead work with
// the inner service's types, and _at most_ require that those be `Op::Input` and `Op::Error`, for
// maximum composability:
//
// ```
// ...
// where
//     S: Service<(Op::Input, ($($var,)*)), Error = Op::Error>
//     ...
// {
//     type Response = S::Response;
//     type Error = S::Error;
//     type Future = Pin<Box<dyn Future<Output = Result<S::Response, S::Error>> + Send>>;
// }
//
// ```
//
// This plugin still exemplifies how changing a type can be done to make it more interesting.

impl<P, E> IntoResponse<P> for AuthorizeServiceError<E>
where
    E: IntoResponse<P>,
{
    fn into_response(self) -> http::Response<BoxBody> {
        match self {
            AuthorizeServiceError::InnerServiceError(e) => e.into_response(),
            AuthorizeServiceError::AuthorizeError { message } => http::Response::builder()
                .status(http::StatusCode::UNAUTHORIZED)
                .body(aws_smithy_http_server::body::to_boxed(message))
                .expect("attempted to build an invalid HTTP response; please file a bug report"),
        }
    }
}

macro_rules! impl_service {
    ($($var:ident),*) => {
        impl<S, Op, $($var,)*> Service<(Op::Input, ($($var,)*))> for AuthorizeService<Op, S>
        where
            S: Service<(Op::Input, ($($var,)*)), Error = Op::Error> + Clone + Send + 'static,
            S::Future: Send,
            Op: OperationShape + Send + Sync + 'static,
            Op::Input: Send + Sync + 'static,
            $($var: Send + 'static,)*
        {
            type Response = S::Response;
            type Error = AuthorizeServiceError<Op::Error>;
            type Future =
                Pin<Box<dyn std::future::Future<Output = Result<S::Response, Self::Error>> + Send>>;

            fn poll_ready(
                &mut self,
                cx: &mut std::task::Context<'_>,
            ) -> std::task::Poll<Result<(), Self::Error>> {
                self.inner
                    .poll_ready(cx)
                    .map_err(|e| Self::Error::InnerServiceError(e))
            }

            fn call(&mut self, req: (Op::Input, ($($var,)*))) -> Self::Future {
                let (input, exts) = req;

                // Replacing the service is necessary to avoid readiness problems.
                // https://docs.rs/tower/latest/tower/trait.Service.html#be-careful-when-cloning-inner-services
                let service = self.inner.clone();
                let mut service = std::mem::replace(&mut self.inner, service);

                let authorizer = self.authorizer.clone();

                let fut = async move {
                    let is_authorized = authorizer.authorize(&input).await;
                    if !is_authorized {
                        return Err(Self::Error::AuthorizeError {
                            message: "Not authorized!".to_owned(),
                        });
                    }

                    service
                        .call((input, exts))
                        .await
                        .map_err(|e| Self::Error::InnerServiceError(e))
                };
                Box::pin(fut)
            }
        }
    };
}

struct Authorizer<Op> {
    operation: PhantomData<Op>,
}

/// We manually implement `Clone` instead of adding `#[derive(Clone)]` because we don't require
/// `Op` to be cloneable.
impl<Op> Clone for Authorizer<Op> {
    fn clone(&self) -> Self {
        Self {
            operation: PhantomData,
        }
    }
}

impl<Op> Authorizer<Op> {
    fn new() -> Self {
        Self {
            operation: PhantomData,
        }
    }

    async fn authorize(&self, _input: &Op::Input) -> bool
    where
        Op: OperationShape,
    {
        // We'd perform the actual authorization here.
        // We would likely need to add bounds on `Op::Input`, `Op::Error`, if we wanted to do
        // anything useful.
        true
    }
}

// If we want our plugin to be as reusable as possible, the service it applies should work with
// inner services (i.e. operation handlers) that take a variable number of parameters. A Rust macro
// is helpful in providing those implementations concisely.
// Each handler function registered must accept the operation's input type (if there is one).
// Additionally, it can take up to 7 different parameters, each of which must implement the
// `FromParts` trait. To ensure that this `AuthorizeService` works with any of those inner
// services, we must implement it to handle up to
// 7 different types. Therefore, we invoke the `impl_service` macro 8 times.

impl_service!();
impl_service!(T1);
impl_service!(T1, T2);
impl_service!(T1, T2, T3);
impl_service!(T1, T2, T3, T4);
impl_service!(T1, T2, T3, T4, T5);
impl_service!(T1, T2, T3, T4, T5, T6);
impl_service!(T1, T2, T3, T4, T5, T6, T7);
+8 −2
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@
 * SPDX-License-Identifier: Apache-2.0
 */

mod authz;
mod plugin;

use std::{net::SocketAddr, sync::Arc};
@@ -11,7 +12,7 @@ use aws_smithy_http_server::{
    extension::OperationExtensionExt,
    instrumentation::InstrumentExt,
    layer::alb_health_check::AlbHealthCheckLayer,
    plugin::{HttpPlugins, IdentityPlugin, Scoped},
    plugin::{HttpPlugins, ModelPlugins, Scoped},
    request::request_id::ServerRequestIdProviderLayer,
    AddExtensionLayer,
};
@@ -29,6 +30,8 @@ use pokemon_service_common::{
};
use pokemon_service_server_sdk::{scope, PokemonService};

use crate::authz::AuthorizationPlugin;

#[derive(Parser, Debug)]
#[clap(author, version, about, long_about = None)]
struct Args {
@@ -64,7 +67,10 @@ pub async fn main() {
        // Adds `tracing` spans and events to the request lifecycle.
        .instrument();

    let app = PokemonService::builder_with_plugins(plugins, IdentityPlugin)
    let authz_plugin = AuthorizationPlugin::new();
    let model_plugins = ModelPlugins::new().push(authz_plugin);

    let app = PokemonService::builder_with_plugins(plugins, model_plugins)
        // Build a registry containing implementations to all the operations in the service. These
        // are async functions or async closures that take as input the operation's input and
        // return the operation's output.
+7 −6
Original line number Diff line number Diff line
@@ -264,12 +264,13 @@ impl<'a, Pl> HttpMarker for &'a Pl where Pl: HttpMarker {}
///
/// # Example implementation of a model plugin
///
/// Model plugins are most useful when you really need to rely on the actual shape of your
/// modeled operation input, operation output, and/or operation errors. For this reason, most
/// model plugins' implementation are _operation-specific_: somewhere in the type signature
/// of their definition, they'll rely on a operation shape's types. It is therefore important
/// that you scope application of model plugins to the operations they are meant to work on, via
/// [`Scoped`](crate::plugin::Scoped) or [`filter_by_operation`](crate::plugin::filter_by_operation).
/// Model plugins are most useful when you really need to rely on the actual shape of your modeled
/// operation input, operation output, and/or operation errors. For this reason, most (but not all)
/// model plugins are _operation-specific_: somewhere in the type signature of their definition,
/// they'll rely on a particular operation shape's types. It is therefore important that you scope
/// application of model plugins to the operations they are meant to work on, via
/// [`Scoped`](crate::plugin::Scoped) or
/// [`filter_by_operation`](crate::plugin::filter_by_operation).
///
/// Below is an example implementation of a model plugin that can only be applied to the
/// `CheckHealth` operation: note how in the `Service` trait implementation, we require access to