Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
769f5a3
30-access-policies app domain
smart--petea Feb 5, 2024
4d39ea5
30-access-policies sqlx-adapter. new version
smart--petea Feb 10, 2024
c53bcb7
30-access-policies
smart--petea Feb 14, 2024
ff82dbf
30-access-policies replace crate::routes::xxx with routes::xx
smart--petea Feb 17, 2024
7e575c6
30-access-policies authorization manager
smart--petea Feb 17, 2024
eae0116
30-access-polices - Authorization/Authentication manager
smart--petea Feb 17, 2024
e4ad08a
30-access-policies get_header
smart--petea Feb 17, 2024
be13d2a
30-access-policies app authorization
smart--petea Feb 17, 2024
be1cb81
30-access-policies two authentication managers in one bowl
smart--petea Feb 17, 2024
f0e3df5
30-access-policies authorization.rs
smart--petea Feb 18, 2024
626baec
30-access-policies
smart--petea Feb 18, 2024
1820776
30-access-policies refactor
smart--petea Feb 19, 2024
5330ad9
30-access-policies anonym logic separated
smart--petea Feb 20, 2024
44c28ef
30-access-policies removed http authentication
smart--petea Feb 20, 2024
0e5901d
30-access-policies Bearer token extract logic
smart--petea Feb 20, 2024
35815f6
30-access-policies user_fetch improved
smart--petea Feb 21, 2024
60520ee
30-access-polices merge with dev
smart--petea Feb 28, 2024
046425c
30-access-policies
smart--petea Feb 28, 2024
47eac8f
30-access-policies admin enable client
smart--petea Mar 1, 2024
b9e6284
30-access-policies client admin
smart--petea Mar 2, 2024
de85d4b
30-access-policies removed src/routes/stack/service.rs
smart--petea Mar 2, 2024
618c30e
30-access-policies owner logic for POST /stack/:id
smart--petea Mar 2, 2024
b599b98
30-access-policies admin, GET /stack/:id
smart--petea Mar 2, 2024
15b890b
30-access-policies GET /stack, GET /admin/stack/user/:id
smart--petea Mar 3, 2024
98b5a7d
30-access-policies /stack/:id/deploy
smart--petea Mar 4, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
571 changes: 379 additions & 192 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ thiserror = "1.0"
serde_valid = "0.18.0"
serde_json = { version = "1.0.111", features = [] }
serde_derive = "1.0.195"
actix-web-httpauth = "0.8.1"
actix-cors = "0.6.4"
tracing-actix-web = "0.7.7"
regex = "1.10.2"
Expand All @@ -44,8 +43,7 @@ tokio-stream = "0.1.14"
actix-http = "3.4.0"
hmac = "0.12.1"
sha2 = "0.10.8"
#sqlx-adapter = { version = "0.4.2", default-features = false, features = ["postgres", "runtime-tokio-native-tls"]}
sqlx-adapter = { git="https://github.com/smart--petea/sqlx-adapter.git", branch="dirty-master", default-features = false, features = ["postgres"]}
sqlx-adapter = { version = "1.0.0", default-features = false, features = ["postgres", "runtime-tokio-native-tls"]}

# dctypes
derive_builder = "0.12.0"
Expand Down
2 changes: 1 addition & 1 deletion access_control.conf.dist
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@ g = _, _
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && r.obj == p.obj && regexMatch(r.act, p.act)
m = g(r.sub, p.sub) && keyMatch2(r.obj, p.obj) && r.act == p.act
1 change: 1 addition & 0 deletions src/forms/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ pub mod stack;
pub mod user;

pub use rating::*;
pub use user::UserForm;
4 changes: 3 additions & 1 deletion src/forms/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ pub struct UserForm {
pub user: User,
}

//todo deref for UserForm. userForm.id, userForm.first_name

#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, Validate)]
#[serde(rename_all = "camelCase")]
pub struct User {
Expand Down Expand Up @@ -139,4 +141,4 @@ impl TryInto<UserModel> for UserForm {
})
}

}
}
24 changes: 24 additions & 0 deletions src/middleware/authentication/getheader.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use actix_web::{ http::header::HeaderName, dev::ServiceRequest};
use std::str::FromStr;

pub fn get_header<T>(req: &ServiceRequest, header_name: &'static str) -> Result<Option<T>, String>
where
T: FromStr,
{
let header_value = req
.headers()
.get(HeaderName::from_static(header_name));

if header_value.is_none() {
return Ok(None);
}

header_value
.unwrap()
.to_str()
.map_err(|_| format!("header {header_name} can't be converted to string"))?
.parse::<T>()
.map_err(|_| format!("header {header_name} has wrong type"))
.map(|v| Some(v))
}

37 changes: 37 additions & 0 deletions src/middleware/authentication/manager.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
use crate::middleware::authentication::*;

use std::sync::Arc;
use std::future::{ready, Ready};
use futures::lock::Mutex;

use actix_web::{
Error,
dev::{Service, ServiceRequest, ServiceResponse, Transform},
};

pub struct Manager {}

impl Manager {
pub fn new() -> Self {
Self {}
}
}

impl<S, B> Transform<S, ServiceRequest> for Manager
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = ManagerMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;

fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(ManagerMiddleware {
service: Arc::new(Mutex::new(service)),
}))
}
}
51 changes: 51 additions & 0 deletions src/middleware/authentication/manager_middleware.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
use crate::middleware::authentication::*;
use actix_web::{error::ErrorBadRequest, HttpMessage, Error, dev::{ServiceRequest, ServiceResponse, Service}};
use crate::helpers::JsonResponse;
use futures::{task::{Poll, Context}, future::{FutureExt, LocalBoxFuture}, lock::Mutex};
use crate::models;
use std::sync::Arc;

pub struct ManagerMiddleware<S> {
pub service: Arc<Mutex<S>>,
}

impl<S, B> Service<ServiceRequest> for ManagerMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = S::Error;
type Future = LocalBoxFuture<'static, Result<ServiceResponse<B>, Error>>;

fn poll_ready(&self, ctx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.service
.try_lock()
.expect("Authentication ManagerMiddleware was called allready")
.poll_ready(ctx)
}

fn call(&self, mut req: ServiceRequest) -> Self::Future {
let service = self.service.clone();
async move {
let _ = method::try_oauth(&mut req).await?
|| method::try_hmac(&mut req).await?
|| method::anonym(&mut req)?;

Ok(req)
}
.then(|req: Result<ServiceRequest, String>| async move {
match req {
Ok(req) => {
let service = service.lock().await;
service.call(req).await
}
Err(msg) => Err(ErrorBadRequest(
JsonResponse::<models::Client>::build().set_msg(msg).to_string(),
)),
}
})
.boxed_local()
}
}
15 changes: 15 additions & 0 deletions src/middleware/authentication/method/f_anonym.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
use actix_web::dev::ServiceRequest;
use actix_web::HttpMessage;

#[tracing::instrument(name = "authenticate as anonym")]
pub fn anonym(req: &mut ServiceRequest) -> Result<bool, String> {
let accesscontrol_vals = actix_casbin_auth::CasbinVals {
subject: "anonym".to_string(),
domain: None,
};
if req.extensions_mut().insert(accesscontrol_vals).is_some() {
return Err("sth wrong with access control".to_string());
}

Ok(true)
}
102 changes: 102 additions & 0 deletions src/middleware/authentication/method/f_hmac.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use hmac::{Hmac, Mac};
use sha2::Sha256;
use sqlx::{Pool, Postgres};
use tracing::Instrument;
use std::sync::Arc;
use crate::models;
use actix_web::{web, dev::ServiceRequest, HttpMessage};
use crate::middleware::authentication::get_header; //todo move to helpers
use actix_http::header::CONTENT_LENGTH;
use futures::StreamExt;

async fn db_fetch_client(db_pool: &Pool<Postgres>, client_id: i32) -> Result<models::Client, String> { //todo
let query_span = tracing::info_span!("Fetching the client by ID");

sqlx::query_as!(
models::Client,
r#"SELECT id, user_id, secret FROM client c WHERE c.id = $1"#,
client_id,
)
.fetch_one(db_pool)
.instrument(query_span)
.await
.map_err(|err| {
match err {
sqlx::Error::RowNotFound => "the client is not found".to_string(),
e => {
tracing::error!("Failed to execute fetch query: {:?}", e);
String::new()
}
}
})
}

async fn compute_body_hash(req: &mut ServiceRequest, client_secret: &[u8]) -> Result<String, String> {
let content_length: usize = get_header(req, CONTENT_LENGTH.as_str())?.unwrap();
let mut body = web::BytesMut::with_capacity(content_length);
let mut payload = req.take_payload();
while let Some(chunk) = payload.next().await {
body.extend_from_slice(&chunk.expect("can't unwrap the chunk"));
}

let mut mac =
match Hmac::<Sha256>::new_from_slice(client_secret) {
Ok(mac) => mac,
Err(err) => {
tracing::error!("error generating hmac {err:?}");
return Err("".to_string());
}
};

mac.update(body.as_ref());
let (_, mut payload) = actix_http::h1::Payload::create(true);
payload.unread_data(body.into());
req.set_payload(payload.into());

Ok(format!("{:x}", mac.finalize().into_bytes()))
}

#[tracing::instrument(name = "try authenticate via hmac")]
pub async fn try_hmac(req: &mut ServiceRequest) -> Result<bool, String> {
let client_id = get_header::<i32>(&req, "stacker-id")?;
if client_id.is_none() {
return Ok(false);
}
let client_id = client_id.unwrap();

let header_hash = get_header::<String>(&req, "stacker-hash")?;
if header_hash.is_none() {
return Err("stacker-hash header is not set".to_string());
} //todo
let header_hash = header_hash.unwrap();

let db_pool = req.app_data::<web::Data<Pool<Postgres>>>().unwrap().get_ref();
let client: models::Client = db_fetch_client(db_pool, client_id).await?;
if client.secret.is_none() {
return Err("client is not active".to_string());
}

let client_secret = client.secret.as_ref().unwrap().as_bytes();
let body_hash = compute_body_hash(req, client_secret).await?;
if header_hash != body_hash {
return Err("hash is wrong".to_string());
}

match req.extensions_mut().insert(Arc::new(client)) {
Some(_) => {
tracing::error!("client middleware already called once");
return Err("".to_string());
}
None => {}
}

let accesscontrol_vals = actix_casbin_auth::CasbinVals {
subject: client_id.to_string(),
domain: None,
};
if req.extensions_mut().insert(accesscontrol_vals).is_some() {
return Err("sth wrong with access control".to_string());
}

Ok(true)
}
72 changes: 72 additions & 0 deletions src/middleware/authentication/method/f_oauth.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use crate::middleware::authentication::get_header;
use actix_web::{web, dev::{ServiceRequest}, HttpMessage};
use crate::configuration::Settings;
use crate::models;
use crate::forms;
use reqwest::header::{ACCEPT, CONTENT_TYPE};
use std::sync::Arc;

fn try_extract_token(authentication: String) -> Result<String, String> {
let mut authentication_parts = authentication.splitn(2, ' ');
match authentication_parts.next() {
Some("Bearer") => {}
_ => return Err("Bearer missing scheme".to_string())
}
let token = authentication_parts.next();
if token.is_none() {
return Err("Empty bearer token".to_string());
}

Ok(token.unwrap().into())
}

#[tracing::instrument(name = "try authenticate via bearer")]
pub async fn try_oauth(req: &mut ServiceRequest) -> Result<bool, String> {
let authentication = get_header::<String>(&req, "authorization")?;
if authentication.is_none() {
return Ok(false);
}

let token = try_extract_token(authentication.unwrap())?;
let settings = req.app_data::<web::Data<Settings>>().unwrap();
let user = fetch_user(settings.auth_url.as_str(), &token)
.await
.map_err(|err| format!("{err}"))?;

let accesscontrol_vals = actix_casbin_auth::CasbinVals {
subject: user.id.clone(),
domain: None,
};

if req.extensions_mut().insert(Arc::new(user)).is_some() {
return Err("user already logged".to_string());
}

if req.extensions_mut().insert(accesscontrol_vals).is_some() {
return Err("sth wrong with access control".to_string());
}

Ok(true)
}

async fn fetch_user(auth_url: &str, token: &str) -> Result<models::User, String> {
let client = reqwest::Client::new();
let resp = client
.get(auth_url)
.bearer_auth(token)
.header(CONTENT_TYPE, "application/json")
.header(ACCEPT, "application/json")
.send()
.await
.map_err(|_err| "no resp from auth server".to_string())?;

if !resp.status().is_success() {
return Err("401 Unauthorized".to_string());
}

resp
.json::<forms::UserForm>()
.await
.map_err(|_err| "can't parse the response body".to_string())?
.try_into()
}
7 changes: 7 additions & 0 deletions src/middleware/authentication/method/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
mod f_oauth;
mod f_anonym;
mod f_hmac;

pub use f_oauth::try_oauth;
pub use f_anonym::anonym;
pub use f_hmac::try_hmac;
8 changes: 8 additions & 0 deletions src/middleware/authentication/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
mod getheader;
mod manager;
mod manager_middleware;
mod method;

pub use getheader::*;
pub use manager::*;
pub use manager_middleware::*;
File renamed without changes.
Loading