diff --git a/src/resources/access_policy/mod.rs b/src/resources/access_policy/mod.rs index 8e7c9f3..a750aa8 100644 --- a/src/resources/access_policy/mod.rs +++ b/src/resources/access_policy/mod.rs @@ -72,7 +72,7 @@ pub const UUID_QUERY_KEYS: &[&str] = &[ pub const DEFAULT_ACCESS_POLICY_LIST_LIMIT: i64 = 1000; -#[derive(Debug, PartialEq, Eq, ToSql, FromSql, Clone, Copy, Serialize, Deserialize, Default)] +#[derive(Debug, PartialEq, Eq, ToSql, FromSql, Clone, Copy, Serialize, Deserialize, Default, PartialOrd)] #[postgres(name = "permission_level")] pub enum AccessPolicyPermissionLevel { #[default] @@ -311,6 +311,19 @@ impl FromStr for AccessPolicyPrincipalType { } +#[derive(Debug, Clone)] +pub enum Principal { + + User(Uuid), + + Group(Uuid), + + Role(Uuid), + + App(Uuid) + +} + #[derive(Debug, Default)] pub struct InitialAccessPolicyProperties { @@ -354,6 +367,8 @@ pub struct InitialAccessPolicyProperties { } +#[derive(Debug, Deserialize, Clone)] +#[serde(deny_unknown_fields)] pub struct EditableAccessPolicyProperties { permission_level: Option, @@ -680,7 +695,7 @@ impl AccessPolicy { } /// Returns a list of access policies based on a hierarchy. - pub async fn list_by_hierarchy(resource_hierarchy: &ResourceHierarchy, action_id: &Uuid, postgres_client: &mut deadpool_postgres::Client) -> Result, AccessPolicyError> { + pub async fn list_by_hierarchy(principal: &Principal, action_id: &Uuid, resource_hierarchy: &ResourceHierarchy, postgres_client: &mut deadpool_postgres::Client) -> Result, AccessPolicyError> { let mut query_clauses: Vec = Vec::new(); @@ -706,8 +721,16 @@ impl AccessPolicy { // This will turn the query into something like: // action_id = $1 and (scoped_resource_type = 'Instance' or scoped_workspace_id = $2 or scoped_project_id = $3 or scoped_milestone_id = $4 or scoped_item_id = $5) + let principal_clause = match principal { + + Principal::User(user_id) => format!("principal_user_id = '{}'", user_id), + Principal::Group(group_id) => format!("principal_group_id = '{}'", group_id), + Principal::Role(role_id) => format!("principal_role_id = '{}'", role_id), + Principal::App(app_id) => format!("principal_app_id = '{}'", app_id) + + }; let mut query_filter = String::new(); - query_filter.push_str(format!("action_id = {} and (", quote_literal(&action_id.to_string())).as_str()); + query_filter.push_str(format!("{} and action_id = {} and (", principal_clause, quote_literal(&action_id.to_string())).as_str()); for i in 0..query_clauses.len() { if i > 0 { @@ -737,7 +760,7 @@ impl AccessPolicy { } - fn add_parameter(mut parameter_boxes: Vec>, mut query: String, key: &str, parameter_value: &Option) -> (Vec>, String) { + fn add_parameter(mut parameter_boxes: Vec>, mut query: String, key: &str, parameter_value: &Option) -> (Vec>, String) { if let Some(parameter_value) = parameter_value.clone() { @@ -754,7 +777,7 @@ impl AccessPolicy { pub async fn update(&self, properties: &EditableAccessPolicyProperties, postgres_client: &mut deadpool_postgres::Client) -> Result { let query = String::from("update access_policies set "); - let parameter_boxes: Vec> = Vec::new(); + let parameter_boxes: Vec> = Vec::new(); postgres_client.query("begin;", &[]).await?; let (parameter_boxes, query) = Self::add_parameter(parameter_boxes, query, "permission_level", &properties.permission_level); @@ -762,7 +785,7 @@ impl AccessPolicy { query.push_str(format!(" where id = ${} returning *;", parameter_boxes.len() + 1).as_str()); parameter_boxes.push(Box::new(&self.id)); - let parameters = parameter_boxes.iter().map(|parameter| parameter.as_ref()).collect::>(); + let parameters: Vec<&(dyn ToSql + Sync)> = parameter_boxes.iter().map(|parameter| parameter.as_ref() as &(dyn ToSql + Sync)).collect(); let row = postgres_client.query_one(&query, ¶meters).await?; postgres_client.query("commit;", &[]).await?; diff --git a/src/resources/access_policy/tests.rs b/src/resources/access_policy/tests.rs index ec109f4..ee5cca5 100644 --- a/src/resources/access_policy/tests.rs +++ b/src/resources/access_policy/tests.rs @@ -18,7 +18,7 @@ use crate::{ AccessPolicyScopedResourceType, DEFAULT_ACCESS_POLICY_LIST_LIMIT, EditableAccessPolicyProperties, - InitialAccessPolicyProperties + InitialAccessPolicyProperties, Principal }, tests::TestEnvironment }; use anyhow::{anyhow, Result}; @@ -299,7 +299,7 @@ async fn list_access_policies_by_hierarchy() -> Result<()> { let instance_access_policy = AccessPolicy::create(&instance_access_policy_properties, &mut postgres_client).await?; let access_policy_hierarchy = instance_access_policy.get_hierarchy(&mut postgres_client).await?; - let retrieved_access_policies = AccessPolicy::list_by_hierarchy(&access_policy_hierarchy, &action.id, &mut postgres_client).await?; + let retrieved_access_policies = AccessPolicy::list_by_hierarchy(&Principal::User(user.id), &action.id, &access_policy_hierarchy, &mut postgres_client).await?; assert_eq!(retrieved_access_policies.len(), access_policy_hierarchy.len()); diff --git a/src/routes/access-policies/{access_policy_id}/mod.rs b/src/routes/access-policies/{access_policy_id}/mod.rs index 27a1074..93396a2 100644 --- a/src/routes/access-policies/{access_policy_id}/mod.rs +++ b/src/routes/access-policies/{access_policy_id}/mod.rs @@ -1,11 +1,11 @@ use std::sync::Arc; -use axum::{Extension, Json, Router, extract::{Path, State}}; +use axum::{Extension, Json, Router, extract::{Path, State, rejection::JsonRejection}}; use reqwest::StatusCode; use uuid::Uuid; use colored::Colorize; -use crate::{AppState, HTTPError, middleware::authentication_middleware, resources::{access_policy::{AccessPolicy, AccessPolicyError, AccessPolicyPermissionLevel}, action::Action, http_transaction::HTTPTransaction, server_log_entry::ServerLogEntry, user::User}, utilities::principal_permission_verifier::{PrincipalPermissionVerifier, PrincipalPermissionVerifierError}}; +use crate::{AppState, HTTPError, middleware::authentication_middleware, resources::{access_policy::{AccessPolicy, AccessPolicyError, AccessPolicyPermissionLevel, EditableAccessPolicyProperties, Principal, ResourceHierarchy}, action::Action, http_transaction::HTTPTransaction, server_log_entry::ServerLogEntry, user::User}, utilities::principal_permission_verifier::{PrincipalPermissionVerifier, PrincipalPermissionVerifierError}}; fn map_postgres_error_to_http_error(error: deadpool_postgres::PoolError) -> HTTPError { @@ -15,6 +15,58 @@ fn map_postgres_error_to_http_error(error: deadpool_postgres::PoolError) -> HTTP } +async fn get_user_from_option_user(user: &Option>, http_transaction: &HTTPTransaction, mut postgres_client: &mut deadpool_postgres::Client) -> Result, HTTPError> { + + let Some(user) = user else { + + let http_error = HTTPError::InternalServerError(Some(format!("Couldn't find a user for the request. This is a bug. Make sure the authentication middleware is installed and is working properly."))); + let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; + return Err(http_error); + + }; + + return Ok(user.clone()); + +} + +async fn get_resource_hierarchy(access_policy: &AccessPolicy, http_transaction: &HTTPTransaction, mut postgres_client: &mut deadpool_postgres::Client) -> Result { + + let _ = ServerLogEntry::trace(&format!("Getting resource hierarchy for access policy {}...", access_policy.id), Some(&http_transaction.id), &mut postgres_client).await; + let resource_hierarchy = match access_policy.get_hierarchy(&mut postgres_client).await { + + Ok(resource_hierarchy) => resource_hierarchy, + + Err(error) => { + + let http_error = match error { + AccessPolicyError::ScopedResourceIDMissingError(scoped_resource_type) => { + + let _ = ServerLogEntry::trace(&format!("Deleting orphaned access policy {}...", access_policy.id), Some(&http_transaction.id), &mut postgres_client).await; + let http_error = match access_policy.delete(&mut postgres_client).await { + + Ok(_) => HTTPError::GoneError(Some(format!("The {} resource has been deleted because it was orphaned.", scoped_resource_type))), + + Err(error) => HTTPError::InternalServerError(Some(format!("Failed to delete orphaned access policy: {:?}", error))) + + }; + + let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; + return Err(http_error); + + }, + _ => HTTPError::InternalServerError(Some(error.to_string())) + }; + let _ = ServerLogEntry::from_http_error(&http_error, Some(&http_transaction.id), &mut postgres_client).await; + return Err(http_error); + + } + + }; + + return Ok(resource_hierarchy); + +} + async fn get_access_policy(access_policy_id: &str, http_transaction: &HTTPTransaction, mut postgres_client: &mut deadpool_postgres::Client) -> Result { let access_policy_id = match Uuid::parse_str(&access_policy_id) { @@ -55,67 +107,37 @@ async fn get_access_policy(access_policy_id: &str, http_transaction: &HTTPTransa } -#[axum::debug_handler] -async fn handle_get_access_policy_request( - Path(access_policy_id): Path, - State(state): State, - Extension(http_transaction): Extension>, - Extension(user): Extension>> -) -> Result, HTTPError> { - - let http_transaction = http_transaction.clone(); - let mut postgres_client = state.database_pool.get().await.map_err(map_postgres_error_to_http_error)?; - let access_policy = get_access_policy(&access_policy_id, &http_transaction, &mut postgres_client).await?; - - // Verify the principal has permission to get the access policy. - let Some(user) = user else { +async fn get_action_from_name(action_name: &str, http_transaction: &HTTPTransaction, mut postgres_client: &mut deadpool_postgres::Client) -> Result { - let http_error = HTTPError::InternalServerError(Some(format!("Couldn't find a user for the request. This is a bug. Make sure the authentication middleware is installed and is working properly."))); - let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; - return Err(http_error); + let _ = ServerLogEntry::trace(&format!("Getting action \"{}\"...", action_name), Some(&http_transaction.id), &mut postgres_client).await; + let action = match Action::get_by_name(&action_name, &mut postgres_client).await { - }; - - let _ = ServerLogEntry::trace(&format!("Getting resource hierarchy for access policy {}...", access_policy_id), Some(&http_transaction.id), &mut postgres_client).await; - let resource_hierarchy = match access_policy.get_hierarchy(&mut postgres_client).await { - - Ok(resource_hierarchy) => resource_hierarchy, + Ok(action) => action, Err(error) => { - let http_error = match error { - AccessPolicyError::ScopedResourceIDMissingError(scoped_resource_type) => { - - let _ = ServerLogEntry::trace(&format!("Deleting orphaned access policy {}...", access_policy_id), Some(&http_transaction.id), &mut postgres_client).await; - let http_error = match access_policy.delete(&mut postgres_client).await { - - Ok(_) => HTTPError::GoneError(Some(format!("The {} resource has been deleted because it was orphaned.", scoped_resource_type))), - - Err(error) => HTTPError::InternalServerError(Some(format!("Failed to delete orphaned access policy: {:?}", error))) - - }; - - let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; - return Err(http_error); - - }, - _ => HTTPError::InternalServerError(Some(error.to_string())) - }; - let _ = ServerLogEntry::from_http_error(&http_error, Some(&http_transaction.id), &mut postgres_client).await; + let http_error = HTTPError::InternalServerError(Some(format!("Failed to get action \"{}\": {:?}", action_name, error))); + let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; return Err(http_error); } }; - let _ = ServerLogEntry::trace(&format!("Getting action \"slashstep.accessPolicies.get\"..."), Some(&http_transaction.id), &mut postgres_client).await; - let action = match Action::get_by_name("slashstep.accessPolicies.get", &mut postgres_client).await { + return Ok(action); + +} + +async fn get_action_from_id(action_id: &Uuid, http_transaction: &HTTPTransaction, mut postgres_client: &mut deadpool_postgres::Client) -> Result { + + let _ = ServerLogEntry::trace(&format!("Getting action {}", action_id), Some(&http_transaction.id), postgres_client).await; + let action = match Action::get_by_id(action_id, postgres_client).await { Ok(action) => action, Err(error) => { - let http_error = HTTPError::InternalServerError(Some(format!("Failed to get action \"slashstep.accessPolicies.get\": {:?}", error))); + let http_error = HTTPError::InternalServerError(Some(format!("Failed to get action {}: {:?}", action_id, error))); let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; return Err(http_error); @@ -123,29 +145,39 @@ async fn handle_get_access_policy_request( }; - let _ = ServerLogEntry::trace(&format!("Verifying principal's permissions to get access policy {}...", access_policy_id), Some(&http_transaction.id), &mut postgres_client).await; + return Ok(action); + +} + +async fn verify_user_permissions(user: &User, action: &Action, resource_hierarchy: &ResourceHierarchy, http_transaction: &HTTPTransaction, minimum_permission_level: &AccessPolicyPermissionLevel, mut postgres_client: &mut deadpool_postgres::Client) -> Result<(), HTTPError> { + + let _ = ServerLogEntry::trace(&format!("Verifying principal may use \"{}\" action...", action.name), Some(&http_transaction.id), &mut postgres_client).await; - match PrincipalPermissionVerifier::verify_user_permissions(&user.id, &action.id, &resource_hierarchy, &AccessPolicyPermissionLevel::User, &mut postgres_client).await { + match PrincipalPermissionVerifier::verify_permissions(&Principal::User(user.id), &action.id, &resource_hierarchy, &minimum_permission_level, &mut postgres_client).await { Ok(_) => {}, Err(error) => { let http_error = match error { + PrincipalPermissionVerifierError::ForbiddenError { .. } => { + let message = format!("You need at least {} permission to the \"{}\" action.", minimum_permission_level.to_string(), action.name); if user.is_anonymous { - HTTPError::UnauthorizedError(Some("You need at least user-level permission to the \"slashstep.accessPolicies.get\" action.".to_string())) + HTTPError::UnauthorizedError(Some(message)) } else { - HTTPError::ForbiddenError(Some("You need at least user-level permission to the \"slashstep.accessPolicies.get\" action.".to_string())) + HTTPError::ForbiddenError(Some(message)) } }, + _ => HTTPError::InternalServerError(Some(error.to_string())) + }; let _ = ServerLogEntry::from_http_error(&http_error, Some(&http_transaction.id), &mut postgres_client).await; return Err(http_error); @@ -154,78 +186,90 @@ async fn handle_get_access_policy_request( } - let _ = ServerLogEntry::success(&format!("Successfully returned access policy {}.", access_policy_id), Some(&http_transaction.id), &mut postgres_client).await; - - return Ok(Json(access_policy)); + return Ok(()); } #[axum::debug_handler] -async fn patch_access_policy() { - - -} - -async fn delete_access_policy( +async fn handle_get_access_policy_request( Path(access_policy_id): Path, State(state): State, Extension(http_transaction): Extension>, Extension(user): Extension>> -) -> Result { +) -> Result, HTTPError> { let http_transaction = http_transaction.clone(); let mut postgres_client = state.database_pool.get().await.map_err(map_postgres_error_to_http_error)?; let access_policy = get_access_policy(&access_policy_id, &http_transaction, &mut postgres_client).await?; + let user = get_user_from_option_user(&user, &http_transaction, &mut postgres_client).await?; + let resource_hierarchy = get_resource_hierarchy(&access_policy, &http_transaction, &mut postgres_client).await?; + let action = get_action_from_name("slashstep.accessPolicies.get", &http_transaction, &mut postgres_client).await?; + verify_user_permissions(&user, &action, &resource_hierarchy, &http_transaction, &AccessPolicyPermissionLevel::User, &mut postgres_client).await?; + + let _ = ServerLogEntry::success(&format!("Successfully returned access policy {}.", access_policy_id), Some(&http_transaction.id), &mut postgres_client).await; - // Verify the principal has permission to get the access policy. - let Some(user) = user else { + return Ok(Json(access_policy)); - let http_error = HTTPError::InternalServerError(Some(format!("Couldn't find a user for the request. This is a bug. Make sure the authentication middleware is installed and is working properly."))); - let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; - return Err(http_error); +} - }; - let _ = ServerLogEntry::trace(&format!("Getting resource hierarchy for access policy {}...", access_policy_id), Some(&http_transaction.id), &mut postgres_client).await; - let resource_hierarchy = match access_policy.get_hierarchy(&mut postgres_client).await { +#[axum::debug_handler] +async fn handle_patch_access_policy_request( + Path(access_policy_id): Path, + State(state): State, + Extension(http_transaction): Extension>, + Extension(user): Extension>>, + body: Result, JsonRejection> +) -> Result, HTTPError> { - Ok(resource_hierarchy) => resource_hierarchy, + let http_transaction = http_transaction.clone(); + let mut postgres_client = state.database_pool.get().await.map_err(map_postgres_error_to_http_error)?; + + let _ = ServerLogEntry::trace("Verifying request body...", Some(&http_transaction.id), &mut postgres_client).await; + let updated_access_policy_properties = match body { + + Ok(updated_access_policy_properties) => updated_access_policy_properties, Err(error) => { let http_error = match error { - AccessPolicyError::ScopedResourceIDMissingError(scoped_resource_type) => { - let _ = ServerLogEntry::trace(&format!("Deleting orphaned access policy {}...", access_policy_id), Some(&http_transaction.id), &mut postgres_client).await; - let http_error = match access_policy.delete(&mut postgres_client).await { + JsonRejection::JsonDataError(error) => HTTPError::BadRequestError(Some(error.to_string())), - Ok(_) => HTTPError::GoneError(Some(format!("The {} resource has been deleted because it was orphaned.", scoped_resource_type))), + JsonRejection::JsonSyntaxError(_) => HTTPError::BadRequestError(Some(format!("Failed to parse request body. Ensure the request body is valid JSON."))), - Err(error) => HTTPError::InternalServerError(Some(format!("Failed to delete orphaned access policy: {:?}", error))) + JsonRejection::MissingJsonContentType(_) => HTTPError::BadRequestError(Some(format!("Missing request body content type. It should be \"application/json\"."))), - }; - - let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; - return Err(http_error); + JsonRejection::BytesRejection(error) => HTTPError::InternalServerError(Some(format!("Failed to parse request body: {:?}", error))), - }, _ => HTTPError::InternalServerError(Some(error.to_string())) + }; - let _ = ServerLogEntry::from_http_error(&http_error, Some(&http_transaction.id), &mut postgres_client).await; + + let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; return Err(http_error); } }; - let _ = ServerLogEntry::trace(&format!("Getting action \"slashstep.accessPolicies.delete\"..."), Some(&http_transaction.id), &mut postgres_client).await; - let action = match Action::get_by_name("slashstep.accessPolicies.delete", &mut postgres_client).await { + let access_policy = get_access_policy(&access_policy_id, &http_transaction, &mut postgres_client).await?; + let user = get_user_from_option_user(&user, &http_transaction, &mut postgres_client).await?; + let resource_hierarchy = get_resource_hierarchy(&access_policy, &http_transaction, &mut postgres_client).await?; + let update_access_policy_action = get_action_from_name("slashstep.accessPolicies.update", &http_transaction, &mut postgres_client).await?; + verify_user_permissions(&user, &update_access_policy_action, &resource_hierarchy, &http_transaction, &AccessPolicyPermissionLevel::User, &mut postgres_client).await?; - Ok(action) => action, + let access_policy_action = get_action_from_id(&access_policy.action_id, &http_transaction, &mut postgres_client).await?; + verify_user_permissions(&user, &access_policy_action, &resource_hierarchy, &http_transaction, &AccessPolicyPermissionLevel::Editor, &mut postgres_client).await?; + + let _ = ServerLogEntry::trace(&format!("Updating access policy {}...", access_policy_id), Some(&http_transaction.id), &mut postgres_client).await; + let access_policy = match access_policy.update(&updated_access_policy_properties, &mut postgres_client).await { + + Ok(access_policy) => access_policy, Err(error) => { - let http_error = HTTPError::InternalServerError(Some(format!("Failed to get action \"slashstep.accessPolicies.delete\": {:?}", error))); + let http_error = HTTPError::InternalServerError(Some(format!("Failed to update access policy: {:?}", error))); let _ = http_error.print_and_save(Some(&http_transaction.id), &mut postgres_client).await; return Err(http_error); @@ -233,36 +277,30 @@ async fn delete_access_policy( }; - let _ = ServerLogEntry::trace(&format!("Verifying principal's permissions to delete access policy {}...", access_policy_id), Some(&http_transaction.id), &mut postgres_client).await; - - match PrincipalPermissionVerifier::verify_user_permissions(&user.id, &action.id, &resource_hierarchy, &AccessPolicyPermissionLevel::User, &mut postgres_client).await { - - Ok(_) => {}, - - Err(error) => { - - let http_error = match error { - PrincipalPermissionVerifierError::ForbiddenError { .. } => { - - if user.is_anonymous { - - HTTPError::UnauthorizedError(Some("You need at least user-level permission to the \"slashstep.accessPolicies.get\" action.".to_string())) + let _ = ServerLogEntry::success(&format!("Successfully updated access policy {}.", access_policy_id), Some(&http_transaction.id), &mut postgres_client).await; - } else { + return Ok(Json(access_policy)); - HTTPError::ForbiddenError(Some("You need at least user-level permission to the \"slashstep.accessPolicies.get\" action.".to_string())) - - } +} - }, - _ => HTTPError::InternalServerError(Some(error.to_string())) - }; - let _ = ServerLogEntry::from_http_error(&http_error, Some(&http_transaction.id), &mut postgres_client).await; - return Err(http_error); +#[axum::debug_handler] +async fn handle_delete_access_policy_request( + Path(access_policy_id): Path, + State(state): State, + Extension(http_transaction): Extension>, + Extension(user): Extension>> +) -> Result { - } + let http_transaction = http_transaction.clone(); + let mut postgres_client = state.database_pool.get().await.map_err(map_postgres_error_to_http_error)?; + let access_policy = get_access_policy(&access_policy_id, &http_transaction, &mut postgres_client).await?; + let user = get_user_from_option_user(&user, &http_transaction, &mut postgres_client).await?; + let resource_hierarchy = get_resource_hierarchy(&access_policy, &http_transaction, &mut postgres_client).await?; + let delete_access_policy_action = get_action_from_name("slashstep.accessPolicies.delete", &http_transaction, &mut postgres_client).await?; + verify_user_permissions(&user, &delete_access_policy_action, &resource_hierarchy, &http_transaction, &AccessPolicyPermissionLevel::User, &mut postgres_client).await?; - } + let access_policy_action = get_action_from_id(&access_policy.action_id, &http_transaction, &mut postgres_client).await?; + verify_user_permissions(&user, &access_policy_action, &resource_hierarchy, &http_transaction, &AccessPolicyPermissionLevel::Editor, &mut postgres_client).await?; match access_policy.delete(&mut postgres_client).await { @@ -288,8 +326,8 @@ pub fn get_router(state: AppState) -> Router { let router = Router::::new() .route("/access-policies/{access_policy_id}", axum::routing::get(handle_get_access_policy_request)) - .route("/access-policies/{access_policy_id}", axum::routing::patch(patch_access_policy)) - .route("/access-policies/{access_policy_id}", axum::routing::delete(delete_access_policy)) + .route("/access-policies/{access_policy_id}", axum::routing::patch(handle_patch_access_policy_request)) + .route("/access-policies/{access_policy_id}", axum::routing::delete(handle_delete_access_policy_request)) .layer(axum::middleware::from_fn_with_state(state, authentication_middleware::authenticate_user)); return router; diff --git a/src/routes/access-policies/{access_policy_id}/tests.rs b/src/routes/access-policies/{access_policy_id}/tests.rs index 98f9713..ab18490 100644 --- a/src/routes/access-policies/{access_policy_id}/tests.rs +++ b/src/routes/access-policies/{access_policy_id}/tests.rs @@ -3,11 +3,12 @@ use axum::middleware; use axum_extra::extract::cookie::Cookie; use axum_test::TestServer; use ntest::timeout; +use uuid::Uuid; use crate::{Action, AppState, SlashstepServerError, initialize_required_tables, middleware::http_request_middleware, pre_definitions::{initialize_pre_defined_actions, initialize_pre_defined_roles}, resources::{access_policy::{AccessPolicy, AccessPolicyError, AccessPolicyInheritanceLevel, AccessPolicyPermissionLevel, AccessPolicyPrincipalType, AccessPolicyScopedResourceType, InitialAccessPolicyProperties}, session::Session}, tests::TestEnvironment}; /// Verifies that the router can return a 200 status code and the requested access policy. #[tokio::test] -#[timeout(15000)] +#[timeout(20000)] async fn verify_returned_access_policy_by_id() -> Result<(), SlashstepServerError> { let test_environment = TestEnvironment::new().await?; @@ -130,7 +131,7 @@ async fn verify_authentication_when_getting_access_policy_by_id() -> Result<(), /// Verifies that the router can return a 403 status code if the user does not have permission to view the access policy. #[tokio::test] -#[timeout(15000)] +#[timeout(20000)] async fn verify_permission_when_getting_access_policy_by_id() -> Result<(), SlashstepServerError> { let test_environment = TestEnvironment::new().await?; @@ -165,7 +166,7 @@ async fn verify_permission_when_getting_access_policy_by_id() -> Result<(), Slas /// Verifies that the router can return a 404 status code if the requested access policy doesn't exist #[tokio::test] -#[timeout(15000)] +#[timeout(20000)] async fn verify_not_found_when_getting_access_policy_by_id() -> Result<(), SlashstepServerError> { let test_environment = TestEnvironment::new().await?; @@ -220,7 +221,7 @@ async fn verify_successful_deletion_when_deleting_access_policy_by_id() -> Resul let delete_access_policies_action = Action::get_by_name("slashstep.accessPolicies.delete", &mut postgres_client).await?; let access_policy_properties = InitialAccessPolicyProperties { action_id: delete_access_policies_action.id, - permission_level: AccessPolicyPermissionLevel::User, + permission_level: AccessPolicyPermissionLevel::Editor, inheritance_level: AccessPolicyInheritanceLevel::Enabled, principal_type: AccessPolicyPrincipalType::User, principal_user_id: Some(user.id), @@ -367,27 +368,318 @@ async fn verify_access_policy_exists_when_deleting_access_policy_by_id() -> Resu } -#[test] -fn patch_access_policy() { +/// Verifies that the router can return a 200 status code if the access policy is successfully patched. +#[tokio::test] +async fn verify_successful_patch_access_policy_by_id() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + let _ = initialize_pre_defined_actions(&mut postgres_client).await?; + let _ = initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let user = test_environment.create_random_user().await?; + let session = test_environment.create_session(&user.id).await?; + let json_web_token_private_key = Session::get_json_web_token_private_key().await?; + let session_token = session.generate_json_web_token(&json_web_token_private_key).await?; + let get_access_policies_action = Action::get_by_name("slashstep.accessPolicies.update", &mut postgres_client).await?; + let access_policy_properties = InitialAccessPolicyProperties { + action_id: get_access_policies_action.id, + permission_level: AccessPolicyPermissionLevel::Editor, + inheritance_level: AccessPolicyInheritanceLevel::Enabled, + principal_type: AccessPolicyPrincipalType::User, + principal_user_id: Some(user.id), + scoped_resource_type: AccessPolicyScopedResourceType::Instance, + ..Default::default() + }; + let access_policy = AccessPolicy::create(&access_policy_properties, &mut postgres_client).await?; + + let response = test_server.patch(&format!("/access-policies/{}", access_policy.id)) + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) + .json(&serde_json::json!({ + "permission_level": "User", + "inheritance_level": "Disabled" + })) + .await; + + assert_eq!(response.status_code(), 200); + + let response_access_policy: AccessPolicy = response.json(); + assert_eq!(response_access_policy.id, access_policy.id); + assert_eq!(response_access_policy.action_id, access_policy.action_id); + assert_eq!(response_access_policy.permission_level, AccessPolicyPermissionLevel::User); + assert_eq!(response_access_policy.inheritance_level, AccessPolicyInheritanceLevel::Disabled); + assert_eq!(response_access_policy.principal_type, access_policy.principal_type); + assert_eq!(response_access_policy.principal_user_id, access_policy.principal_user_id); + assert_eq!(response_access_policy.principal_group_id, access_policy.principal_group_id); + assert_eq!(response_access_policy.principal_role_id, access_policy.principal_role_id); + assert_eq!(response_access_policy.principal_app_id, access_policy.principal_app_id); + assert_eq!(response_access_policy.scoped_resource_type, access_policy.scoped_resource_type); + assert_eq!(response_access_policy.scoped_action_id, access_policy.scoped_action_id); + assert_eq!(response_access_policy.scoped_app_id, access_policy.scoped_app_id); + assert_eq!(response_access_policy.scoped_group_id, access_policy.scoped_group_id); + assert_eq!(response_access_policy.scoped_item_id, access_policy.scoped_item_id); + assert_eq!(response_access_policy.scoped_milestone_id, access_policy.scoped_milestone_id); + assert_eq!(response_access_policy.scoped_project_id, access_policy.scoped_project_id); + assert_eq!(response_access_policy.scoped_role_id, access_policy.scoped_role_id); + assert_eq!(response_access_policy.scoped_user_id, access_policy.scoped_user_id); + assert_eq!(response_access_policy.scoped_workspace_id, access_policy.scoped_workspace_id); + + return Ok(()); + +} + +/// Verifies that the router can return a 400 status code if the request doesn't have a valid content type. +#[tokio::test] +async fn verify_content_type_when_patching_access_policy_by_id() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + let _ = initialize_pre_defined_actions(&mut postgres_client).await?; + let _ = initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let response = test_server.patch("/access-policies/not-a-uuid") + .await; + + assert_eq!(response.status_code(), 400); + return Ok(()); + +} + +/// Verifies that the router can return a 400 status code if the request body is not valid JSON. +#[tokio::test] +async fn verify_request_body_exists_when_patching_access_policy_by_id() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + let _ = initialize_pre_defined_actions(&mut postgres_client).await?; + let _ = initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let response = test_server.patch("/access-policies/not-a-uuid") + .add_header("Content-Type", "application/json") + .await; + + assert_eq!(response.status_code(), 400); + return Ok(()); + +} + +/// Verifies that the router can return a 400 status code if the request body includes unwanted data. +#[tokio::test] +async fn verify_request_body_json_when_patching_access_policy_by_id() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + let _ = initialize_pre_defined_actions(&mut postgres_client).await?; + let _ = initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let response = test_server.patch("/access-policies/not-a-uuid") + .add_header("Content-Type", "application/json") + .json(&serde_json::json!({ + "permission_level": "Super Duper Admin", + "inheritance_level": "Required", + "principal_type": "User2", + })) + .await; + + assert_eq!(response.status_code(), 400); + return Ok(()); } -#[test] -fn verify_uuid_when_patching_access_policy() { +/// Verifies that the router can return a 400 status code if the access policy ID is not a UUID. +#[tokio::test] +async fn verify_uuid_when_patching_access_policy_by_id() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + let _ = initialize_pre_defined_actions(&mut postgres_client).await?; + let _ = initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let response = test_server.patch("/access-policies/not-a-uuid") + .add_header("Content-Type", "application/json") + .json(&serde_json::json!({ + "permission_level": "Editor", + "inheritance_level": "Disabled" + })) + .await; + + assert_eq!(response.status_code(), 400); + return Ok(()); } -#[test] -fn verify_authentication_when_patching_access_policy() { +/// Verifies that the router can return a 401 status code if the user needs authentication. +#[tokio::test] +async fn verify_authentication_when_patching_access_policy_by_id() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + let _ = initialize_pre_defined_actions(&mut postgres_client).await?; + let _ = initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let user = test_environment.create_random_user().await?; + let get_access_policies_action = Action::get_by_name("slashstep.accessPolicies.update", &mut postgres_client).await?; + let access_policy_properties = InitialAccessPolicyProperties { + action_id: get_access_policies_action.id, + permission_level: AccessPolicyPermissionLevel::Editor, + inheritance_level: AccessPolicyInheritanceLevel::Enabled, + principal_type: AccessPolicyPrincipalType::User, + principal_user_id: Some(user.id), + scoped_resource_type: AccessPolicyScopedResourceType::Instance, + ..Default::default() + }; + let access_policy = AccessPolicy::create(&access_policy_properties, &mut postgres_client).await?; + + let response = test_server.patch(&format!("/access-policies/{}", access_policy.id)) + .json(&serde_json::json!({ + "permission_level": "User", + "inheritance_level": "Disabled" + })) + .await; + + assert_eq!(response.status_code(), 401); + + return Ok(()); } -#[test] -fn verify_permission_when_patching_access_policy() { +/// Verifies that the router can return a 403 status code if the user does not have permission to patch the access policy. +#[tokio::test] +async fn verify_permission_when_patching_access_policy() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + let _ = initialize_pre_defined_actions(&mut postgres_client).await?; + let _ = initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let user = test_environment.create_random_user().await?; + let session = test_environment.create_session(&user.id).await?; + let json_web_token_private_key = Session::get_json_web_token_private_key().await?; + let session_token = session.generate_json_web_token(&json_web_token_private_key).await?; + let update_access_policies_action = Action::get_by_name("slashstep.accessPolicies.update", &mut postgres_client).await?; + let access_policy_properties = InitialAccessPolicyProperties { + action_id: update_access_policies_action.id, + permission_level: AccessPolicyPermissionLevel::None, + inheritance_level: AccessPolicyInheritanceLevel::Enabled, + principal_type: AccessPolicyPrincipalType::User, + principal_user_id: Some(user.id), + scoped_resource_type: AccessPolicyScopedResourceType::Instance, + ..Default::default() + }; + let access_policy = AccessPolicy::create(&access_policy_properties, &mut postgres_client).await?; + + let response = test_server.patch(&format!("/access-policies/{}", access_policy.id)) + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) + .json(&serde_json::json!({ + "permission_level": "User", + "inheritance_level": "Disabled" + })) + .await; + + assert_eq!(response.status_code(), 403); + + return Ok(()); } -#[test] -fn verify_access_policy_exists_when_patching_access_policy() { +/// Verifies that the router can return a 404 status code if the access policy does not exist. +#[tokio::test] +async fn verify_access_policy_exists_when_patching_access_policy() -> Result<(), SlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + let mut postgres_client = test_environment.postgres_pool.get().await?; + test_environment.initialize_required_tables().await?; + let _ = initialize_pre_defined_actions(&mut postgres_client).await?; + let _ = initialize_pre_defined_roles(&mut postgres_client).await?; + let state = AppState { + database_pool: test_environment.postgres_pool.clone(), + }; + + let router = super::get_router(state.clone()) + .layer(middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let response = test_server.patch(&format!("/access-policies/{}", Uuid::now_v7())) + .json(&serde_json::json!({ + "permission_level": "User", + "inheritance_level": "Disabled" + })) + .await; + + assert_eq!(response.status_code(), 404); + + return Ok(()); } \ No newline at end of file diff --git a/src/utilities/principal_permission_verifier.rs b/src/utilities/principal_permission_verifier.rs index 5b8e4b5..c318da4 100644 --- a/src/utilities/principal_permission_verifier.rs +++ b/src/utilities/principal_permission_verifier.rs @@ -1,14 +1,14 @@ use thiserror::Error; use uuid::Uuid; -use crate::resources::{access_policy::{AccessPolicy, AccessPolicyError, AccessPolicyPermissionLevel, ResourceHierarchy}, user::UserError}; +use crate::resources::{access_policy::{AccessPolicy, AccessPolicyError, AccessPolicyPermissionLevel, Principal, ResourceHierarchy}, user::UserError}; #[derive(Debug, Error)] pub enum PrincipalPermissionVerifierError { #[error("The principal does not have the required permissions to perform the action \"{action_id}\".")] ForbiddenError { - user_id: String, + principal: Principal, action_id: String, minimum_permission_level: AccessPolicyPermissionLevel, actual_permission_level: AccessPolicyPermissionLevel @@ -25,18 +25,29 @@ pub struct PrincipalPermissionVerifier; impl PrincipalPermissionVerifier { - pub async fn verify_user_permissions(user_id: &Uuid, action_id: &Uuid, resource_hierarchy: &ResourceHierarchy, minimum_permission_level: &AccessPolicyPermissionLevel, postgres_client: &mut deadpool_postgres::Client) -> Result<(), PrincipalPermissionVerifierError> { + pub async fn verify_permissions(principal: &Principal, action_id: &Uuid, resource_hierarchy: &ResourceHierarchy, minimum_permission_level: &AccessPolicyPermissionLevel, postgres_client: &mut deadpool_postgres::Client) -> Result<(), PrincipalPermissionVerifierError> { - let relevant_access_policies = AccessPolicy::list_by_hierarchy(resource_hierarchy, action_id, postgres_client).await?; - let deepest_access_policy = relevant_access_policies.first(); + let relevant_access_policies = AccessPolicy::list_by_hierarchy(principal, action_id, resource_hierarchy, postgres_client).await?; + let deepest_access_policy = match relevant_access_policies.first() { - if deepest_access_policy.is_none() { + Some(access_policy) => access_policy, - return Err(PrincipalPermissionVerifierError::ForbiddenError { - user_id: user_id.to_string(), + None => return Err(PrincipalPermissionVerifierError::ForbiddenError { + principal: principal.clone(), action_id: action_id.to_string(), minimum_permission_level: minimum_permission_level.clone(), actual_permission_level: AccessPolicyPermissionLevel::None + }) + + }; + + if &deepest_access_policy.permission_level < minimum_permission_level { + + return Err(PrincipalPermissionVerifierError::ForbiddenError { + principal: principal.clone(), + action_id: action_id.to_string(), + minimum_permission_level: minimum_permission_level.clone(), + actual_permission_level: deepest_access_policy.permission_level }); }