Skip to content

Commit

Permalink
add authentication middleware (#39)
Browse files Browse the repository at this point in the history
  • Loading branch information
tdikland authored May 4, 2024
1 parent 2dd9417 commit c041305
Show file tree
Hide file tree
Showing 4 changed files with 301 additions and 0 deletions.
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ utoipa = { version = "4", features = ["axum_extras"] }
utoipa-swagger-ui = { version = "6", features = ["axum"] }
uuid = { version = "1.3.0", features = ["v4", "serde"] }
validator = { version = "0.16.0", features = ["derive"] }
http = "1.1.0"

[dev-dependencies]
async-std = { version = "1.12.0", features = ["attributes"] }
Expand Down
157 changes: 157 additions & 0 deletions src/auth/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
//! Authentication middleware
//!
//! Depending on the use case, there are vastly different requirements on how to
//! implement authentication and authorization for the Delta Sharing server.
//!
//! Example use cases include:
//! - Public access: No authentication is required, all data is public.
//! - Share based access: Recipients can access data based on a shared secret.
//! - OAuth: Recipients can access data based on an OAuth token.
//!
//! In order to support these different use cases, the Delta Sharing server is
//! designed to be extensible. The authentication middleware is implemented as a
//! [tower::Layer](https://docs.rs/tower/0.4.4/tower/trait.Layer.html) that
//! wraps the application service. The authentication middleware is responsible
//! for authenticating the client and setting the recipient identifier in the
//! request extensions. the recipient identifier is used by the Delta Sharing
//! server to determine the access control policy for the request.

use std::fmt::Display;

pub mod public;

/// Recipient identifier.
///
/// The recipient identifier is used to identify the client that is making the
/// request. The recipient identifier is used by the Delta Sharing server to
/// determine the access control policy for the request.
#[derive(Debug, Clone, PartialEq)]
pub enum RecipientId {
/// Anonymous recipient identifier.
Unknown,
/// Known recipient identifier.
Known(String),
}

impl RecipientId {
/// Create a new [`RecipientId`] for an anonymous recipient.
///
/// # Example
/// ```
/// use delta_sharing::auth::RecipientId;
///
/// let recipient_id = RecipientId::unknown();
/// assert_eq!(recipient_id, RecipientId::Unknown);
/// ```
pub fn unknown() -> Self {
Self::Unknown
}

/// Create a new [`RecipientId`] for an anonymous recipient.
///
/// # Example
/// ```
/// use delta_sharing::auth::RecipientId;
///
/// let recipient_id = RecipientId::anonymous();
/// assert_eq!(recipient_id, RecipientId::Unknown);
/// ```
pub fn anonymous() -> Self {
Self::Unknown
}

/// Create a new [`RecipientId`] for an authenticated recipient.
///
/// # Example
/// ```
/// use delta_sharing::auth::RecipientId;
///
/// let recipient_id = RecipientId::known("foo");
/// assert_eq!(recipient_id, RecipientId::Known("foo".to_owned()));
/// ```
pub fn known<S: Into<String>>(recipient_id: S) -> Self {
Self::Known(recipient_id.into())
}

/// Check if the recipient identifier is unknown.
///
/// # Example
/// ```
/// use delta_sharing::auth::RecipientId;
///
/// let recipient_id = RecipientId::anonymous();
/// assert!(recipient_id.is_unknown());
/// ```
pub fn is_unknown(&self) -> bool {
matches!(self, RecipientId::Unknown)
}

/// Check if the recipient identifier is anonymous.
///
/// # Example
/// ```
/// use delta_sharing::auth::RecipientId;
///
/// let recipient_id = RecipientId::anonymous();
/// assert!(recipient_id.is_anonymous());
/// ```
pub fn is_anonymous(&self) -> bool {
matches!(self, RecipientId::Unknown)
}

/// Check if the recipient identifier is known.
///
/// # Example
/// ```
/// use delta_sharing::auth::RecipientId;
///
/// let recipient_id = RecipientId::known("foo");
/// assert!(recipient_id.is_known());
/// ```
pub fn is_known(&self) -> bool {
matches!(self, RecipientId::Known(_))
}
}

impl Default for RecipientId {
fn default() -> Self {
Self::Unknown
}
}

impl AsRef<str> for RecipientId {
fn as_ref(&self) -> &str {
match self {
RecipientId::Unknown => "ANONYMOUS",
RecipientId::Known(id) => id.as_str(),
}
}
}

impl Display for RecipientId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.as_ref().fmt(f)
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn default_recipient_id() {
assert_eq!(RecipientId::default(), RecipientId::Unknown);
}

#[test]
fn display_recipient_id() {
assert_eq!(
RecipientId::anonymous().to_string(),
String::from("ANONYMOUS")
);
assert_eq!(
RecipientId::known("my_recipient_id").to_string(),
String::from("my_recipient_id")
);
}
}
141 changes: 141 additions & 0 deletions src/auth/public.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
//! Authentication middleware for public access
//!
//! The public access authentication middleware does not perform any
//! authentication and sets the [`RecipientId`] in the request extension to
//! anonymous.
//!
//! # Example
//! ```rust
//! use axum::extract::Request;
//! use axum::response::Response;
//! use axum::routing::get;
//! use axum::body::Body;
//! use tower::{ServiceBuilder, ServiceExt, Service, BoxError};
//!
//! use delta_sharing::auth::RecipientId;
//! use delta_sharing::auth::public::PublicAccessAuthLayer;
//!
//! async fn handler(req: Request<Body>) -> Result<Response<Body>, BoxError> {
//! let recipient_id = req.extensions().get::<RecipientId>().unwrap();
//! assert_eq!(recipient_id, &RecipientId::Unknown);
//! Ok(Response::new(Body::empty()))
//! }
//!
//! # #[tokio::main]
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
//!
//! let mut service = ServiceBuilder::new()
//! .layer(PublicAccessAuthLayer::new())
//! .service_fn(handler);
//!
//! let request = Request::get("/")
//! .body(Body::empty())
//! .unwrap();
//!
//! let res = service.ready().await.unwrap().call(request).await.unwrap();
//!
//! # Ok(())
//! # }
//! ```
use std::task::{Context, Poll};

use axum::extract::Request;
use tower::{Layer, Service};

use crate::auth::RecipientId;

/// Layer that applies the [`PublicAccessAuth`] middleware.
#[derive(Debug, Clone)]
pub struct PublicAccessAuthLayer;

impl PublicAccessAuthLayer {
/// Create a new [`PublicAccessAuthLayer`].
pub fn new() -> Self {
Self
}
}

impl<S> Layer<S> for PublicAccessAuthLayer {
type Service = PublicAccessAuth<S>;

fn layer(&self, inner: S) -> Self::Service {
PublicAccessAuth { inner }
}
}

/// Authentication middleware.
///
/// Does not perform any authentication and sets the [`RecipientId`] in the
/// request extension to anonymous.
#[derive(Debug, Clone)]
pub struct PublicAccessAuth<S> {
inner: S,
}

impl<S> Service<Request> for PublicAccessAuth<S>
where
S: Service<Request> + Send + 'static,
S::Future: Send + 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = S::Future;

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

fn call(&mut self, mut req: Request) -> Self::Future {
let id = RecipientId::unknown();
tracing::info!(recipient_id=?id, "authenticated");

req.extensions_mut().insert(id);
self.inner.call(req)
}
}

#[cfg(test)]
mod tests {
use super::*;

use axum::body::Body;
use axum::response::Response;
use http::{header, Request, StatusCode};
use tower::BoxError;
use tower::ServiceBuilder;
use tower::ServiceExt;

#[tokio::test]
async fn public_access_auth_with_bearer() {
let mut service = ServiceBuilder::new()
.layer(PublicAccessAuthLayer::new())
.service_fn(check_recipient);

let request = Request::get("/")
.header(header::AUTHORIZATION, "Bearer foo")
.body(Body::empty())
.unwrap();

let res = service.ready().await.unwrap().call(request).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
}

#[tokio::test]
async fn public_access_auth_without_bearer() {
let mut service = ServiceBuilder::new()
.layer(PublicAccessAuthLayer::new())
.service_fn(check_recipient);

let request = Request::get("/").body(Body::empty()).unwrap();
let res = service.ready().await.unwrap().call(request).await.unwrap();
assert_eq!(res.status(), StatusCode::OK);
}

async fn check_recipient(req: Request<Body>) -> Result<Response<Body>, BoxError> {
assert_eq!(
req.extensions().get::<RecipientId>(),
Some(&RecipientId::Unknown)
);
Ok(Response::new(req.into_body()))
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ pub mod logging;
mod macros;
pub mod server;

pub mod auth;

pub const VERSION: &str = git_version::git_version!();

0 comments on commit c041305

Please sign in to comment.