blob: 0373226fb1087400792f721ce6394a7fb496920d [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#![cfg(test)]
use crate::{AuthProviderSupplier, TokenManagerError};
use async_trait::async_trait;
use fidl::endpoints::{create_request_stream, ClientEnd};
use fidl_fuchsia_auth::Status;
use fidl_fuchsia_identity_external::{
OauthMarker, OauthOpenIdConnectMarker, OauthOpenIdConnectRequest,
OauthOpenIdConnectRequestStream, OauthRefreshTokenRequest, OauthRequest, OauthRequestStream,
OpenIdConnectMarker, OpenIdConnectRequest, OpenIdConnectRequestStream,
OpenIdTokenFromOauthRefreshTokenRequest,
};
use fidl_fuchsia_identity_tokens::OpenIdToken;
use futures::future::join;
use futures::future::BoxFuture;
use futures::prelude::*;
use futures::stream::FuturesUnordered;
use parking_lot::Mutex;
use std::collections::HashMap;
use std::sync::Arc;
use thiserror::Error;
/// FakeAuthProviderSupplier implements AuthProviderSupplier, which is needed by TokenManager
/// during instantiation. This fake has a bit of logic: (1) auth provider clients can be
/// pre-populated during test setup, (2) clients will only be given out once through the `get()`
/// method and (3) the `run()` method checks that all clients were given out during the
/// test. The reason for this design is that TokenManager caches auth provider clients and
/// reuses them throughout its lifetime, hence it will never ask for the same auth provider
/// (as defined by its `auth_provider_type`) twice.
pub struct FakeAuthProviderSupplier {
/// A mapping from auth provider type to client ends of `Oauth`
/// implementations provided in `add_oauth`.
oauth_clients: Mutex<HashMap<String, ClientEnd<OauthMarker>>>,
/// A mapping from auth provider type to client ends of `OauthOpenIdConnect`
/// implementations provided in `add_oauth_open_id_connect`.
oauth_open_id_connect_clients: Mutex<HashMap<String, ClientEnd<OauthOpenIdConnectMarker>>>,
/// A mapping from auth provider type to client ends of `OpenIdConnect`
/// implementations provided in `add_open_id_connect`.
open_id_connect_clients: Mutex<HashMap<String, ClientEnd<OpenIdConnectMarker>>>,
/// The set of Futures that serve added auth provider implementations.
servers: Mutex<FuturesUnordered<BoxFuture<'static, Result<(), fidl::Error>>>>,
}
#[derive(Debug, Error)]
pub enum FakeAuthProviderError {
#[error("FakeAuthProvider error: A server error occurred: {:?}", _0)]
ServerError(fidl::Error),
}
impl FakeAuthProviderSupplier {
pub fn new() -> Self {
Self {
oauth_clients: Mutex::new(HashMap::new()),
oauth_open_id_connect_clients: Mutex::new(HashMap::new()),
open_id_connect_clients: Mutex::new(HashMap::new()),
servers: Mutex::new(FuturesUnordered::new()),
}
}
/// Add an `Oauth` implementation, by supplying two arguments: the type, and a
/// function which acts as the server end for the auth provider. This function will be
/// invoked when `run()` is called.
pub fn add_oauth<'a, F, Fut>(&'a self, auth_provider_type: &'a str, server_fn: F)
where
F: (FnOnce(OauthRequestStream) -> Fut),
Fut: Future<Output = Result<(), fidl::Error>> + Send + 'static,
{
let (client_end, stream) = create_request_stream::<OauthMarker>().unwrap();
let serve = server_fn(stream);
self.oauth_clients.lock().insert(auth_provider_type.to_string(), client_end);
self.servers.lock().push(serve.boxed());
}
/// Add an `OpenIdConnect` implementation, by supplying two arguments: the type, and a
/// function which acts as the server end for the auth provider. This function will be
/// invoked when `run()` is called.
#[allow(dead_code)]
pub fn add_open_id_connect<'a, F, Fut>(&'a self, auth_provider_type: &'a str, server_fn: F)
where
F: (FnOnce(OpenIdConnectRequestStream) -> Fut),
Fut: Future<Output = Result<(), fidl::Error>> + Send + 'static,
{
let (client_end, stream) = create_request_stream::<OpenIdConnectMarker>().unwrap();
let serve = server_fn(stream);
self.open_id_connect_clients.lock().insert(auth_provider_type.to_string(), client_end);
self.servers.lock().push(serve.boxed());
}
/// Add an `OauthOpenIdConnect` implementation, by supplying two arguments: the type, and a
/// function which acts as the server end for the auth provider. This function will be
/// invoked when `run()` is called.
pub fn add_oauth_open_id_connect<'a, F, Fut>(
&'a self,
auth_provider_type: &'a str,
server_fn: F,
) where
F: (FnOnce(OauthOpenIdConnectRequestStream) -> Fut),
Fut: Future<Output = Result<(), fidl::Error>> + Send + 'static,
{
let (client_end, stream) = create_request_stream::<OauthOpenIdConnectMarker>().unwrap();
let serve = server_fn(stream);
self.oauth_open_id_connect_clients
.lock()
.insert(auth_provider_type.to_string(), client_end);
self.servers.lock().push(serve.boxed());
}
/// Run the added auth providers to completion. We return an error if a server function
/// returns an error. If a server function is never invoked by retrieving the
/// corresponding ClientEnd using get(), the returned future will never complete.
/// This is intended to run concurrently with the client code under test.
pub fn run(&self) -> impl Future<Output = Result<(), FakeAuthProviderError>> {
let futs = std::mem::replace(&mut *self.servers.lock(), FuturesUnordered::new());
// This method is not async and instead returns a future so that the future does
// not need to borrow `self`. This allows the future to be polled while a
// concurrent task takes ownership of `self`.
async move {
futs.collect::<Vec<_>>()
.await
.into_iter()
.collect::<Result<Vec<_>, fidl::Error>>()
.map_err(|err| FakeAuthProviderError::ServerError(err))?;
Ok(())
}
}
}
#[async_trait]
impl AuthProviderSupplier for FakeAuthProviderSupplier {
async fn get_oauth(
&self,
auth_provider_type: &str,
) -> Result<ClientEnd<OauthMarker>, TokenManagerError> {
self.oauth_clients
.lock()
.remove(auth_provider_type)
.ok_or(TokenManagerError::new(Status::AuthProviderServiceUnavailable))
}
async fn get_open_id_connect(
&self,
auth_provider_type: &str,
) -> Result<ClientEnd<OpenIdConnectMarker>, TokenManagerError> {
self.open_id_connect_clients
.lock()
.remove(auth_provider_type)
.ok_or(TokenManagerError::new(Status::AuthProviderServiceUnavailable))
}
async fn get_oauth_open_id_connect(
&self,
auth_provider_type: &str,
) -> Result<ClientEnd<OauthOpenIdConnectMarker>, TokenManagerError> {
self.oauth_open_id_connect_clients
.lock()
.remove(auth_provider_type)
.ok_or(TokenManagerError::new(Status::AuthProviderServiceUnavailable))
}
}
mod tests {
use super::*;
use fidl_fuchsia_identity_external::{
Error as ApiError, OauthAccessTokenFromOauthRefreshTokenRequest,
};
use fidl_fuchsia_identity_tokens::OauthRefreshToken;
use fuchsia_async;
/// This is a meta-test, of the FakeAuthProviderSupplier itself, since it has a bit of logic.
/// First we create a FakeAuthProviderSupplier with two auth providers, Hooli and Pied Piper,
/// for which we create two servers with expects on the incoming messages, and pre-populated
/// responses. Finally, we create a client that retrieves the auth providers from the fake,
/// sends the expected messages to them and checks that the responses match.
#[fuchsia_async::run_until_stalled(test)]
async fn auth_provider_fake_test() {
let auth_provider_supplier = Arc::new(FakeAuthProviderSupplier::new());
// Non existing auth provider
assert!(auth_provider_supplier.get_oauth("myspace").await.is_err());
auth_provider_supplier.add_oauth("hooli", |mut stream| async move {
match stream.try_next().await? {
Some(OauthRequest::RevokeRefreshToken { responder, refresh_token }) => {
assert_eq!(refresh_token.content.unwrap(), "HOOLI_CREDENTIAL");
responder.send(&mut Err(ApiError::InvalidRequest))?;
}
_ => panic!("Unexpected message received"),
}
assert!(stream.try_next().await?.is_none());
Ok(())
});
auth_provider_supplier.add_oauth("pied-piper", |mut stream| async move {
match stream.try_next().await? {
Some(OauthRequest::GetAccessTokenFromRefreshToken { responder, request }) => {
assert_eq!(
request.refresh_token.unwrap().content.unwrap(),
"PIED_PIPER_CREDENTIAL"
);
assert!(request.client_id.is_none());
assert!(request.scopes.unwrap().is_empty());
responder.send(&mut Err(ApiError::Network))?;
}
_ => panic!("Unexpected message received"),
}
assert!(stream.try_next().await?.is_none());
Ok(())
});
let auth_provider_supplier_clone = Arc::clone(&auth_provider_supplier);
let client_fn = async move {
let oauth_proxy = auth_provider_supplier_clone
.get_oauth("hooli")
.await
.unwrap()
.into_proxy()
.unwrap();
assert_eq!(
oauth_proxy
.revoke_refresh_token(OauthRefreshToken {
content: Some("HOOLI_CREDENTIAL".to_string()),
account_id: None,
..OauthRefreshToken::EMPTY
})
.await
.unwrap(),
Err(ApiError::InvalidRequest)
);
let oauth_proxy = auth_provider_supplier_clone
.get_oauth("pied-piper")
.await
.unwrap()
.into_proxy()
.unwrap();
assert_eq!(
oauth_proxy
.get_access_token_from_refresh_token(
OauthAccessTokenFromOauthRefreshTokenRequest {
refresh_token: Some(OauthRefreshToken {
content: Some("PIED_PIPER_CREDENTIAL".to_string()),
account_id: None,
..OauthRefreshToken::EMPTY
}),
client_id: None,
scopes: Some(vec![]),
..OauthAccessTokenFromOauthRefreshTokenRequest::EMPTY
}
)
.await
.unwrap(),
Err(ApiError::Network)
);
};
let (run_result, _) = join(auth_provider_supplier.run(), client_fn).await;
assert!(run_result.is_ok());
assert!(auth_provider_supplier.get_oauth("hooli").await.is_err());
assert!(auth_provider_supplier.get_oauth("pied-piper").await.is_err());
}
/// A test for FakeAuthProviderSupplier that walks through setting and retrieving one of
/// each possible protocol.
#[fuchsia_async::run_until_stalled(test)]
async fn multiple_protocols_test() {
let auth_provider_supplier = FakeAuthProviderSupplier::new();
auth_provider_supplier.add_oauth("hooli", |mut stream| async move {
match stream.try_next().await? {
Some(OauthRequest::CreateRefreshToken { responder, .. }) => {
responder
.send(&mut Err(fidl_fuchsia_identity_external::Error::InvalidRequest))?;
}
_ => panic!("Unexpected message received"),
}
assert!(stream.try_next().await?.is_none());
Ok(())
});
auth_provider_supplier.add_oauth_open_id_connect("hooli", |mut stream| async move {
match stream.try_next().await? {
Some(OauthOpenIdConnectRequest::GetIdTokenFromRefreshToken {
responder, ..
}) => {
responder
.send(&mut Err(fidl_fuchsia_identity_external::Error::InvalidRequest))?;
}
_ => panic!("Unexpected message received"),
}
assert!(stream.try_next().await?.is_none());
Ok(())
});
auth_provider_supplier.add_open_id_connect("hooli", |mut stream| async move {
match stream.try_next().await? {
Some(OpenIdConnectRequest::RevokeIdToken { responder, .. }) => {
responder
.send(&mut Err(fidl_fuchsia_identity_external::Error::InvalidRequest))?;
}
_ => panic!("Unexpected message received"),
}
assert!(stream.try_next().await?.is_none());
Ok(())
});
let client_fn = async {
let oauth_proxy =
auth_provider_supplier.get_oauth("hooli").await.unwrap().into_proxy().unwrap();
assert_eq!(
oauth_proxy
.create_refresh_token(OauthRefreshTokenRequest {
account_id: None,
ui_context: None,
..OauthRefreshTokenRequest::EMPTY
})
.await
.unwrap(),
Err(fidl_fuchsia_identity_external::Error::InvalidRequest)
);
let oauth_open_id_connect_proxy = auth_provider_supplier
.get_oauth_open_id_connect("hooli")
.await
.unwrap()
.into_proxy()
.unwrap();
assert_eq!(
oauth_open_id_connect_proxy
.get_id_token_from_refresh_token(OpenIdTokenFromOauthRefreshTokenRequest {
refresh_token: None,
audiences: None,
..OpenIdTokenFromOauthRefreshTokenRequest::EMPTY
})
.await
.unwrap(),
Err(fidl_fuchsia_identity_external::Error::InvalidRequest)
);
let open_id_connect_proxy = auth_provider_supplier
.get_open_id_connect("hooli")
.await
.unwrap()
.into_proxy()
.unwrap();
assert_eq!(
open_id_connect_proxy
.revoke_id_token(OpenIdToken {
content: None,
expiry_time: None,
..OpenIdToken::EMPTY
})
.await
.unwrap(),
Err(fidl_fuchsia_identity_external::Error::InvalidRequest)
);
};
let (run_result, _) = join(auth_provider_supplier.run(), client_fn).await;
assert!(run_result.is_ok());
}
}