Compare commits

...

12 commits

29 changed files with 792 additions and 58 deletions

94
Cargo.lock generated
View file

@ -261,6 +261,16 @@ version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "038dfcf04a5feb68e9c60b21c9625a54c2c0616e79b72b0fd87075a056ae1d1b"
[[package]]
name = "assert-json-diff"
version = "2.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12"
dependencies = [
"serde",
"serde_json",
]
[[package]]
name = "async-trait"
version = "0.1.80"
@ -455,6 +465,16 @@ dependencies = [
"phf_codegen",
]
[[package]]
name = "colored"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cbf2150cce219b664a8a70df7a1f933836724b503f8a413af9365b4dcc4d90b8"
dependencies = [
"lazy_static",
"windows-sys 0.48.0",
]
[[package]]
name = "config"
version = "0.14.0"
@ -829,6 +849,7 @@ dependencies = [
"lazy_static",
"log",
"mockall",
"mockito",
"pretty_env_logger",
"rand",
"reqwest",
@ -1113,6 +1134,17 @@ dependencies = [
"itoa",
]
[[package]]
name = "http-body"
version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2"
dependencies = [
"bytes",
"http 0.2.12",
"pin-project-lite",
]
[[package]]
name = "http-body"
version = "1.0.0"
@ -1132,7 +1164,7 @@ dependencies = [
"bytes",
"futures-core",
"http 1.1.0",
"http-body",
"http-body 1.0.0",
"pin-project-lite",
]
@ -1163,6 +1195,29 @@ version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4"
[[package]]
name = "hyper"
version = "0.14.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80"
dependencies = [
"bytes",
"futures-channel",
"futures-core",
"futures-util",
"h2 0.3.26",
"http 0.2.12",
"http-body 0.4.6",
"httparse",
"httpdate",
"itoa",
"pin-project-lite",
"tokio",
"tower-service",
"tracing",
"want",
]
[[package]]
name = "hyper"
version = "1.3.1"
@ -1174,7 +1229,7 @@ dependencies = [
"futures-util",
"h2 0.4.4",
"http 1.1.0",
"http-body",
"http-body 1.0.0",
"httparse",
"itoa",
"pin-project-lite",
@ -1191,7 +1246,7 @@ checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
dependencies = [
"bytes",
"http-body-util",
"hyper",
"hyper 1.3.1",
"hyper-util",
"native-tls",
"tokio",
@ -1209,8 +1264,8 @@ dependencies = [
"futures-channel",
"futures-util",
"http 1.1.0",
"http-body",
"hyper",
"http-body 1.0.0",
"hyper 1.3.1",
"pin-project-lite",
"socket2",
"tokio",
@ -1498,6 +1553,25 @@ dependencies = [
"syn 2.0.60",
]
[[package]]
name = "mockito"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f6e023aa5bdf392aa06c78e4a4e6d498baab5138d0c993503350ebbc37bf1e"
dependencies = [
"assert-json-diff",
"colored",
"futures-core",
"hyper 0.14.28",
"log",
"rand",
"regex",
"serde_json",
"serde_urlencoded",
"similar",
"tokio",
]
[[package]]
name = "mutually_exclusive_features"
version = "0.0.3"
@ -2011,9 +2085,9 @@ dependencies = [
"futures-util",
"h2 0.4.4",
"http 1.1.0",
"http-body",
"http-body 1.0.0",
"http-body-util",
"hyper",
"hyper 1.3.1",
"hyper-tls",
"hyper-util",
"ipnet",
@ -2368,6 +2442,12 @@ dependencies = [
"rand_core",
]
[[package]]
name = "similar"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640"
[[package]]
name = "siphasher"
version = "0.3.11"

View file

@ -34,3 +34,4 @@ mockall = "0.12"
[dev-dependencies]
actix-rt= "2.9"
mockall = { version = "0.12", features = ["nightly"] }
mockito = "1.4.0"

View file

@ -1,6 +1,5 @@
use std::sync::Arc;
use actix_web::{http::header, HttpResponse};
use url::Url;
use crate::auth::application::port::input::ui::{errors::*, login::RequestAuthorizationInterface};
@ -38,7 +37,7 @@ impl RequestAuthorizationHandler {
#[async_trait::async_trait]
impl RequestAuthorizationInterface for RequestAuthorizationHandler {
#[tracing::instrument(name = "web adapter request_oauth_authorization", skip(self))]
async fn request_oauth_authorization(&self, forge_name: String) -> InUIResult<HttpResponse> {
async fn request_oauth_authorization(&self, forge_name: String) -> InUIResult<Url> {
let service = RequestAuthorizationService::new(
self.save_oauth_state_adapter.clone(),
self.oauth_auth_req_uri_adapter.clone(),
@ -48,16 +47,12 @@ impl RequestAuthorizationInterface for RequestAuthorizationHandler {
let cmd = RequestAuthorizationCommand::new_command(forge_name)?;
let auth_page = service.request_authorization(cmd).await?;
Ok(HttpResponse::Found()
.insert_header((header::LOCATION, auth_page.as_str()))
.finish())
Ok(auth_page)
}
}
#[cfg(test)]
mod tests {
use actix_web::http::{header, StatusCode};
use super::*;
use crate::auth::application::port::out::{
db::save_oauth_state::tests::*, forge::oauth_auth_req_uri::tests::*,
@ -89,14 +84,6 @@ mod tests {
.request_oauth_authorization(oauth_provider.into())
.await
.unwrap();
assert_eq!(res.status(), StatusCode::FOUND);
assert_eq!(
res.headers()
.get(header::LOCATION)
.unwrap()
.to_str()
.unwrap(),
redirect_uri.as_str()
);
assert_eq!(res, redirect_uri);
}
}

View file

@ -1,4 +1,4 @@
use actix_web::{post, web, HttpResponse};
use actix_web::{http::header, post, web, HttpResponse};
use url::Url;
use super::types;
@ -47,9 +47,13 @@ async fn handler(
process_authorization_response_redirect_uri,
);
web_adapter
let redirect_to = web_adapter
.request_oauth_authorization(SupportedForges::Forgejo.to_string())
.await
.await?;
Ok(HttpResponse::Found()
.insert_header((header::LOCATION, redirect_to.as_str()))
.finish())
}
#[cfg(test)]

View file

@ -79,7 +79,7 @@ mod tests {
let page = LoginPageTemplate.get_login_page(ctx).unwrap();
for forge in forges.iter() {
assert!(page.contains(&forge.to_string()));
assert!(page.contains(&routes.oauth_login(&forge)));
assert!(page.contains(&routes.oauth_login(forge)));
}
}
}

View file

@ -3,6 +3,7 @@ use std::sync::Arc;
use actix_web::web;
pub mod login;
mod process_authorization;
mod routes;
mod template;
pub mod types;
@ -15,6 +16,7 @@ pub fn load_ctx() -> impl FnOnce(&mut web::ServiceConfig) {
let f = move |cfg: &mut web::ServiceConfig| {
cfg.app_data(routes);
cfg.configure(login::services);
cfg.configure(process_authorization::services);
};
Box::new(f)

View file

@ -0,0 +1,141 @@
use std::sync::Arc;
use url::Url;
use crate::auth::application::port::input::ui::{
errors::*, process_authorization::ProcessAuthorizationInterface,
};
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 crate::auth::application::services::process_authorization_response::{
command::ProcessAuthorizationResponseCommand, service::ProcessAuthorizationResponseService,
ProcessAuthorizationResponseUseCase,
};
pub struct ProcessAuthorizationAdapter {
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 ProcessAuthorizationAdapter {
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 ProcessAuthorizationInterface for ProcessAuthorizationAdapter {
#[tracing::instrument(name = "web adapter process_authorization", skip(self, code))]
async fn process_authorization(
&self,
code: String,
state: String,
oauth_provider: String,
redirect_uri: Option<Url>,
) -> InUIResult<()> {
let service = ProcessAuthorizationResponseService::new(
self.oauth_state_exists_adapter.clone(),
self.delete_oauth_state_adapter.clone(),
self.save_oauth_access_token_adapter.clone(),
self.request_access_token_adapter.clone(),
self.get_username_adapter.clone(),
self.process_authorization_response_redirect_uri.clone(),
);
let cmd = ProcessAuthorizationResponseCommand::new_command(
redirect_uri,
state,
code,
oauth_provider,
)?;
service.process_authorization_response(cmd).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::*,
},
forge::{get_username::tests::*, request_access_token::tests::*},
};
use crate::tests::bdd::*;
#[actix_web::test]
async fn test_adapter() {
let random_string = "foorand";
let username = "processauthouathusername";
let url = Url::parse("http://test_ui_req_auth_interface_adapter").unwrap();
let oauth_provider = "test_ui_req_auth_interface_adapter";
let code = "code";
let state = "state";
let mut redirect_uri = url.clone();
redirect_uri.set_query(Some(&format!("state={random_string}")));
// authorization response with redirect_uri = None
{
let adapter = ProcessAuthorizationAdapter::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),
url.clone(),
);
adapter
.process_authorization(code.into(), state.into(), oauth_provider.into(), None)
.await
.unwrap();
}
// authorization response with redirect_uri = Some(Url)
{
let adapter = ProcessAuthorizationAdapter::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),
url.clone(),
);
adapter
.process_authorization(
code.into(),
state.into(),
oauth_provider.into(),
Some(url.clone()),
)
.await
.unwrap();
}
}
}

View file

@ -0,0 +1,189 @@
use actix_web::{get, web, HttpResponse};
use serde::{Deserialize, Serialize};
use url::Url;
use super::types;
use crate::auth::adapter::out::forge::SupportedForges;
use crate::auth::application::port::input::ui::{
errors::*, process_authorization::ProcessAuthorizationInterface,
};
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(handler);
}
#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
struct ProcessAuthorizationReqQueryParams {
code: String,
state: String,
redirect_uri: Option<Url>,
}
#[get("/oauth/forgejo/authorize")]
#[tracing::instrument(
name = "login page handler",
skip(
forges,
oauth_state_exists_adapter,
delete_oauth_state_adapter,
save_oauth_access_token_adapter,
routes,
settings,
q,
)
)]
async fn handler(
forges: types::WebForgeRepositoryInterface,
oauth_state_exists_adapter: types::WebOauthStateExists,
delete_oauth_state_adapter: types::WebDeleteOauthState,
save_oauth_access_token_adapter: types::WebSaveOAuthAccessToken,
routes: types::WebRouteRepository,
settings: types::WebSettings,
q: web::Query<ProcessAuthorizationReqQueryParams>,
) -> InUIResult<HttpResponse> {
let request_access_token_adapter = forges
.get_forge_factory(&SupportedForges::Forgejo)
.unwrap()
.request_access_token_adapter();
let get_username_adapter = forges
.get_forge_factory(&SupportedForges::Forgejo)
.unwrap()
.get_username_adapter();
let process_authorization_response_redirect_uri = Url::parse(&format!(
"{}://{}{}",
"http",
&settings.server.domain,
&routes.process_oauth_authorization_response(&SupportedForges::Forgejo)
))
.map_err(|_| InUIError::InternalServerError)?;
let web_adapter = super::adapter::ProcessAuthorizationAdapter::new(
oauth_state_exists_adapter.as_ref().clone(),
delete_oauth_state_adapter.as_ref().clone(),
save_oauth_access_token_adapter.as_ref().clone(),
request_access_token_adapter.clone(),
get_username_adapter.clone(),
process_authorization_response_redirect_uri,
);
let q = q.into_inner();
web_adapter
.process_authorization(
q.code,
q.state,
SupportedForges::Forgejo.to_string(),
q.redirect_uri,
)
.await?;
Ok(HttpResponse::Ok().finish())
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use actix_web::http::StatusCode;
use actix_web::{http::header::ContentType, test, App};
use crate::auth::adapter::input::web::routes::RoutesRepository;
use crate::auth::adapter::out::forge::forge_factory::{
ForgeAdapterFactoryInterface, MockForgeAdapterFactoryInterface,
};
use crate::auth::adapter::out::forge::forge_repository::MockForgeRepositoryInterface;
use crate::auth::application::port::out::{
db::{
delete_oauth_state::tests::*, oauth_state_exists::tests::*,
save_oauth_access_token::tests::*,
},
forge::{get_username::tests::*, request_access_token::tests::*},
};
use crate::tests::bdd::*;
#[actix_web::test]
async fn test_ui_handler_process_oauth_authorization_forgejo() {
let username = "foorand";
let state = "hstate";
let code = "hcode";
let settings = crate::settings::Settings::new().unwrap();
let mock_delete_oauth_state =
types::WebDeleteOauthState::new(mock_delete_oauth_state(IS_CALLED_ONLY_ONCE));
let mock_oauth_state_exists = types::WebOauthStateExists::new(mock_oauth_state_exists(
IS_CALLED_ONLY_ONCE,
RETURNS_TRUE,
));
let mock_save_oauth_access_token =
types::WebSaveOAuthAccessToken::new(mock_save_oauth_access_token(IS_CALLED_ONLY_ONCE));
let mock_forges = {
let mut mock_forge_factory = MockForgeAdapterFactoryInterface::default();
mock_forge_factory
.expect_request_access_token_adapter()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.returning(|| mock_request_access_token(IS_CALLED_ONLY_ONCE));
mock_forge_factory
.expect_get_username_adapter()
.times(IS_CALLED_ONLY_ONCE.unwrap())
.returning(|| mock_get_username(username.into(), IS_CALLED_ONLY_ONCE));
let mock_forge_factory: Arc<dyn ForgeAdapterFactoryInterface> =
Arc::new(mock_forge_factory);
let mut mock_forges = MockForgeRepositoryInterface::default();
mock_forges
.expect_get_forge_factory()
.times(IS_CALLED_ONLY_TWICE.unwrap())
.returning(move |_| Some(mock_forge_factory.clone()));
types::WebForgeRepositoryInterface::new(Arc::new(mock_forges))
};
let routes = RoutesRepository::default();
let process_authorization_response_redirect_uri = Url::parse(&format!(
"{}://{}{}",
"http",
&settings.server.domain,
&routes.process_oauth_authorization_response(&SupportedForges::Forgejo)
))
.unwrap();
let app = test::init_service(
App::new()
.wrap(tracing_actix_web::TracingLogger::default())
.app_data(mock_oauth_state_exists)
.app_data(mock_save_oauth_access_token)
.app_data(mock_forges)
.app_data(mock_delete_oauth_state)
.app_data(web::Data::new(Arc::new(routes.clone())))
.app_data(web::Data::new(settings.clone()))
.service(handler),
)
.await;
let path = {
let mut u = process_authorization_response_redirect_uri.clone();
u.set_path("/oauth/forgejo/authorize");
u.set_query(Some(&format!(
"state={state}&code={code}&redirect_uri={}",
process_authorization_response_redirect_uri
)));
format!("{}?{}", u.path(), u.query().unwrap())
};
println!("path: {path}");
let req = test::TestRequest::get()
.uri(&path)
.insert_header(ContentType::html())
.to_request();
let resp = test::call_service(&app, req).await;
let status = resp.status();
assert_eq!(status, StatusCode::OK);
}
}

View file

@ -0,0 +1,10 @@
use actix_web::web;
use super::types;
mod adapter;
mod handlers;
pub fn services(cfg: &mut web::ServiceConfig) {
handlers::services(cfg);
}

View file

@ -7,9 +7,6 @@ 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;
@ -24,6 +21,3 @@ 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>>;

View file

@ -4,11 +4,16 @@ use mockall::predicate::*;
use mockall::*;
use super::forgejo::Forgejo;
use crate::auth::application::port::out::forge::oauth_auth_req_uri::OAuthAuthReqUri;
use crate::auth::application::port::out::forge::{
get_username::GetUsername, oauth_auth_req_uri::OAuthAuthReqUri,
request_access_token::RequestAccessToken,
};
#[automock]
pub trait ForgeAdapterFactoryInterface: Send + Sync {
fn get_oauth_auth_req_uri_adapter(&self) -> Arc<dyn OAuthAuthReqUri>;
fn oauth_auth_req_uri_adapter(&self) -> Arc<dyn OAuthAuthReqUri>;
fn request_access_token_adapter(&self) -> Arc<dyn RequestAccessToken>;
fn get_username_adapter(&self) -> Arc<dyn GetUsername>;
}
#[derive(Clone)]
@ -17,7 +22,13 @@ pub struct ForgeAdapterFactory {
}
impl ForgeAdapterFactoryInterface for ForgeAdapterFactory {
fn get_oauth_auth_req_uri_adapter(&self) -> Arc<dyn OAuthAuthReqUri> {
fn oauth_auth_req_uri_adapter(&self) -> Arc<dyn OAuthAuthReqUri> {
self.forgejo.clone()
}
fn request_access_token_adapter(&self) -> Arc<dyn RequestAccessToken> {
self.forgejo.clone()
}
fn get_username_adapter(&self) -> Arc<dyn GetUsername> {
self.forgejo.clone()
}
}

View file

@ -0,0 +1,82 @@
use serde::{Deserialize, Serialize};
use super::Forgejo;
use crate::auth::application::port::out::forge::{
errors::OutForgePortResult, get_username::GetUsername,
};
use crate::auth::domain::OAuthAccessToken;
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
struct ForgejoUserResponse {
login: String,
}
#[async_trait::async_trait]
impl GetUsername for Forgejo {
async fn get_username(&self, access_token: &OAuthAccessToken) -> OutForgePortResult<String> {
let u = {
let mut u = self.url().to_owned();
u.set_path("/api/v1/user");
u
};
let res = self
.http_client
.get(u)
.header(
reqwest::header::AUTHORIZATION,
format!("token {}", access_token.access_token),
)
.send()
.await
.unwrap();
let res: ForgejoUserResponse = res.json().await.unwrap();
Ok(res.login)
}
}
#[cfg(test)]
mod tests {
use super::*;
use url::Url;
#[actix_rt::test]
async fn test_forgejo_get_username() {
let client_id = "statetestpostgres";
let client_secret = "oauthprovitestpostgres";
let access_token = "access_token_uuu";
let username = "access_token_user";
let mut settings = crate::settings::tests::get_settings().await;
let mut srv = mockito::Server::new_async().await;
settings.forges.forgejo.url = Url::parse(&srv.url()).unwrap();
let f = Forgejo::new(
settings.forges.forgejo.url.clone(),
client_id.into(),
client_secret.into(),
);
let mock = {
let resp = ForgejoUserResponse {
login: username.into(),
};
srv.mock("GET", "/api/v1/user")
.with_status(200)
.match_header("Authorization", format!("token {access_token}").as_str())
.with_header("Content-Type", "application/json")
.with_body(serde_json::to_string(&resp).unwrap())
.create_async()
.await
};
let req = OAuthAccessToken {
access_token: access_token.into(),
..Default::default()
};
assert_eq!(f.get_username(&req).await.unwrap(), username);
mock.assert_async().await;
}
}

View file

@ -1,12 +1,16 @@
use reqwest::Client;
use url::Url;
pub mod get_username;
pub mod oauth_auth_req_uri;
pub mod request_access_token;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[derive(Debug, Clone)]
pub struct Forgejo {
url: Url,
client_id: String,
client_secret: String,
pub http_client: Client,
}
impl Forgejo {
@ -15,6 +19,7 @@ impl Forgejo {
url,
client_id,
client_secret,
http_client: Client::default(),
}
}

View file

@ -0,0 +1,133 @@
use std::time::Duration;
use serde::{Deserialize, Serialize};
use url::Url;
use super::Forgejo;
use crate::auth::application::port::out::forge::{
errors::OutForgePortResult, request_access_token::RequestAccessToken,
};
use crate::auth::domain::OAuthAccessToken;
#[derive(Clone, Debug, Default, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
struct ForgejoAccessTokenResponse {
refresh_token: String,
access_token: String,
token_type: String,
expires_in: u64,
}
impl From<ForgejoAccessTokenResponse> for OAuthAccessToken {
fn from(v: ForgejoAccessTokenResponse) -> Self {
let expires_at = Duration::from_secs(v.expires_in);
Self {
refresh_token: v.refresh_token,
access_token: v.access_token,
token_type: v.token_type,
expires_at, // unix epoch of the instant
expires_in: v.expires_in,
}
}
}
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
struct ForgejoAccessTokenRequest {
code: String,
client_id: String,
client_secret: String,
grant_type: String,
redirect_uri: Url,
}
impl ForgejoAccessTokenRequest {
pub fn new(code: String, redirect_uri: Url, forgejo: &Forgejo) -> Self {
Self {
code,
client_id: forgejo.client_id.clone(),
client_secret: forgejo.client_secret.clone(),
grant_type: "authorization_code".into(),
redirect_uri,
}
}
}
#[async_trait::async_trait]
impl RequestAccessToken for Forgejo {
async fn request_access_token(
&self,
code: String,
redirect_uri: Url,
) -> OutForgePortResult<OAuthAccessToken> {
let u = {
let mut u = self.url().to_owned();
u.set_path("/login/oauth/access_token");
u
};
let payload = ForgejoAccessTokenRequest::new(code, redirect_uri, self);
let res: ForgejoAccessTokenResponse = self
.http_client
.post(u)
.json(&payload)
.send()
.await
.unwrap()
.json()
.await
.unwrap();
Ok(res.into())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[actix_rt::test]
async fn test_forgejo_request_access_token() {
let client_id = "statetestpostgres";
let client_secret = "oauthprovitestpostgres";
let code = "oauthprovitestpostgres";
let redirect_uri = Url::parse("https://oauthprovitestpostgres").unwrap();
let mut settings = crate::settings::tests::get_settings().await;
let mut srv = mockito::Server::new_async().await;
settings.forges.forgejo.url = Url::parse(&srv.url()).unwrap();
let data = ForgejoAccessTokenResponse {
access_token: "foo".into(),
expires_in: 9999,
..Default::default()
};
let f = Forgejo::new(
settings.forges.forgejo.url.clone(),
client_id.into(),
client_secret.into(),
);
let mock = {
let payload = ForgejoAccessTokenRequest::new(code.into(), redirect_uri.clone(), &f);
srv.mock("POST", "/login/oauth/access_token")
.with_status(200)
.match_header("content-type", "application/json")
.match_body(mockito::Matcher::PartialJsonString(
serde_json::to_string(&payload).unwrap(),
))
.with_header("Content-Type", "application/json")
.with_body(serde_json::to_string(&data).unwrap())
.create_async()
.await
};
let access_token = f
.request_access_token(code.into(), redirect_uri)
.await
.unwrap();
let expected_access_token: OAuthAccessToken = data.into();
assert_eq!(expected_access_token, access_token);
mock.assert_async().await;
}
}

View file

@ -0,0 +1,2 @@
pub mod port;
pub mod services;

View file

@ -0,0 +1 @@
pub mod ui;

View file

@ -1,7 +1,8 @@
use super::errors::*;
use actix_web::HttpResponse;
#[async_trait::async_trait]
pub trait RequestAuthorizationInterface: Send + Sync {
async fn request_oauth_authorization(&self, forge_name: String) -> InUIResult<HttpResponse>;
// TODO: input ports must not be dependent on the type of port. In this case,
// it should return redirect URL only
async fn request_oauth_authorization(&self, forge_name: String) -> InUIResult<url::Url>;
}

View file

@ -1,3 +1,4 @@
pub mod errors;
pub mod login;
pub mod process_authorization;
// login

View file

@ -0,0 +1,14 @@
use url::Url;
use super::errors::*;
#[async_trait::async_trait]
pub trait ProcessAuthorizationInterface: Send + Sync {
async fn process_authorization(
&self,
code: String,
state: String,
oauth_provider: String,
redirect_uri: Option<Url>,
) -> InUIResult<()>;
}

View file

@ -0,0 +1,2 @@
pub mod input;
pub mod out;

View file

@ -0,0 +1,32 @@
use mockall::predicate::*;
use mockall::*;
use super::errors::*;
use crate::auth::domain::OAuthAccessToken;
#[automock]
#[async_trait::async_trait]
pub trait GetUsername: Send + Sync {
async fn get_username(&self, access_token: &OAuthAccessToken) -> OutForgePortResult<String>;
}
#[cfg(test)]
pub mod tests {
use super::*;
use std::sync::Arc;
pub fn mock_get_username(username: String, times: Option<usize>) -> Arc<dyn GetUsername> {
let mut m = MockGetUsername::default();
if let Some(times) = times {
m.expect_get_username()
.times(times)
.returning(move |_| Ok(username.clone()));
} else {
m.expect_get_username()
.returning(move |_| Ok(username.clone()));
}
Arc::new(m)
}
}

View file

@ -1,5 +1,6 @@
use mockall::predicate::*;
use mockall::*;
use url::Url;
use super::errors::*;
use crate::auth::domain::OAuthAccessToken;
@ -7,7 +8,11 @@ use crate::auth::domain::OAuthAccessToken;
#[automock]
#[async_trait::async_trait]
pub trait RequestAccessToken: Send + Sync {
async fn request_access_token(&self, code: &str) -> OutForgePortResult<OAuthAccessToken>;
async fn request_access_token(
&self,
code: String,
redirect_uri: Url,
) -> OutForgePortResult<OAuthAccessToken>;
}
#[cfg(test)]
@ -21,10 +26,10 @@ pub mod tests {
if let Some(times) = times {
m.expect_request_access_token()
.times(times)
.returning(|_| Ok(OAuthAccessToken::default()));
.returning(|_, _| Ok(OAuthAccessToken::default()));
} else {
m.expect_request_access_token()
.returning(|_| Ok(OAuthAccessToken::default()));
.returning(|_, _| Ok(OAuthAccessToken::default()));
}
Arc::new(m)

View file

@ -0,0 +1,2 @@
pub mod db;
pub mod forge;

View file

@ -1,9 +1,9 @@
mod command;
pub mod command;
pub mod errors;
mod service;
pub mod service;
#[async_trait::async_trait]
pub trait ProcessAuthorizationResponseUserCase {
pub trait ProcessAuthorizationResponseUseCase {
async fn process_authorization_response(
&self,
cmd: command::ProcessAuthorizationResponseCommand,

View file

@ -10,11 +10,9 @@ use crate::auth::application::port::out::forge::{
get_username::GetUsername, request_access_token::RequestAccessToken,
};
use super::{command, errors::*, ProcessAuthorizationResponseUserCase};
use super::{command, errors::*, ProcessAuthorizationResponseUseCase};
const STATE_LEN: usize = 8;
pub struct ProcessAuthorizationResponseProcessAuthorizationService {
pub struct ProcessAuthorizationResponseService {
oauth_state_exists_adapter: Arc<dyn OAuthStateExists>,
delete_oauth_state_adapter: Arc<dyn DeleteOAuthState>,
save_oauth_access_token_adapter: Arc<dyn SaveOAuthAccessToken>,
@ -23,7 +21,7 @@ pub struct ProcessAuthorizationResponseProcessAuthorizationService {
process_authorization_response_redirect_uri: Url,
}
impl ProcessAuthorizationResponseProcessAuthorizationService {
impl ProcessAuthorizationResponseService {
pub fn new(
oauth_state_exists_adapter: Arc<dyn OAuthStateExists>,
delete_oauth_state_adapter: Arc<dyn DeleteOAuthState>,
@ -44,9 +42,7 @@ impl ProcessAuthorizationResponseProcessAuthorizationService {
}
#[async_trait::async_trait]
impl ProcessAuthorizationResponseUserCase
for ProcessAuthorizationResponseProcessAuthorizationService
{
impl ProcessAuthorizationResponseUseCase for ProcessAuthorizationResponseService {
async fn process_authorization_response(
&self,
cmd: command::ProcessAuthorizationResponseCommand,
@ -55,7 +51,7 @@ impl ProcessAuthorizationResponseUserCase
if u.host() != self.process_authorization_response_redirect_uri.host()
&& u.path() != self.process_authorization_response_redirect_uri.path()
{
return Err(ProcessAuthorizationServiceError::BadRequest);
return Err(ProcessAuthorizationServiceError::InteralError);
}
}
@ -88,7 +84,7 @@ impl ProcessAuthorizationResponseUserCase
.await?;
self.save_oauth_access_token_adapter
.save_oauth_access_token(&username, &cmd.oauth_provider(), &access_token)
.save_oauth_access_token(&username, cmd.oauth_provider(), &access_token)
.await?;
Ok(())
@ -128,7 +124,7 @@ mod tests {
)
.unwrap();
let s = ProcessAuthorizationResponseProcessAuthorizationService::new(
let s = ProcessAuthorizationResponseService::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),

View file

@ -71,8 +71,6 @@ impl RequestAuthorizationUserCase for RequestAuthorizationService {
#[cfg(test)]
mod tests {
use std::sync::Arc;
use crate::utils::random_string::tests::*;
use crate::{
auth::application::{

36
src/auth/domain/mod.rs Normal file
View file

@ -0,0 +1,36 @@
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use serde::{Deserialize, Serialize};
#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct OAuthAccessToken {
//pub username: String,
pub refresh_token: String,
pub access_token: String,
pub token_type: String,
pub expires_at: Duration, // unix epoch of the instant
pub expires_in: u64,
}
impl OAuthAccessToken {
pub fn new(
// username: String,
refresh_token: String,
access_token: String,
token_type: String,
expires_in: u64,
) -> Self {
let mut expires_at = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
expires_at = expires_at
.checked_add(Duration::from_secs(expires_in))
.unwrap();
Self {
// username,
refresh_token,
access_token,
token_type,
expires_in,
expires_at,
}
}
}

3
src/auth/mod.rs Normal file
View file

@ -0,0 +1,3 @@
pub mod adapter;
mod application;
mod domain;

View file

@ -1,2 +1,4 @@
pub const IS_CALLED_ONLY_ONCE: Option<usize> = Some(1);
pub const IS_CALLED_ONLY_TWICE: Option<usize> = Some(2);
pub const RETURNS_TRUE: bool = true;