From 063f47e8fca976d127d3069fb278bc2111d8d8cc Mon Sep 17 00:00:00 2001 From: Ruben Nijveld Date: Tue, 20 Aug 2024 14:02:13 +0200 Subject: [PATCH 1/2] Allow user with multiple roles, add UserManager role --- openadr-vtn/src/api/auth.rs | 2 +- openadr-vtn/src/api/event.rs | 14 ++- openadr-vtn/src/api/program.rs | 14 ++- openadr-vtn/src/api/report.rs | 4 +- openadr-vtn/src/data_source/mod.rs | 3 +- openadr-vtn/src/jwt.rs | 146 +++++++++++++++++++++++------ openadr-vtn/src/main.rs | 3 +- 7 files changed, 134 insertions(+), 52 deletions(-) diff --git a/openadr-vtn/src/api/auth.rs b/openadr-vtn/src/api/auth.rs index c0a212e..5758ec5 100644 --- a/openadr-vtn/src/api/auth.rs +++ b/openadr-vtn/src/api/auth.rs @@ -127,7 +127,7 @@ pub async fn token( }; let expiration = std::time::Duration::from_secs(3600 * 24 * 30); - let token = jwt_manager.create(expiration, user.client_id, user.role, user.ven)?; + let token = jwt_manager.create(expiration, user.client_id, user.roles)?; Ok(AccessTokenResponse { access_token: token, diff --git a/openadr-vtn/src/api/event.rs b/openadr-vtn/src/api/event.rs index 6b4eb3b..b710217 100644 --- a/openadr-vtn/src/api/event.rs +++ b/openadr-vtn/src/api/event.rs @@ -20,7 +20,7 @@ use crate::api::{AppResponse, ValidatedQuery}; use crate::data_source::{Crud, EventCrud}; use crate::error::AppError; use crate::error::AppError::NotImplemented; -use crate::jwt::{BLUser, User}; +use crate::jwt::{BusinessUser, User}; impl EventCrud for RwLock> {} @@ -105,7 +105,7 @@ pub async fn get( pub async fn add( State(event_source): State>, - BLUser(_user): BLUser, + BusinessUser(_user): BusinessUser, Json(new_event): Json, ) -> Result<(StatusCode, Json), AppError> { let event = event_source.create(new_event).await?; @@ -118,7 +118,7 @@ pub async fn add( pub async fn edit( State(event_source): State>, Path(id): Path, - BLUser(_user): BLUser, + BusinessUser(_user): BusinessUser, Json(content): Json, ) -> AppResponse { let event = event_source.update(&id, content).await?; @@ -131,7 +131,7 @@ pub async fn edit( pub async fn delete( State(event_source): State>, Path(id): Path, - BLUser(_user): BLUser, + BusinessUser(_user): BusinessUser, ) -> AppResponse { let event = event_source.delete(&id).await?; info!(%id, "deleted event"); @@ -219,8 +219,7 @@ mod test { store.auth.try_write().unwrap().push(AuthInfo { client_id: "admin".to_string(), client_secret: "admin".to_string(), - role: AuthRole::BL, - ven: None, + roles: vec![AuthRole::Business(None), AuthRole::UserManager], }); { @@ -239,8 +238,7 @@ mod test { .create( std::time::Duration::from_secs(3600), "admin".to_string(), - AuthRole::BL, - None, + vec![AuthRole::Business(None), AuthRole::UserManager], ) .unwrap() } diff --git a/openadr-vtn/src/api/program.rs b/openadr-vtn/src/api/program.rs index 6ccd36b..071db1c 100644 --- a/openadr-vtn/src/api/program.rs +++ b/openadr-vtn/src/api/program.rs @@ -17,7 +17,7 @@ use openadr_wire::Program; use crate::api::{AppResponse, ValidatedQuery}; use crate::data_source::{Crud, ProgramCrud}; use crate::error::AppError; -use crate::jwt::{BLUser, User}; +use crate::jwt::{BusinessUser, User}; impl ProgramCrud for RwLock> {} @@ -132,7 +132,7 @@ pub async fn get( pub async fn add( State(program_source): State>, - BLUser(_user): BLUser, + BusinessUser(_user): BusinessUser, Json(new_program): Json, ) -> Result<(StatusCode, Json), AppError> { let program = program_source.create(new_program).await?; @@ -143,7 +143,7 @@ pub async fn add( pub async fn edit( State(program_source): State>, Path(id): Path, - BLUser(_user): BLUser, + BusinessUser(_user): BusinessUser, Json(content): Json, ) -> AppResponse { let program = program_source.update(&id, content).await?; @@ -156,7 +156,7 @@ pub async fn edit( pub async fn delete( State(program_source): State>, Path(id): Path, - BLUser(_user): BLUser, + BusinessUser(_user): BusinessUser, ) -> AppResponse { let program = program_source.delete(&id).await?; info!(%id, "deleted program"); @@ -254,8 +254,7 @@ mod test { store.auth.try_write().unwrap().push(AuthInfo { client_id: "admin".to_string(), client_secret: "admin".to_string(), - role: AuthRole::BL, - ven: None, + roles: vec![AuthRole::Business(None), AuthRole::UserManager], }); { @@ -274,8 +273,7 @@ mod test { .create( std::time::Duration::from_secs(3600), "admin".to_string(), - AuthRole::BL, - None, + vec![AuthRole::Business(None), AuthRole::UserManager], ) .unwrap() } diff --git a/openadr-vtn/src/api/report.rs b/openadr-vtn/src/api/report.rs index e69af1a..ba99e74 100644 --- a/openadr-vtn/src/api/report.rs +++ b/openadr-vtn/src/api/report.rs @@ -19,7 +19,7 @@ use openadr_wire::Report; use crate::api::{AppResponse, ValidatedQuery}; use crate::data_source::{Crud, ReportCrud}; use crate::error::AppError; -use crate::jwt::{BLUser, User}; +use crate::jwt::{BusinessUser, User}; impl ReportCrud for RwLock> {} @@ -136,7 +136,7 @@ pub async fn edit( pub async fn delete( State(report_source): State>, - BLUser(_user): BLUser, + BusinessUser(_user): BusinessUser, Path(id): Path, ) -> AppResponse { let report = report_source.delete(&id).await?; diff --git a/openadr-vtn/src/data_source/mod.rs b/openadr-vtn/src/data_source/mod.rs index fdbb794..545b582 100644 --- a/openadr-vtn/src/data_source/mod.rs +++ b/openadr-vtn/src/data_source/mod.rs @@ -80,8 +80,7 @@ pub trait DataSource: Send + Sync + 'static { pub struct AuthInfo { pub client_id: String, pub client_secret: String, - pub role: AuthRole, - pub ven: Option, + pub roles: Vec, } #[derive(Default, Clone)] diff --git a/openadr-vtn/src/jwt.rs b/openadr-vtn/src/jwt.rs index 1c89fca..5c25686 100644 --- a/openadr-vtn/src/jwt.rs +++ b/openadr-vtn/src/jwt.rs @@ -19,11 +19,26 @@ pub struct JwtManager { decoding_key: DecodingKey, } -#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] -#[allow(clippy::upper_case_acronyms)] +#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(tag = "role", content = "id")] pub enum AuthRole { - BL, - VEN, + UserManager, + Business(Option), + VEN(String), +} + +impl AuthRole { + pub fn is_business(&self) -> bool { + matches!(self, AuthRole::Business(_)) + } + + pub fn is_ven(&self) -> bool { + matches!(self, AuthRole::VEN(_)) + } + + pub fn is_user_manager(&self) -> bool { + matches!(self, AuthRole::UserManager) + } } #[derive(Debug, serde::Serialize, serde::Deserialize)] @@ -31,8 +46,56 @@ pub struct Claims { exp: usize, nbf: usize, pub sub: String, - pub role: AuthRole, - pub ven: Option, + pub roles: Vec, +} + +impl Claims { + pub fn ven_ids(&self) -> Vec { + self.roles + .iter() + .filter_map(|role| { + if let AuthRole::VEN(id) = role { + Some(id.clone()) + } else { + None + } + }) + .collect() + } + + pub fn business_ids(&self) -> (Vec, bool) { + let mut allow_any = false; + let ids = self + .roles + .iter() + .filter_map(|role| { + if let AuthRole::Business(id) = role { + if let Some(id) = id { + Some(id.clone()) + } else { + allow_any = true; + None + } + } else { + None + } + }) + .collect(); + + (ids, allow_any) + } + + pub fn is_ven(&self) -> bool { + self.roles.iter().any(AuthRole::is_ven) + } + + pub fn is_business(&self) -> bool { + self.roles.iter().any(AuthRole::is_business) + } + + pub fn is_user_manager(&self) -> bool { + self.roles.iter().any(AuthRole::is_user_manager) + } } impl JwtManager { @@ -63,8 +126,7 @@ impl JwtManager { &self, expires_in: std::time::Duration, client_id: String, - role: AuthRole, - ven: Option, + roles: Vec, ) -> Result { let now = chrono::Utc::now(); let exp = now + expires_in; @@ -73,8 +135,7 @@ impl JwtManager { exp: exp.timestamp() as usize, nbf: now.timestamp() as usize, sub: client_id, - role, - ven, + roles, }; let token = encode(&Header::default(), &claims, &self.encoding_key)?; @@ -90,9 +151,17 @@ impl JwtManager { } } +/// User claims extracted from the request pub struct User(pub Claims); -pub struct BLUser(pub Claims); -// pub struct VENUser(pub Claims); + +/// User claims extracted from the request, with the requirement that the user is a business user +pub struct BusinessUser(pub Claims); + +/// User claims extracted from the request, with the requirement that the user is a VEN user +pub struct VENUser(pub Claims); + +/// User claims extracted from the request, with the requirement that the user is a user manager +pub struct UserManagerUser(pub Claims); #[async_trait] impl FromRequestParts for User @@ -125,36 +194,55 @@ where } #[async_trait] -impl FromRequestParts for BLUser +impl FromRequestParts for BusinessUser where Arc: FromRef, { type Rejection = AppError; async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { - let user = User::from_request_parts(parts, state).await?; - - if user.0.role != AuthRole::BL { + let User(user) = User::from_request_parts(parts, state).await?; + if !user.is_business() { return Err(AppError::Auth( "User does not have the required role".to_string(), )); } - - Ok(BLUser(user.0)) + Ok(BusinessUser(user)) } } -// #[async_trait] -// impl FromRequestParts for VENUser where Arc: FromRef { -// type Rejection = AppError; +#[async_trait] +impl FromRequestParts for VENUser +where + Arc: FromRef, +{ + type Rejection = AppError; -// async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { -// let user = User::from_request_parts(parts, state).await?; + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let User(user) = User::from_request_parts(parts, state).await?; + if !user.is_ven() { + return Err(AppError::Auth( + "User does not have the required role".to_string(), + )); + } + Ok(VENUser(user)) + } +} -// if user.0.role != AuthRole::VEN { -// return Err(AppError::Auth("User does not have the required role".to_string())); -// } +#[async_trait] +impl FromRequestParts for UserManagerUser +where + Arc: FromRef, +{ + type Rejection = AppError; -// Ok(VENUser(user.0)) -// } -// } + async fn from_request_parts(parts: &mut Parts, state: &S) -> Result { + let User(user) = User::from_request_parts(parts, state).await?; + if !user.is_user_manager() { + return Err(AppError::Auth( + "User does not have the required role".to_string(), + )); + } + Ok(UserManagerUser(user)) + } +} diff --git a/openadr-vtn/src/main.rs b/openadr-vtn/src/main.rs index 5caa844..48fbf1b 100644 --- a/openadr-vtn/src/main.rs +++ b/openadr-vtn/src/main.rs @@ -24,8 +24,7 @@ async fn main() { storage.auth.write().await.push(AuthInfo { client_id: "admin".to_string(), client_secret: "admin".to_string(), - role: AuthRole::BL, - ven: None, + roles: vec![AuthRole::Business(None), AuthRole::UserManager], }); let state = AppState::new(storage, JwtManager::from_base64_secret("test").unwrap()); From b1d9618a4a480236fe8ccaab8d855d08a0e6c07f Mon Sep 17 00:00:00 2001 From: Ruben Nijveld Date: Tue, 20 Aug 2024 14:49:38 +0200 Subject: [PATCH 2/2] Separate out AnyBusiness role --- openadr-vtn/src/api/event.rs | 4 ++-- openadr-vtn/src/api/program.rs | 4 ++-- openadr-vtn/src/jwt.rs | 40 ++++++++++++++++------------------ openadr-vtn/src/main.rs | 2 +- 4 files changed, 24 insertions(+), 26 deletions(-) diff --git a/openadr-vtn/src/api/event.rs b/openadr-vtn/src/api/event.rs index b710217..e8ea009 100644 --- a/openadr-vtn/src/api/event.rs +++ b/openadr-vtn/src/api/event.rs @@ -219,7 +219,7 @@ mod test { store.auth.try_write().unwrap().push(AuthInfo { client_id: "admin".to_string(), client_secret: "admin".to_string(), - roles: vec![AuthRole::Business(None), AuthRole::UserManager], + roles: vec![AuthRole::AnyBusiness, AuthRole::UserManager], }); { @@ -238,7 +238,7 @@ mod test { .create( std::time::Duration::from_secs(3600), "admin".to_string(), - vec![AuthRole::Business(None), AuthRole::UserManager], + vec![AuthRole::AnyBusiness, AuthRole::UserManager], ) .unwrap() } diff --git a/openadr-vtn/src/api/program.rs b/openadr-vtn/src/api/program.rs index 071db1c..65f8507 100644 --- a/openadr-vtn/src/api/program.rs +++ b/openadr-vtn/src/api/program.rs @@ -254,7 +254,7 @@ mod test { store.auth.try_write().unwrap().push(AuthInfo { client_id: "admin".to_string(), client_secret: "admin".to_string(), - roles: vec![AuthRole::Business(None), AuthRole::UserManager], + roles: vec![AuthRole::AnyBusiness, AuthRole::UserManager], }); { @@ -273,7 +273,7 @@ mod test { .create( std::time::Duration::from_secs(3600), "admin".to_string(), - vec![AuthRole::Business(None), AuthRole::UserManager], + vec![AuthRole::AnyBusiness, AuthRole::UserManager], ) .unwrap() } diff --git a/openadr-vtn/src/jwt.rs b/openadr-vtn/src/jwt.rs index 5c25686..b0d5cb6 100644 --- a/openadr-vtn/src/jwt.rs +++ b/openadr-vtn/src/jwt.rs @@ -23,13 +23,14 @@ pub struct JwtManager { #[serde(tag = "role", content = "id")] pub enum AuthRole { UserManager, - Business(Option), + Business(String), + AnyBusiness, VEN(String), } impl AuthRole { pub fn is_business(&self) -> bool { - matches!(self, AuthRole::Business(_)) + matches!(self, AuthRole::Business(_) | AuthRole::AnyBusiness) } pub fn is_ven(&self) -> bool { @@ -49,6 +50,11 @@ pub struct Claims { pub roles: Vec, } +pub enum BusinessIds { + Specific(Vec), + Any, +} + impl Claims { pub fn ven_ids(&self) -> Vec { self.roles @@ -63,26 +69,18 @@ impl Claims { .collect() } - pub fn business_ids(&self) -> (Vec, bool) { - let mut allow_any = false; - let ids = self - .roles - .iter() - .filter_map(|role| { - if let AuthRole::Business(id) = role { - if let Some(id) = id { - Some(id.clone()) - } else { - allow_any = true; - None - } - } else { - None - } - }) - .collect(); + pub fn business_ids(&self) -> BusinessIds { + let mut ids = vec![]; + + for role in &self.roles { + match role { + AuthRole::Business(id) => ids.push(id.clone()), + AuthRole::AnyBusiness => return BusinessIds::Any, + _ => {} + } + } - (ids, allow_any) + BusinessIds::Specific(ids) } pub fn is_ven(&self) -> bool { diff --git a/openadr-vtn/src/main.rs b/openadr-vtn/src/main.rs index 48fbf1b..b07ef4f 100644 --- a/openadr-vtn/src/main.rs +++ b/openadr-vtn/src/main.rs @@ -24,7 +24,7 @@ async fn main() { storage.auth.write().await.push(AuthInfo { client_id: "admin".to_string(), client_secret: "admin".to_string(), - roles: vec![AuthRole::Business(None), AuthRole::UserManager], + roles: vec![AuthRole::AnyBusiness, AuthRole::UserManager], }); let state = AppState::new(storage, JwtManager::from_base64_secret("test").unwrap());