Compare commits
8 Commits
2394a74b22
...
36fd8a3f88
Author | SHA1 | Date |
---|---|---|
Aravinth Manivannan | 36fd8a3f88 | |
Aravinth Manivannan | 8fb056a669 | |
Aravinth Manivannan | 32b4ca24f3 | |
Aravinth Manivannan | d72361e36f | |
Aravinth Manivannan | c277c7af9a | |
Aravinth Manivannan | e835adf6c8 | |
Aravinth Manivannan | 74aacecae4 | |
Aravinth Manivannan | 428b6d413a |
|
@ -60,42 +60,28 @@ mod tests {
|
|||
|
||||
use super::*;
|
||||
use crate::auth::application::port::out::{
|
||||
db::save_oauth_state::MockSaveOAuthState, forge::oauth_auth_req_uri::MockOAuthAuthReqUri,
|
||||
db::save_oauth_state::tests::*, forge::oauth_auth_req_uri::tests::*,
|
||||
};
|
||||
use crate::utils::random_string::*;
|
||||
use crate::tests::bdd::IS_CALLED_ONLY_ONCE;
|
||||
use crate::utils::random_string::tests::*;
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_adapter() {
|
||||
let random_string = "foorand";
|
||||
|
||||
let url = Url::parse("http://test_ui_req_auth_interface_adapter").unwrap();
|
||||
let oauth_provider = "test_ui_req_auth_interface_adapter";
|
||||
let mut redirect_uri = url.clone();
|
||||
redirect_uri.set_query(Some(&format!("state={random_string}")));
|
||||
|
||||
let mut mock_random_generate_string = MockGenerateRandomStringInterface::new();
|
||||
mock_random_generate_string
|
||||
.expect_get_random()
|
||||
.times(1)
|
||||
.return_const(random_string.to_string());
|
||||
|
||||
let r = redirect_uri.clone();
|
||||
let mut mock_oauth_req_uri = MockOAuthAuthReqUri::new();
|
||||
mock_oauth_req_uri
|
||||
.expect_oauth_auth_req_uri()
|
||||
.times(1)
|
||||
.returning(move |_, _| Ok(r.clone()));
|
||||
|
||||
let mut mock_save_oauth_state = MockSaveOAuthState::new();
|
||||
mock_save_oauth_state
|
||||
.expect_save_oauth_state()
|
||||
.times(1)
|
||||
.returning(|_, _, _| Ok(()));
|
||||
let mock_random_generate_string =
|
||||
mock_generate_random_string(IS_CALLED_ONLY_ONCE, random_string.into());
|
||||
let mock_oauth_req_uri = mock_oauth_auth_req_uri(IS_CALLED_ONLY_ONCE, redirect_uri.clone());
|
||||
let mock_save_oauth_state = mock_save_oauth_state(IS_CALLED_ONLY_ONCE);
|
||||
|
||||
let adapter = RequestAuthorizationHandler::new(
|
||||
Arc::new(mock_save_oauth_state),
|
||||
Arc::new(mock_oauth_req_uri),
|
||||
Arc::new(mock_random_generate_string),
|
||||
mock_save_oauth_state,
|
||||
mock_oauth_req_uri,
|
||||
mock_random_generate_string,
|
||||
url.clone(),
|
||||
);
|
||||
|
||||
|
|
|
@ -30,7 +30,7 @@ async fn handler(
|
|||
let oauth_auth_req_uri_adapter = &forges
|
||||
.get_forge_factory(&SupportedForges::Forgejo)
|
||||
.unwrap()
|
||||
.get_oauth_auth_req_uri_adapter();
|
||||
.oauth_auth_req_uri_adapter();
|
||||
|
||||
let process_authorization_response_redirect_uri = Url::parse(&format!(
|
||||
"{}://{}{}",
|
||||
|
@ -66,11 +66,11 @@ mod tests {
|
|||
};
|
||||
use crate::auth::adapter::out::forge::forge_repository::MockForgeRepositoryInterface;
|
||||
|
||||
use crate::auth::application::port::out::forge::oauth_auth_req_uri::OAuthAuthReqUri;
|
||||
use crate::auth::application::port::out::{
|
||||
db::save_oauth_state::MockSaveOAuthState, forge::oauth_auth_req_uri::MockOAuthAuthReqUri,
|
||||
db::save_oauth_state::tests::*, forge::oauth_auth_req_uri::tests::*,
|
||||
};
|
||||
use crate::utils::random_string::*;
|
||||
use crate::tests::bdd::IS_CALLED_ONLY_ONCE;
|
||||
use crate::utils::random_string::{tests::*, WebGenerateRandomStringInterface};
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_ui_handler_request_oauth_authorization_forgejo() {
|
||||
|
@ -81,39 +81,21 @@ mod tests {
|
|||
let mut redirect_uri = url.clone();
|
||||
redirect_uri.set_query(Some(&format!("state={random_string}")));
|
||||
|
||||
let mock_random_generate_string = {
|
||||
let mut mock_random_generate_string = MockGenerateRandomStringInterface::new();
|
||||
mock_random_generate_string
|
||||
.expect_get_random()
|
||||
.times(1)
|
||||
.return_const(random_string.to_string());
|
||||
WebGenerateRandomStringInterface::new(Arc::new(mock_random_generate_string))
|
||||
};
|
||||
let mock_random_generate_string = WebGenerateRandomStringInterface::new(
|
||||
mock_generate_random_string(IS_CALLED_ONLY_ONCE, random_string.into()),
|
||||
);
|
||||
|
||||
let mock_save_oauth_state = {
|
||||
let mut mock_save_oauth_state = MockSaveOAuthState::new();
|
||||
mock_save_oauth_state
|
||||
.expect_save_oauth_state()
|
||||
.times(1)
|
||||
.returning(|_, _, _| Ok(()));
|
||||
types::WebSaveOauthState::new(Arc::new(mock_save_oauth_state))
|
||||
};
|
||||
let mock_save_oauth_state =
|
||||
types::WebSaveOauthState::new(mock_save_oauth_state(IS_CALLED_ONLY_ONCE));
|
||||
|
||||
let mock_forges = {
|
||||
let mock_oauth_req_uri: Arc<dyn OAuthAuthReqUri> = {
|
||||
let r = redirect_uri.clone();
|
||||
let mut mock_oauth_req_uri = MockOAuthAuthReqUri::new();
|
||||
mock_oauth_req_uri
|
||||
.expect_oauth_auth_req_uri()
|
||||
.times(1)
|
||||
.returning(move |_, _| Ok(r.clone()));
|
||||
Arc::new(mock_oauth_req_uri)
|
||||
};
|
||||
let mock_oauth_req_uri =
|
||||
mock_oauth_auth_req_uri(IS_CALLED_ONLY_ONCE, redirect_uri.clone());
|
||||
|
||||
let mut mock_forge_factory = MockForgeAdapterFactoryInterface::default();
|
||||
let a = mock_oauth_req_uri.clone();
|
||||
mock_forge_factory
|
||||
.expect_get_oauth_auth_req_uri_adapter()
|
||||
.expect_oauth_auth_req_uri_adapter()
|
||||
.times(1)
|
||||
.returning(move || a.clone());
|
||||
let mock_forge_factory: Arc<dyn ForgeAdapterFactoryInterface> =
|
||||
|
|
|
@ -3,12 +3,27 @@ use std::sync::Arc;
|
|||
use actix_web::web;
|
||||
|
||||
use crate::auth::adapter::out::forge::forge_repository::ForgeRepositoryInterface;
|
||||
use crate::auth::application::port::out::db::save_oauth_state::SaveOAuthState;
|
||||
use crate::auth::application::port::out::db::{
|
||||
delete_oauth_state::DeleteOAuthState, oauth_state_exists::OAuthStateExists,
|
||||
save_oauth_access_token::SaveOAuthAccessToken, save_oauth_state::SaveOAuthState,
|
||||
};
|
||||
use crate::auth::application::port::out::forge::{
|
||||
get_username::GetUsername, request_access_token::RequestAccessToken,
|
||||
};
|
||||
pub(super) use crate::utils::random_string::WebGenerateRandomStringInterface;
|
||||
|
||||
use super::RoutesRepository;
|
||||
|
||||
pub type WebForgeRepositoryInterface = web::Data<Arc<dyn ForgeRepositoryInterface>>;
|
||||
pub type WebSaveOauthState = web::Data<Arc<dyn SaveOAuthState>>;
|
||||
|
||||
pub type WebRouteRepository = web::Data<Arc<RoutesRepository>>;
|
||||
|
||||
pub type WebSettings = web::Data<crate::settings::Settings>;
|
||||
|
||||
pub type WebSaveOauthState = web::Data<Arc<dyn SaveOAuthState>>;
|
||||
pub type WebOauthStateExists = web::Data<Arc<dyn OAuthStateExists>>;
|
||||
pub type WebDeleteOauthState = web::Data<Arc<dyn DeleteOAuthState>>;
|
||||
pub type WebSaveOAuthAccessToken = web::Data<Arc<dyn SaveOAuthAccessToken>>;
|
||||
|
||||
pub type WebGetUsername = web::Data<Arc<dyn GetUsername>>;
|
||||
pub type WebRequestAccessToken = web::Data<Arc<dyn RequestAccessToken>>;
|
||||
|
|
|
@ -10,7 +10,7 @@ use crate::settings;
|
|||
use out::db::postgres::DBOutPostgresAdapter;
|
||||
use out::forge::{forge_repository::ForgeRepository, forgejo::Forgejo};
|
||||
|
||||
use input::web::types;
|
||||
use input::web::types::*;
|
||||
|
||||
pub fn load_adapters(
|
||||
pool: PgPool,
|
||||
|
@ -23,17 +23,29 @@ pub fn load_adapters(
|
|||
);
|
||||
|
||||
let forge_repository_interface =
|
||||
types::WebForgeRepositoryInterface::new(Arc::new(ForgeRepository::new(forgejo)));
|
||||
let db = DBOutPostgresAdapter::new(pool);
|
||||
let save_oauth_state_adapter: types::WebSaveOauthState =
|
||||
types::WebSaveOauthState::new(Arc::new(db.clone()));
|
||||
WebForgeRepositoryInterface::new(Arc::new(ForgeRepository::new(forgejo)));
|
||||
|
||||
let s = types::WebSettings::new(settings.clone());
|
||||
let db = DBOutPostgresAdapter::new(pool);
|
||||
let save_oauth_state_adapter: WebSaveOauthState = WebSaveOauthState::new(Arc::new(db.clone()));
|
||||
let delete_oauth_state_adapter: WebDeleteOauthState =
|
||||
WebDeleteOauthState::new(Arc::new(db.clone()));
|
||||
let save_oauth_access_token: WebSaveOAuthAccessToken =
|
||||
WebSaveOAuthAccessToken::new(Arc::new(db.clone()));
|
||||
let oauth_state_exists_adapter: WebOauthStateExists =
|
||||
WebOauthStateExists::new(Arc::new(db.clone()));
|
||||
|
||||
let s = WebSettings::new(settings.clone());
|
||||
|
||||
let f = move |cfg: &mut web::ServiceConfig| {
|
||||
cfg.app_data(save_oauth_state_adapter);
|
||||
cfg.app_data(delete_oauth_state_adapter);
|
||||
cfg.app_data(save_oauth_access_token);
|
||||
cfg.app_data(oauth_state_exists_adapter);
|
||||
|
||||
cfg.app_data(forge_repository_interface);
|
||||
|
||||
cfg.app_data(s);
|
||||
|
||||
cfg.configure(input::web::load_ctx());
|
||||
};
|
||||
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
use url::Url;
|
||||
|
||||
use super::DBOutPostgresAdapter;
|
||||
use crate::auth::application::port::out::db::{
|
||||
delete_oauth_state::DeleteOAuthState, errors::OutDBPortResult,
|
||||
};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl DeleteOAuthState for DBOutPostgresAdapter {
|
||||
async fn delete_oauth_state(&self, state: &str, oauth_provider: &str) -> OutDBPortResult<()> {
|
||||
sqlx::query!(
|
||||
"DELETE FROM
|
||||
oauth_state
|
||||
WHERE
|
||||
state = $1
|
||||
AND
|
||||
oauth_provider = $2;",
|
||||
state,
|
||||
oauth_provider,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::auth::application::port::out::db::oauth_state_exists::OAuthStateExists;
|
||||
use crate::auth::application::port::out::db::save_oauth_state::SaveOAuthState;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_postgres_delete_oauth_state() {
|
||||
let state = "delete_oauth_state";
|
||||
let oauth_provider = "oauthprovitestpostgres";
|
||||
let redirect_uri = Url::parse("https://oauthprovitestpostgres").unwrap();
|
||||
|
||||
let settings = crate::settings::tests::get_settings().await;
|
||||
|
||||
let db = super::DBOutPostgresAdapter::new(
|
||||
sqlx::postgres::PgPool::connect(&settings.database.url)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// create state
|
||||
db.save_oauth_state(state, oauth_provider, &redirect_uri)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// delete state
|
||||
db.delete_oauth_state(state, oauth_provider).await.unwrap();
|
||||
|
||||
// check state doesn't exist
|
||||
assert!(!db
|
||||
.oauth_state_exists(state, oauth_provider, &redirect_uri)
|
||||
.await
|
||||
.unwrap());
|
||||
|
||||
// try to delete non-existent state; should succeed.
|
||||
db.delete_oauth_state(state, oauth_provider).await.unwrap();
|
||||
|
||||
settings.drop_db().await;
|
||||
}
|
||||
}
|
|
@ -1,28 +1,24 @@
|
|||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::borrow::Cow;
|
||||
|
||||
use sqlx::Error as SqlxError;
|
||||
|
||||
use crate::auth::application::port::out::db::errors::OutDBPortError;
|
||||
|
||||
impl From<SqlxError> for OutDBPortError {
|
||||
fn from(value: SqlxError) -> Self {
|
||||
unimplemented!()
|
||||
fn from(e: SqlxError) -> Self {
|
||||
log::error!("[postgres] err: {}", e);
|
||||
if let SqlxError::Database(err) = e {
|
||||
if err.code() == Some(Cow::from("23505")) {
|
||||
let msg = err.message();
|
||||
if msg.contains("oauth_state_state_key") {
|
||||
return Self::DuplicateState;
|
||||
} else if msg.contains("oauth_access_token_username_oauth_source_key") {
|
||||
return Self::DuplicateAccessToken;
|
||||
} else {
|
||||
println!("{msg}");
|
||||
}
|
||||
}
|
||||
}
|
||||
Self::InternalError
|
||||
}
|
||||
}
|
||||
|
||||
//impl From<ProcessAuthorizationServiceError> for OutDBPostgresError {
|
||||
// fn from(v: ProcessAuthorizationServiceError) -> Self {
|
||||
// match v {
|
||||
// ProcessAuthorizationServiceError::InteralError => Self::InternalServerError,
|
||||
// ProcessAuthorizationServiceError::BadRequest => Self::BadRequest,
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//impl From<RequestAuthorizationServiceError> for OutDBPostgresError {
|
||||
// fn from(v: RequestAuthorizationServiceError) -> Self {
|
||||
// match v {
|
||||
// RequestAuthorizationServiceError::InteralError => Self::InternalServerError,
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
|
|
|
@ -5,7 +5,10 @@ use sqlx::postgres::PgPool;
|
|||
|
||||
use crate::db::{migrate::RunMigrations, sqlx_postgres::Postgres};
|
||||
|
||||
mod delete_oauth_state;
|
||||
mod errors;
|
||||
mod oauth_state_exists;
|
||||
mod save_oauth_access_token;
|
||||
mod save_oauth_state;
|
||||
|
||||
#[derive(Clone)]
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
use url::Url;
|
||||
|
||||
use super::DBOutPostgresAdapter;
|
||||
use crate::auth::application::port::out::db::{
|
||||
errors::OutDBPortResult, oauth_state_exists::OAuthStateExists,
|
||||
};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl OAuthStateExists for DBOutPostgresAdapter {
|
||||
async fn oauth_state_exists(
|
||||
&self,
|
||||
state: &str,
|
||||
oauth_provider: &str,
|
||||
redirect_uri: &Url,
|
||||
) -> OutDBPortResult<bool> {
|
||||
let res = sqlx::query!(
|
||||
"SELECT EXISTS (
|
||||
SELECT 1
|
||||
FROM oauth_state
|
||||
WHERE
|
||||
state = $1
|
||||
AND
|
||||
oauth_provider = $2
|
||||
AND
|
||||
redirect_uri = $3
|
||||
);",
|
||||
state,
|
||||
oauth_provider,
|
||||
redirect_uri.as_str()
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
if let Some(x) = res.exists {
|
||||
Ok(x)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::auth::application::port::out::db::save_oauth_state::SaveOAuthState;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_postgres_oauth_state_exists() {
|
||||
let state = "oauthstateexists";
|
||||
let oauth_provider = "oauthprovitestpostgres";
|
||||
let redirect_uri = Url::parse("https://oauthprovitestpostgres").unwrap();
|
||||
|
||||
let settings = crate::settings::tests::get_settings().await;
|
||||
|
||||
let db = super::DBOutPostgresAdapter::new(
|
||||
sqlx::postgres::PgPool::connect(&settings.database.url)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
// state doesn't exist
|
||||
assert!(!db
|
||||
.oauth_state_exists(state, oauth_provider, &redirect_uri)
|
||||
.await
|
||||
.unwrap());
|
||||
|
||||
db.save_oauth_state(state, oauth_provider, &redirect_uri)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// state exists
|
||||
assert!(db
|
||||
.oauth_state_exists(state, oauth_provider, &redirect_uri)
|
||||
.await
|
||||
.unwrap());
|
||||
|
||||
settings.drop_db().await;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
use super::DBOutPostgresAdapter;
|
||||
use crate::auth::application::port::out::db::{
|
||||
errors::OutDBPortResult, save_oauth_access_token::SaveOAuthAccessToken,
|
||||
};
|
||||
use crate::auth::domain::OAuthAccessToken;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SaveOAuthAccessToken for DBOutPostgresAdapter {
|
||||
async fn save_oauth_access_token(
|
||||
&self,
|
||||
username: &str,
|
||||
oauth_provider: &str,
|
||||
access_token: &OAuthAccessToken,
|
||||
) -> OutDBPortResult<()> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO oauth_access_token (
|
||||
access_token, token_type, expires_in, refresh_token, oauth_source, username
|
||||
) VALUES ($1, $2, $3, $4, $5, $6)",
|
||||
&access_token.access_token,
|
||||
&access_token.token_type,
|
||||
access_token.expires_in as i64,
|
||||
&access_token.refresh_token,
|
||||
oauth_provider,
|
||||
username
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::auth::application::port::out::db::errors::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_postgres_save_oauth_access_token() {
|
||||
let oauth_provider = "oauthprovitestpostgres";
|
||||
let username = "test_postgres_save_oauth_access_token";
|
||||
let access_token = OAuthAccessToken {
|
||||
refresh_token: "asdfasdfarefresh".into(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let settings = crate::settings::tests::get_settings().await;
|
||||
|
||||
let db = super::DBOutPostgresAdapter::new(
|
||||
sqlx::postgres::PgPool::connect(&settings.database.url)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
db.save_oauth_access_token(username, oauth_provider, &access_token)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
db.save_oauth_access_token(username, oauth_provider, &access_token)
|
||||
.await
|
||||
.err(),
|
||||
Some(OutDBPortError::DuplicateAccessToken)
|
||||
);
|
||||
|
||||
settings.drop_db().await;
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ impl SaveOAuthState for DBOutPostgresAdapter {
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::auth::application::port::out::db::errors::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_postgres_save_oauth_state() {
|
||||
|
@ -47,6 +48,13 @@ mod tests {
|
|||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
db.save_oauth_state(state, oauth_provider, &redirect_uri)
|
||||
.await
|
||||
.err(),
|
||||
Some(OutDBPortError::DuplicateState)
|
||||
);
|
||||
|
||||
settings.drop_db().await;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,30 @@
|
|||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use super::errors::*;
|
||||
|
||||
#[automock]
|
||||
#[async_trait::async_trait]
|
||||
pub trait DeleteOAuthState: Send + Sync {
|
||||
/// Delete OAuth
|
||||
async fn delete_oauth_state(&self, state: &str, oauth_provider: &str) -> OutDBPortResult<()>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_delete_oauth_state(times: Option<usize>) -> Arc<dyn DeleteOAuthState> {
|
||||
let mut m = MockDeleteOAuthState::default();
|
||||
if let Some(times) = times {
|
||||
m.expect_delete_oauth_state()
|
||||
.times(times)
|
||||
.returning(move |_, _| Ok(()));
|
||||
} else {
|
||||
m.expect_delete_oauth_state().returning(move |_, _| Ok(()));
|
||||
}
|
||||
Arc::new(m)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,4 +6,6 @@ pub type OutDBPortResult<V> = Result<V, OutDBPortError>;
|
|||
#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum OutDBPortError {
|
||||
DuplicateState,
|
||||
InternalError,
|
||||
DuplicateAccessToken,
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod delete_oauth_state;
|
||||
pub mod errors;
|
||||
pub mod oauth_state_exists;
|
||||
pub mod save_oauth_access_token;
|
||||
pub mod save_oauth_state;
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
use url::Url;
|
||||
|
||||
use super::errors::*;
|
||||
|
||||
#[automock]
|
||||
#[async_trait::async_trait]
|
||||
pub trait OAuthStateExists: Send + Sync {
|
||||
/// Save OAuth state code generated during authorization request, which will later be used to
|
||||
|
@ -13,3 +16,27 @@ pub trait OAuthStateExists: Send + Sync {
|
|||
redirect_uri: &Url,
|
||||
) -> OutDBPortResult<bool>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_oauth_state_exists(
|
||||
times: Option<usize>,
|
||||
return_val: bool,
|
||||
) -> Arc<dyn OAuthStateExists> {
|
||||
let mut m = MockOAuthStateExists::default();
|
||||
if let Some(times) = times {
|
||||
m.expect_oauth_state_exists()
|
||||
.times(times)
|
||||
.returning(move |_, _, _| Ok(return_val));
|
||||
} else {
|
||||
m.expect_oauth_state_exists()
|
||||
.returning(move |_, _, _| Ok(return_val));
|
||||
}
|
||||
|
||||
Arc::new(m)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use super::errors::*;
|
||||
|
||||
use crate::auth::domain::OAuthAccessToken;
|
||||
|
||||
#[automock]
|
||||
#[async_trait::async_trait]
|
||||
pub trait SaveOAuthAccessToken: Send + Sync {
|
||||
async fn save_oauth_access_token(
|
||||
&self,
|
||||
username: &str,
|
||||
oauth_provider: &str,
|
||||
access_token: &OAuthAccessToken,
|
||||
) -> OutDBPortResult<()>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_save_oauth_access_token(times: Option<usize>) -> Arc<dyn SaveOAuthAccessToken> {
|
||||
let mut m = MockSaveOAuthAccessToken::default();
|
||||
if let Some(times) = times {
|
||||
m.expect_save_oauth_access_token()
|
||||
.times(times)
|
||||
.returning(move |_, _, _| Ok(()));
|
||||
} else {
|
||||
m.expect_save_oauth_access_token()
|
||||
.returning(move |_, _, _| Ok(()));
|
||||
}
|
||||
|
||||
Arc::new(m)
|
||||
}
|
||||
}
|
|
@ -16,3 +16,23 @@ pub trait SaveOAuthState: Send + Sync {
|
|||
redirect_uri: &Url,
|
||||
) -> OutDBPortResult<()>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_save_oauth_state(times: Option<usize>) -> Arc<dyn SaveOAuthState> {
|
||||
let mut m = MockSaveOAuthState::new();
|
||||
if let Some(times) = times {
|
||||
m.expect_save_oauth_state()
|
||||
.times(times)
|
||||
.returning(|_, _, _| Ok(()));
|
||||
} else {
|
||||
m.expect_save_oauth_state().returning(|_, _, _| Ok(()));
|
||||
}
|
||||
|
||||
Arc::new(m)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
pub mod errors;
|
||||
pub mod get_username;
|
||||
pub mod oauth_auth_req_uri;
|
||||
pub mod refresh_access_token;
|
||||
pub mod request_access_token;
|
||||
|
|
|
@ -11,3 +11,27 @@ pub trait OAuthAuthReqUri: Send + Sync {
|
|||
process_authorization_response_uri: &Url,
|
||||
) -> OutForgePortResult<Url>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_oauth_auth_req_uri(
|
||||
times: Option<usize>,
|
||||
returning: Url,
|
||||
) -> Arc<dyn OAuthAuthReqUri> {
|
||||
let mut m = MockOAuthAuthReqUri::new();
|
||||
if let Some(times) = times {
|
||||
m.expect_oauth_auth_req_uri()
|
||||
.times(times)
|
||||
.returning(move |_, _| Ok(returning.clone()));
|
||||
} else {
|
||||
m.expect_oauth_auth_req_uri()
|
||||
.returning(move |_, _| Ok(returning.clone()));
|
||||
}
|
||||
|
||||
Arc::new(m)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,11 @@
|
|||
use super::errors::*;
|
||||
use crate::auth::domain::AuthCode;
|
||||
use crate::auth::domain::OAuthAccessToken;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait RefreshAccessToken {
|
||||
// returns ID of newly created user user
|
||||
async fn refresh_access_token(&self, auth_code: &AuthCode) -> OutForgePortResult<AuthCode>;
|
||||
async fn refresh_access_token(
|
||||
&self,
|
||||
auth_code: &OAuthAccessToken,
|
||||
) -> OutForgePortResult<OAuthAccessToken>;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,32 @@
|
|||
use super::errors::*;
|
||||
use crate::auth::domain::AuthCode;
|
||||
use mockall::predicate::*;
|
||||
use mockall::*;
|
||||
|
||||
use super::errors::*;
|
||||
use crate::auth::domain::OAuthAccessToken;
|
||||
|
||||
#[automock]
|
||||
#[async_trait::async_trait]
|
||||
pub trait RequestAccessToken: Send + Sync {
|
||||
// returns ID of newly created user user
|
||||
async fn request_access_token(&self, code: &str) -> OutForgePortResult<AuthCode>;
|
||||
async fn request_access_token(&self, code: &str) -> OutForgePortResult<OAuthAccessToken>;
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_request_access_token(times: Option<usize>) -> Arc<dyn RequestAccessToken> {
|
||||
let mut m = MockRequestAccessToken::default();
|
||||
if let Some(times) = times {
|
||||
m.expect_request_access_token()
|
||||
.times(times)
|
||||
.returning(|_| Ok(OAuthAccessToken::default()));
|
||||
} else {
|
||||
m.expect_request_access_token()
|
||||
.returning(|_| Ok(OAuthAccessToken::default()));
|
||||
}
|
||||
|
||||
Arc::new(m)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
//pub mod errors;
|
||||
pub mod process_authorization_response;
|
||||
pub mod request_authorization;
|
||||
|
||||
// ## 1. Request authorization
|
||||
// 1. Generate state
|
||||
// 2. Save state
|
||||
// 3. Redirect user to authorization page on forge:
|
||||
// Gitea: https://[YOUR-GITEA-URL]/login/oauth/authorize?client_id=CLIENT_ID&redirect_uri=REDIRECT_URI&response_type=code&state=STATE
|
||||
// GitHub: https://github.com/login/oauth/authorize?client_id=12345&state=abcdefg
|
||||
// ## 2. Process authorization response
|
||||
// 1. Get state code. If not present -> error
|
||||
// 2. Get access code against `code`
|
||||
// 3. Save access code, refresh_code and expires_in
|
||||
// ## 3. Refresh access toekn
|
||||
// 1. Get access code against `refresh_code`
|
||||
// 2. Save access code, refresh_code and expires_in
|
|
@ -0,0 +1,47 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use super::errors::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct ProcessAuthorizationResponseCommand {
|
||||
redirect_uri: Option<Url>,
|
||||
state: String,
|
||||
code: String,
|
||||
oauth_provider: String,
|
||||
}
|
||||
|
||||
impl ProcessAuthorizationResponseCommand {
|
||||
pub fn redirect_uri(&self) -> Option<&Url> {
|
||||
self.redirect_uri.as_ref()
|
||||
}
|
||||
|
||||
pub fn state(&self) -> &str {
|
||||
&self.state
|
||||
}
|
||||
|
||||
pub fn oauth_provider(&self) -> &str {
|
||||
&self.oauth_provider
|
||||
}
|
||||
|
||||
pub fn code(&self) -> &str {
|
||||
&self.code
|
||||
}
|
||||
|
||||
pub fn new_command(
|
||||
redirect_uri: Option<Url>,
|
||||
state: String,
|
||||
code: String,
|
||||
oauth_provider: String,
|
||||
) -> ProcessAuthorizationServiceResult<Self> {
|
||||
let state = state.trim().to_string();
|
||||
let oauth_provider = oauth_provider.trim().to_string();
|
||||
let code = code.trim().to_string();
|
||||
Ok(Self {
|
||||
state,
|
||||
code,
|
||||
redirect_uri,
|
||||
oauth_provider,
|
||||
})
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::auth::application::port::out::db::errors::OutDBPortError;
|
||||
use crate::auth::application::port::out::forge::errors::OutForgePortError;
|
||||
|
||||
pub type ProcessAuthorizationServiceResult<V> = Result<V, ProcessAuthorizationServiceError>;
|
||||
|
||||
#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ProcessAuthorizationServiceError {
|
||||
InteralError,
|
||||
BadRequest,
|
||||
}
|
||||
|
||||
impl From<OutDBPortError> for ProcessAuthorizationServiceError {
|
||||
fn from(v: OutDBPortError) -> Self {
|
||||
match v {
|
||||
OutDBPortError::DuplicateState => Self::InteralError, // only happens when there's a
|
||||
// bug in code
|
||||
OutDBPortError::InternalError => Self::InteralError,
|
||||
OutDBPortError::DuplicateAccessToken => Self::InteralError, // only happens when bug in
|
||||
// code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OutForgePortError> for ProcessAuthorizationServiceError {
|
||||
fn from(v: OutForgePortError) -> Self {
|
||||
match v {
|
||||
OutForgePortError::InteralError => Self::InteralError,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
mod command;
|
||||
pub mod errors;
|
||||
mod service;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait ProcessAuthorizationResponseUserCase {
|
||||
async fn process_authorization_response(
|
||||
&self,
|
||||
cmd: command::ProcessAuthorizationResponseCommand,
|
||||
) -> errors::ProcessAuthorizationServiceResult<()>;
|
||||
}
|
|
@ -0,0 +1,142 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use url::Url;
|
||||
|
||||
use crate::auth::application::port::out::db::{
|
||||
delete_oauth_state::DeleteOAuthState, oauth_state_exists::OAuthStateExists,
|
||||
save_oauth_access_token::SaveOAuthAccessToken,
|
||||
};
|
||||
use crate::auth::application::port::out::forge::{
|
||||
get_username::GetUsername, request_access_token::RequestAccessToken,
|
||||
};
|
||||
|
||||
use super::{command, errors::*, ProcessAuthorizationResponseUserCase};
|
||||
|
||||
const STATE_LEN: usize = 8;
|
||||
|
||||
pub struct ProcessAuthorizationResponseProcessAuthorizationService {
|
||||
oauth_state_exists_adapter: Arc<dyn OAuthStateExists>,
|
||||
delete_oauth_state_adapter: Arc<dyn DeleteOAuthState>,
|
||||
save_oauth_access_token_adapter: Arc<dyn SaveOAuthAccessToken>,
|
||||
request_access_token_adapter: Arc<dyn RequestAccessToken>,
|
||||
get_username_adapter: Arc<dyn GetUsername>,
|
||||
process_authorization_response_redirect_uri: Url,
|
||||
}
|
||||
|
||||
impl ProcessAuthorizationResponseProcessAuthorizationService {
|
||||
pub fn new(
|
||||
oauth_state_exists_adapter: Arc<dyn OAuthStateExists>,
|
||||
delete_oauth_state_adapter: Arc<dyn DeleteOAuthState>,
|
||||
save_oauth_access_token_adapter: Arc<dyn SaveOAuthAccessToken>,
|
||||
request_access_token_adapter: Arc<dyn RequestAccessToken>,
|
||||
get_username_adapter: Arc<dyn GetUsername>,
|
||||
process_authorization_response_redirect_uri: Url,
|
||||
) -> Self {
|
||||
Self {
|
||||
oauth_state_exists_adapter,
|
||||
delete_oauth_state_adapter,
|
||||
save_oauth_access_token_adapter,
|
||||
request_access_token_adapter,
|
||||
get_username_adapter,
|
||||
process_authorization_response_redirect_uri,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl ProcessAuthorizationResponseUserCase
|
||||
for ProcessAuthorizationResponseProcessAuthorizationService
|
||||
{
|
||||
async fn process_authorization_response(
|
||||
&self,
|
||||
cmd: command::ProcessAuthorizationResponseCommand,
|
||||
) -> ProcessAuthorizationServiceResult<()> {
|
||||
if let Some(u) = cmd.redirect_uri() {
|
||||
if u.host() != self.process_authorization_response_redirect_uri.host()
|
||||
&& u.path() != self.process_authorization_response_redirect_uri.path()
|
||||
{
|
||||
return Err(ProcessAuthorizationServiceError::BadRequest);
|
||||
}
|
||||
}
|
||||
|
||||
if !self
|
||||
.oauth_state_exists_adapter
|
||||
.oauth_state_exists(
|
||||
cmd.state(),
|
||||
cmd.oauth_provider(),
|
||||
&self.process_authorization_response_redirect_uri,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
return Err(ProcessAuthorizationServiceError::BadRequest);
|
||||
}
|
||||
|
||||
self.delete_oauth_state_adapter
|
||||
.delete_oauth_state(cmd.state(), cmd.oauth_provider())
|
||||
.await?;
|
||||
let access_token = self
|
||||
.request_access_token_adapter
|
||||
.request_access_token(
|
||||
cmd.code().into(),
|
||||
self.process_authorization_response_redirect_uri.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let username = self
|
||||
.get_username_adapter
|
||||
.get_username(&access_token)
|
||||
.await?;
|
||||
|
||||
self.save_oauth_access_token_adapter
|
||||
.save_oauth_access_token(&username, &cmd.oauth_provider(), &access_token)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::auth::application::port::out::db::{
|
||||
delete_oauth_state::tests::*, oauth_state_exists::tests::*,
|
||||
save_oauth_access_token::tests::*,
|
||||
};
|
||||
|
||||
use crate::auth::application::port::out::forge::{
|
||||
get_username::tests::*, request_access_token::tests::*,
|
||||
};
|
||||
|
||||
use crate::tests::bdd::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_process_authorization_response() {
|
||||
let username = "foo";
|
||||
let state = "bar";
|
||||
let code = "baz";
|
||||
let oauth_provider = "test_process_authorization_response_service";
|
||||
let url = Url::parse(&format!("http://{oauth_provider}")).unwrap();
|
||||
let mut redirect_uri = url.clone();
|
||||
redirect_uri.set_query(Some(&format!("state={state}")));
|
||||
|
||||
let cmd = command::ProcessAuthorizationResponseCommand::new_command(
|
||||
Some(redirect_uri.clone()),
|
||||
state.into(),
|
||||
code.into(),
|
||||
oauth_provider.into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let s = ProcessAuthorizationResponseProcessAuthorizationService::new(
|
||||
mock_oauth_state_exists(IS_CALLED_ONLY_ONCE, RETURNS_TRUE),
|
||||
mock_delete_oauth_state(IS_CALLED_ONLY_ONCE),
|
||||
mock_save_oauth_access_token(IS_CALLED_ONLY_ONCE),
|
||||
mock_request_access_token(IS_CALLED_ONLY_ONCE),
|
||||
mock_get_username(username.into(), IS_CALLED_ONLY_ONCE),
|
||||
redirect_uri,
|
||||
);
|
||||
|
||||
s.process_authorization_response(cmd).await.unwrap();
|
||||
}
|
||||
}
|
|
@ -14,7 +14,11 @@ pub enum RequestAuthorizationServiceError {
|
|||
impl From<OutDBPortError> for RequestAuthorizationServiceError {
|
||||
fn from(v: OutDBPortError) -> Self {
|
||||
match v {
|
||||
OutDBPortError::DuplicateState => Self::InteralError,
|
||||
OutDBPortError::DuplicateState => Self::InteralError, // only happens when there's a
|
||||
// bug in code
|
||||
OutDBPortError::InternalError => Self::InteralError,
|
||||
OutDBPortError::DuplicateAccessToken => Self::InteralError, // only happens when bug in
|
||||
// code
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -73,14 +73,14 @@ impl RequestAuthorizationUserCase for RequestAuthorizationService {
|
|||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::auth::application::{
|
||||
port::out::{
|
||||
db::save_oauth_state::MockSaveOAuthState,
|
||||
forge::oauth_auth_req_uri::MockOAuthAuthReqUri,
|
||||
use crate::utils::random_string::tests::*;
|
||||
use crate::{
|
||||
auth::application::{
|
||||
port::out::{db::save_oauth_state::tests::*, forge::oauth_auth_req_uri::tests::*},
|
||||
services::request_authorization::command::RequestAuthorizationCommand,
|
||||
},
|
||||
services::request_authorization::command::RequestAuthorizationCommand,
|
||||
tests::bdd::IS_CALLED_ONLY_ONCE,
|
||||
};
|
||||
use crate::utils::random_string::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
|
@ -93,30 +93,18 @@ mod tests {
|
|||
let mut redirect_uri = url.clone();
|
||||
redirect_uri.set_query(Some(&format!("state={random_string}")));
|
||||
|
||||
let mut mock_random_generate_string = MockGenerateRandomStringInterface::new();
|
||||
mock_random_generate_string
|
||||
.expect_get_random()
|
||||
.times(1)
|
||||
.return_const(random_string.to_string());
|
||||
let mock_random_generate_string =
|
||||
mock_generate_random_string(IS_CALLED_ONLY_ONCE, random_string.into());
|
||||
|
||||
let r = redirect_uri.clone();
|
||||
let mut mock_oauth_req_uri = MockOAuthAuthReqUri::new();
|
||||
mock_oauth_req_uri
|
||||
.expect_oauth_auth_req_uri()
|
||||
.times(1)
|
||||
.returning(move |_, _| Ok(r.clone()));
|
||||
let mock_oauth_req_uri = mock_oauth_auth_req_uri(IS_CALLED_ONLY_ONCE, redirect_uri.clone());
|
||||
|
||||
let mut mock_save_oauth_state = MockSaveOAuthState::new();
|
||||
mock_save_oauth_state
|
||||
.expect_save_oauth_state()
|
||||
.times(1)
|
||||
.returning(|_, _, _| Ok(()));
|
||||
let mock_save_oauth_state = mock_save_oauth_state(IS_CALLED_ONLY_ONCE);
|
||||
|
||||
let s = RequestAuthorizationService::new(
|
||||
Arc::new(mock_save_oauth_state),
|
||||
Arc::new(mock_oauth_req_uri),
|
||||
mock_save_oauth_state,
|
||||
mock_oauth_req_uri,
|
||||
url.clone(),
|
||||
Arc::new(mock_random_generate_string),
|
||||
mock_random_generate_string,
|
||||
);
|
||||
let cmd = RequestAuthorizationCommand::new_command(oauth_provider.to_owned()).unwrap();
|
||||
|
||||
|
|
|
@ -6,6 +6,8 @@ use db::migrate::RunMigrations;
|
|||
mod auth;
|
||||
mod db;
|
||||
mod settings;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod utils;
|
||||
|
||||
#[actix_web::main]
|
||||
|
|
|
@ -0,0 +1,2 @@
|
|||
pub const IS_CALLED_ONLY_ONCE: Option<usize> = Some(1);
|
||||
pub const RETURNS_TRUE: bool = true;
|
|
@ -0,0 +1 @@
|
|||
pub mod bdd;
|
|
@ -38,3 +38,27 @@ impl GenerateRandomString {
|
|||
Box::new(f)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
pub fn mock_generate_random_string(
|
||||
times: Option<usize>,
|
||||
returning: String,
|
||||
) -> Arc<dyn GenerateRandomStringInterface> {
|
||||
let mut m = MockGenerateRandomStringInterface::new();
|
||||
|
||||
if let Some(times) = times {
|
||||
m.expect_get_random()
|
||||
.times(times)
|
||||
.return_const(returning.to_string());
|
||||
} else {
|
||||
m.expect_get_random().return_const(returning.to_string());
|
||||
}
|
||||
|
||||
Arc::new(m)
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue