diff --git a/src/routes/field-values/{field_value_id}/access-policies/mod.rs b/src/routes/field-values/{field_value_id}/access-policies/mod.rs new file mode 100644 index 0000000..f43adf0 --- /dev/null +++ b/src/routes/field-values/{field_value_id}/access-policies/mod.rs @@ -0,0 +1,161 @@ +/** + * + * Any functionality for /field-values/{field_value_id}/access-policies should be handled here. + * + * Programmers: + * - Christian Toney (https://christiantoney.com) + * + * © 2026 Beastslash LLC + * + */ + +#[cfg(test)] +mod tests; + +use std::sync::Arc; +use axum::{Extension, Json, Router, extract::{Path, Query, State, rejection::JsonRejection}}; +use axum_extra::response::ErasedJson; +use pg_escape::quote_literal; +use reqwest::StatusCode; +use crate::{AppState, HTTPError, middleware::{authentication_middleware, http_request_middleware}, resources::{access_policy::{AccessPolicy, AccessPolicyResourceType, ActionPermissionLevel, DEFAULT_MAXIMUM_ACCESS_POLICY_LIST_LIMIT, InitialAccessPolicyProperties, InitialAccessPolicyPropertiesForPredefinedScope}, action_log_entry::{ActionLogEntry, ActionLogEntryActorType, ActionLogEntryTargetResourceType, InitialActionLogEntryProperties}, app::App, app_authorization::AppAuthorization, http_transaction::HTTPTransaction, server_log_entry::ServerLogEntry, user::User}, utilities::{reusable_route_handlers::{ResourceListQueryParameters, list_resources}, route_handler_utilities::{AuthenticatedPrincipal, get_action_by_id, get_action_by_name, get_action_log_entry_expiration_timestamp, get_authenticated_principal, get_field_value_by_id, get_request_body_without_json_rejection, get_resource_hierarchy, get_uuid_from_string, verify_delegate_permissions, verify_principal_permissions}}}; + +/// GET /field-values/{field_value_id}/access-policies +/// +/// Lists access policies for an field value. +#[axum::debug_handler] +async fn handle_list_access_policies_request( + Path(field_value_id): Path, + Query(query_parameters): Query, + State(state): State, + Extension(http_transaction): Extension>, + Extension(authenticated_user): Extension>>, + Extension(authenticated_app): Extension>>, + Extension(authenticated_app_authorization): Extension>> +) -> Result { + + let http_transaction = http_transaction.clone(); + let field_value_id = get_uuid_from_string(&field_value_id, "field value", &http_transaction, &state.database_pool).await?; + let target_field_value = get_field_value_by_id(&field_value_id, &http_transaction, &state.database_pool).await?; + let resource_hierarchy = get_resource_hierarchy(&target_field_value, &AccessPolicyResourceType::FieldValue, &target_field_value.id, &http_transaction, &state.database_pool).await?; + + let query = format!( + "scoped_resource_type = 'FieldValue' AND scoped_field_value_id = {}{}", + quote_literal(&target_field_value.id.to_string()), + query_parameters.query.and_then(|query| Some(format!(" AND {}", query))).unwrap_or("".to_string()) + ); + + let query_parameters = ResourceListQueryParameters { + query: Some(query) + }; + + let response = list_resources( + Query(query_parameters), + State(state), + Extension(http_transaction), + Extension(authenticated_user), + Extension(authenticated_app), + Extension(authenticated_app_authorization), + resource_hierarchy, + ActionLogEntryTargetResourceType::FieldValue, + Some(target_field_value.id), + |query, database_pool, individual_principal| Box::new(AccessPolicy::count(query, database_pool, individual_principal)), + |query, database_pool, individual_principal| Box::new(AccessPolicy::list(query, database_pool, individual_principal)), + "accessPolicies.list", + DEFAULT_MAXIMUM_ACCESS_POLICY_LIST_LIMIT, + "access policies", + "access policy" + ).await; + + return response; + +} + +/// POST /field-values/{field_value_id}/access-policies +/// +/// Creates an access policy for an field value. +#[axum::debug_handler] +async fn handle_create_access_policy_request( + Path(field_value_id): Path, + State(state): State, + Extension(http_transaction): Extension>, + Extension(authenticated_user): Extension>>, + Extension(authenticated_app): Extension>>, + Extension(authenticated_app_authorization): Extension>>, + body: Result, JsonRejection> +) -> Result<(StatusCode, Json), HTTPError> { + + let http_transaction = http_transaction.clone(); + let access_policy_properties_json = get_request_body_without_json_rejection(body, &http_transaction, &state.database_pool).await?; + + // Make sure the user can create access policies for the target action. + let field_value_id = get_uuid_from_string(&field_value_id, "field value", &http_transaction, &state.database_pool).await?; + let target_field_value = get_field_value_by_id(&field_value_id, &http_transaction, &state.database_pool).await?; + let resource_hierarchy = get_resource_hierarchy(&target_field_value, &AccessPolicyResourceType::FieldValue, &target_field_value.id, &http_transaction, &state.database_pool).await?; + let create_access_policies_action = get_action_by_name("accessPolicies.create", &http_transaction, &state.database_pool).await?; + verify_delegate_permissions(authenticated_app_authorization.as_ref().map(|app_authorization| &app_authorization.id), &create_access_policies_action.id, &http_transaction.id, &ActionPermissionLevel::User, &state.database_pool).await?; + let authenticated_principal = get_authenticated_principal(authenticated_user.as_ref(), authenticated_app.as_ref())?; + verify_principal_permissions(&authenticated_principal, &create_access_policies_action, &resource_hierarchy, &http_transaction, &ActionPermissionLevel::User, &state.database_pool).await?; + + // Make sure the user has at least editor access to the access policy's action. + let access_policy_action = get_action_by_id(&access_policy_properties_json.action_id.to_string(), &http_transaction, &state.database_pool).await?; + let minimum_permission_level = if access_policy_properties_json.permission_level > ActionPermissionLevel::Editor { access_policy_properties_json.permission_level } else { ActionPermissionLevel::Editor }; + verify_delegate_permissions(authenticated_app_authorization.as_ref().map(|app_authorization| &app_authorization.id), &access_policy_action.id, &http_transaction.id, &minimum_permission_level, &state.database_pool).await?; + verify_principal_permissions(&authenticated_principal, &access_policy_action, &resource_hierarchy, &http_transaction, &minimum_permission_level, &state.database_pool).await?; + + // Create the access policy. + ServerLogEntry::trace(&format!("Creating access policy for field value {}...", field_value_id), Some(&http_transaction.id), &state.database_pool).await.ok(); + let access_policy = match AccessPolicy::create(&InitialAccessPolicyProperties { + action_id: access_policy_properties_json.action_id, + permission_level: access_policy_properties_json.permission_level, + is_inheritance_enabled: access_policy_properties_json.is_inheritance_enabled, + principal_type: access_policy_properties_json.principal_type, + principal_user_id: access_policy_properties_json.principal_user_id, + principal_group_id: access_policy_properties_json.principal_group_id, + principal_role_id: access_policy_properties_json.principal_role_id, + principal_app_id: access_policy_properties_json.principal_app_id, + scoped_resource_type: AccessPolicyResourceType::FieldValue, + scoped_field_value_id: Some(target_field_value.id), + ..Default::default() + }, &state.database_pool).await { + + Ok(access_policy) => access_policy, + + Err(error) => { + + let http_error = HTTPError::InternalServerError(Some(format!("Failed to create access policy: {:?}", error))); + ServerLogEntry::from_http_error(&http_error, Some(&http_transaction.id), &state.database_pool).await.ok(); + return Err(http_error) + + } + + }; + + let expiration_timestamp = get_action_log_entry_expiration_timestamp(&http_transaction, &state.database_pool).await?; + ActionLogEntry::create(&InitialActionLogEntryProperties { + action_id: create_access_policies_action.id, + http_transaction_id: Some(http_transaction.id), + expiration_timestamp, + actor_type: if let AuthenticatedPrincipal::User(_) = &authenticated_principal { ActionLogEntryActorType::User } else { ActionLogEntryActorType::App }, + actor_user_id: if let AuthenticatedPrincipal::User(user) = &authenticated_principal { Some(user.id.clone()) } else { None }, + actor_app_id: if let AuthenticatedPrincipal::App(app) = &authenticated_principal { Some(app.id.clone()) } else { None }, + target_resource_type: ActionLogEntryTargetResourceType::AccessPolicy, + target_access_policy_id: Some(access_policy.id), + ..Default::default() + }, &state.database_pool).await.ok(); + ServerLogEntry::success(&format!("Successfully created access policy {}.", access_policy.id), Some(&http_transaction.id), &state.database_pool).await.ok(); + + return Ok((StatusCode::CREATED, Json(access_policy))); + +} + +pub fn get_router(state: AppState) -> Router { + + let router = Router::::new() + .route("/field-values/{field_value_id}/access-policies", axum::routing::get(handle_list_access_policies_request)) + .route("/field-values/{field_value_id}/access-policies", axum::routing::post(handle_create_access_policy_request)) + .layer(axum::middleware::from_fn_with_state(state.clone(), authentication_middleware::authenticate_user)) + .layer(axum::middleware::from_fn_with_state(state.clone(), authentication_middleware::authenticate_app)) + .layer(axum::middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)); + return router; + +} diff --git a/src/routes/field-values/{field_value_id}/access-policies/tests.rs b/src/routes/field-values/{field_value_id}/access-policies/tests.rs new file mode 100644 index 0000000..07315b4 --- /dev/null +++ b/src/routes/field-values/{field_value_id}/access-policies/tests.rs @@ -0,0 +1,447 @@ +/** + * + * Any test cases for /field-values/{field_value_id}/access-policies should be handled here. + * + * Programmers: + * - Christian Toney (https://christiantoney.com) + * + * © 2026 Beastslash LLC + * + */ + +use std::net::SocketAddr; +use axum_extra::extract::cookie::Cookie; +use axum_test::TestServer; +use pg_escape::quote_literal; +use reqwest::StatusCode; +use uuid::Uuid; +use crate::{AppState, get_json_web_token_private_key, initialize_required_tables, predefinitions::{initialize_predefined_actions, initialize_predefined_configurations, initialize_predefined_roles}, resources::{access_policy::{AccessPolicy, AccessPolicyPrincipalType, AccessPolicyResourceType, ActionPermissionLevel, DEFAULT_ACCESS_POLICY_LIST_LIMIT, IndividualPrincipal, InitialAccessPolicyProperties, InitialAccessPolicyPropertiesForPredefinedScope}, action::Action}, tests::{TestEnvironment, TestSlashstepServerError}, utilities::reusable_route_handlers::ListResourcesResponseBody}; + +async fn create_field_value_access_policy(database_pool: &deadpool_postgres::Pool, scoped_field_value_id: &Uuid, user_id: &Uuid, action_id: &Uuid, permission_level: &ActionPermissionLevel) -> Result { + + let access_policy = AccessPolicy::create(&InitialAccessPolicyProperties { + action_id: action_id.clone(), + permission_level: permission_level.clone(), + is_inheritance_enabled: true, + principal_type: crate::resources::access_policy::AccessPolicyPrincipalType::User, + principal_user_id: Some(user_id.clone()), + scoped_resource_type: AccessPolicyResourceType::FieldValue, + scoped_field_value_id: Some(scoped_field_value_id.clone()), + ..Default::default() + }, database_pool).await?; + + return Ok(access_policy); + +} + +#[tokio::test] +async fn verify_successful_access_policy_creation() -> Result<(), TestSlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + initialize_required_tables(&test_environment.database_pool).await?; + initialize_predefined_actions(&test_environment.database_pool).await?; + initialize_predefined_roles(&test_environment.database_pool).await?; + initialize_predefined_configurations(&test_environment.database_pool).await?; + + // Give the user access to the "accessPolicies.create" action. + let user = test_environment.create_random_user().await?; + let session = test_environment.create_random_session(Some(&user.id)).await?; + let json_web_token_private_key = get_json_web_token_private_key().await?; + let session_token = session.generate_json_web_token(&json_web_token_private_key).await?; + let create_access_policies_action = Action::get_by_name("accessPolicies.create", &test_environment.database_pool).await?; + test_environment.create_server_access_policy(&user.id, &create_access_policies_action.id, &ActionPermissionLevel::User).await?; + + // Give the user editor access to a dummy action. + let dummy_field_value = test_environment.create_random_field_value().await?; + let dummy_action = test_environment.create_random_action(None).await?; + test_environment.create_server_access_policy(&user.id, &dummy_action.id, &ActionPermissionLevel::Editor).await?; + + // Set up the server and send the request. + let initial_access_policy_properties = InitialAccessPolicyPropertiesForPredefinedScope { + action_id: dummy_action.id, + permission_level: ActionPermissionLevel::User, + is_inheritance_enabled: true, + principal_type: AccessPolicyPrincipalType::User, + principal_user_id: Some(user.id), + ..Default::default() + }; + let state = AppState { + database_pool: test_environment.database_pool.clone(), + }; + let router = super::get_router(state.clone()) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + let response = test_server.post(&format!("/field-values/{}/access-policies", dummy_field_value.id)) + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) + .json(&serde_json::json!(initial_access_policy_properties)) + .await; + + assert_eq!(response.status_code(), StatusCode::CREATED); + + let response_access_policy: AccessPolicy = response.json(); + assert_eq!(initial_access_policy_properties.action_id, response_access_policy.action_id); + assert_eq!(initial_access_policy_properties.principal_type, response_access_policy.principal_type); + assert_eq!(initial_access_policy_properties.principal_user_id, response_access_policy.principal_user_id); + assert_eq!(initial_access_policy_properties.permission_level, response_access_policy.permission_level); + assert_eq!(initial_access_policy_properties.is_inheritance_enabled, response_access_policy.is_inheritance_enabled); + + return Ok(()); + +} + +/// Verifies that the router can return a 200 status code and the requested access policy list. +#[tokio::test] +async fn verify_returned_access_policy_list_without_query() -> Result<(), TestSlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + initialize_required_tables(&test_environment.database_pool).await?; + initialize_predefined_actions(&test_environment.database_pool).await?; + initialize_predefined_roles(&test_environment.database_pool).await?; + initialize_predefined_configurations(&test_environment.database_pool).await?; + + // Give the user access to the "accessPolicies.get" action. + let user = test_environment.create_random_user().await?; + let session = test_environment.create_random_session(Some(&user.id)).await?; + let json_web_token_private_key = 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("accessPolicies.get", &test_environment.database_pool).await?; + test_environment.create_server_access_policy(&user.id, &get_access_policies_action.id, &ActionPermissionLevel::User).await?; + + // Give the user access to the "accessPolicies.list" action. + let list_access_policies_action = Action::get_by_name("accessPolicies.list", &test_environment.database_pool).await?; + test_environment.create_server_access_policy(&user.id, &list_access_policies_action.id, &ActionPermissionLevel::User).await?; + + // Create dummy resources. + let dummy_field_value = test_environment.create_random_field_value().await?; + let shown_access_policy = create_field_value_access_policy(&test_environment.database_pool, &dummy_field_value.id, &user.id, &list_access_policies_action.id, &ActionPermissionLevel::User).await?; + + // Set up the server and send the request. + let state = AppState { + database_pool: test_environment.database_pool.clone(), + }; + let router = super::get_router(state.clone()) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + let response = test_server.get(&format!("/field-values/{}/access-policies", &dummy_field_value.id)) + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", &session_token))) + .await; + + // Verify the response. + assert_eq!(response.status_code(), StatusCode::OK); + + let response_access_policies: ListResourcesResponseBody:: = response.json(); + assert_eq!(response_access_policies.total_count, 1); + assert_eq!(response_access_policies.resources.len(), 1); + + let query = format!("scoped_resource_type = 'FieldValue' AND scoped_field_value_id = {}", quote_literal(&dummy_field_value.id.to_string())); + let actual_access_policy_count = AccessPolicy::count(&query, &test_environment.database_pool, Some(&IndividualPrincipal::User(user.id))).await?; + assert_eq!(response_access_policies.total_count, actual_access_policy_count); + + let actual_access_policies = AccessPolicy::list(&query, &test_environment.database_pool, Some(&IndividualPrincipal::User(user.id))).await?; + assert_eq!(response_access_policies.resources.len(), actual_access_policies.len()); + assert_eq!(response_access_policies.resources[0].id, actual_access_policies[0].id); + assert_eq!(response_access_policies.resources[0].id, shown_access_policy.id); + + return Ok(()); + +} + +/// Verifies that the router can return a 200 status code and the requested access policy list. +#[tokio::test] +async fn verify_returned_access_policy_list_with_query() -> Result<(), TestSlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + initialize_required_tables(&test_environment.database_pool).await?; + initialize_predefined_actions(&test_environment.database_pool).await?; + initialize_predefined_roles(&test_environment.database_pool).await?; + initialize_predefined_configurations(&test_environment.database_pool).await?; + + // Give the user access to the "accessPolicies.get" action. + let user = test_environment.create_random_user().await?; + let session = test_environment.create_random_session(Some(&user.id)).await?; + let json_web_token_private_key = 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("accessPolicies.get", &test_environment.database_pool).await?; + test_environment.create_server_access_policy(&user.id, &get_access_policies_action.id, &ActionPermissionLevel::User).await?; + + // Give the user access to the "accessPolicies.list" action. + let list_access_policies_action = Action::get_by_name("accessPolicies.list", &test_environment.database_pool).await?; + test_environment.create_server_access_policy(&user.id, &list_access_policies_action.id, &ActionPermissionLevel::User).await?; + + // Create a few dummy access policies. + let dummy_field_value = test_environment.create_random_field_value().await?; + create_field_value_access_policy(&test_environment.database_pool, &dummy_field_value.id, &user.id, &list_access_policies_action.id, &ActionPermissionLevel::User).await?; + + let shown_access_policy = create_field_value_access_policy(&test_environment.database_pool, &dummy_field_value.id, &user.id, &get_access_policies_action.id, &ActionPermissionLevel::Editor).await?; + + // Set up the server and send the request. + let additional_query = format!("permission_level = 'Editor'"); + let state = AppState { + database_pool: test_environment.database_pool.clone(), + }; + let router = super::get_router(state.clone()) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + let response = test_server.get(&format!("/field-values/{}/access-policies", &dummy_field_value.id)) + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", &session_token))) + .add_query_param("query", &additional_query) + .await; + + // Verify the response. + assert_eq!(response.status_code(), StatusCode::OK); + + let response_access_policies: ListResourcesResponseBody:: = response.json(); + assert_eq!(response_access_policies.total_count, 1); + assert_eq!(response_access_policies.resources.len(), 1); + + let query = format!("scoped_resource_type = 'FieldValue' AND scoped_field_value_id = {} and permission_level = 'Editor'", quote_literal(&dummy_field_value.id.to_string())); + let actual_access_policy_count = AccessPolicy::count(&query, &test_environment.database_pool, Some(&IndividualPrincipal::User(user.id))).await?; + assert_eq!(response_access_policies.total_count, actual_access_policy_count); + + let actual_access_policies = AccessPolicy::list(&query, &test_environment.database_pool, Some(&IndividualPrincipal::User(user.id))).await?; + assert_eq!(response_access_policies.resources.len(), actual_access_policies.len()); + assert_eq!(response_access_policies.resources[0].id, actual_access_policies[0].id); + assert_eq!(response_access_policies.resources[0].id, shown_access_policy.id); + + return Ok(()); + +} + +/// Verifies that the default access policy list limit is enforced. +#[tokio::test] +async fn verify_default_access_policy_list_limit() -> Result<(), TestSlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + initialize_required_tables(&test_environment.database_pool).await?; + initialize_predefined_actions(&test_environment.database_pool).await?; + initialize_predefined_roles(&test_environment.database_pool).await?; + initialize_predefined_configurations(&test_environment.database_pool).await?; + + // Give the user access to the "accessPolicies.get" action. + let user = test_environment.create_random_user().await?; + let session = test_environment.create_random_session(Some(&user.id)).await?; + let json_web_token_private_key = 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("accessPolicies.get", &test_environment.database_pool).await?; + test_environment.create_server_access_policy(&user.id, &get_access_policies_action.id, &ActionPermissionLevel::User).await?; + + // Give the user access to the "accessPolicies.list" action. + let list_access_policies_action = Action::get_by_name("accessPolicies.list", &test_environment.database_pool).await?; + test_environment.create_server_access_policy(&user.id, &list_access_policies_action.id, &ActionPermissionLevel::User).await?; + + // Create dummy access policies. + let dummy_field_value = test_environment.create_random_field_value().await?; + for _ in 0..(DEFAULT_ACCESS_POLICY_LIST_LIMIT + 1) { + + let random_action = test_environment.create_random_action(None).await?; + let random_user = test_environment.create_random_user().await?; + create_field_value_access_policy(&test_environment.database_pool, &dummy_field_value.id, &random_user.id, &random_action.id, &ActionPermissionLevel::User).await?; + + } + + let state = AppState { + database_pool: test_environment.database_pool.clone(), + }; + let router = super::get_router(state.clone()) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + let response = test_server.get(&format!("/field-values/{}/access-policies", &dummy_field_value.id)) + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) + .await; + + assert_eq!(response.status_code(), StatusCode::OK); + + let response_body: ListResourcesResponseBody:: = response.json(); + assert_eq!(response_body.resources.len(), DEFAULT_ACCESS_POLICY_LIST_LIMIT as usize); + + return Ok(()); + +} + +/// Verifies that the server returns a 422 status code when the provided limit is over the maximum limit. +#[tokio::test] +async fn verify_maximum_access_policy_list_limit() -> Result<(), TestSlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + initialize_required_tables(&test_environment.database_pool).await?; + initialize_predefined_actions(&test_environment.database_pool).await?; + initialize_predefined_roles(&test_environment.database_pool).await?; + initialize_predefined_configurations(&test_environment.database_pool).await?; + + // Create the user and the session. + let user = test_environment.create_random_user().await?; + let session = test_environment.create_random_session(Some(&user.id)).await?; + let json_web_token_private_key = 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("accessPolicies.get", &test_environment.database_pool).await?; + test_environment.create_server_access_policy(&user.id, &get_access_policies_action.id, &ActionPermissionLevel::User).await?; + let list_access_policies_action = Action::get_by_name("accessPolicies.list", &test_environment.database_pool).await?; + test_environment.create_server_access_policy(&user.id, &list_access_policies_action.id, &ActionPermissionLevel::User).await?; + + // Create dummy resources. + let dummy_field_value = test_environment.create_random_field_value().await?; + + // Set up the server and send the request. + let state = AppState { + database_pool: test_environment.database_pool.clone(), + }; + let router = super::get_router(state.clone()) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + let response = test_server.get(&format!("/field-values/{}/access-policies", &dummy_field_value.id)) + .add_query_param("query", format!("LIMIT {}", DEFAULT_ACCESS_POLICY_LIST_LIMIT + 1)) + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) + .await; + + // Verify the response. + assert_eq!(response.status_code(), StatusCode::UNPROCESSABLE_ENTITY); + + return Ok(()); + +} + +/// Verifies that the server returns a 400 status code when the query is invalid. +#[tokio::test] +async fn verify_query_when_listing_access_policies() -> Result<(), TestSlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + initialize_required_tables(&test_environment.database_pool).await?; + initialize_predefined_actions(&test_environment.database_pool).await?; + initialize_predefined_roles(&test_environment.database_pool).await?; + initialize_predefined_configurations(&test_environment.database_pool).await?; + + // Create the user and the session. + let user = test_environment.create_random_user().await?; + let session = test_environment.create_random_session(Some(&user.id)).await?; + let json_web_token_private_key = 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("accessPolicies.get", &test_environment.database_pool).await?; + test_environment.create_server_access_policy(&user.id, &get_access_policies_action.id, &ActionPermissionLevel::User).await?; + + let list_access_policies_action = Action::get_by_name("accessPolicies.list", &test_environment.database_pool).await?; + test_environment.create_server_access_policy(&user.id, &list_access_policies_action.id, &ActionPermissionLevel::User).await?; + + // Create dummy resources. + let dummy_field_value = test_environment.create_random_field_value().await?; + + // Set up the server and send the request. + let state = AppState { + database_pool: test_environment.database_pool.clone(), + }; + let router = super::get_router(state.clone()) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + + let bad_requests = vec![ + test_server.get(&format!("/field-values/{}/access-policies", &dummy_field_value.id)) + .add_query_param("query", format!("SELECT * FROM access_policies")), + test_server.get(&format!("/field-values/{}/access-policies", &dummy_field_value.id)) + .add_query_param("query", format!("SELECT PG_SLEEP(10)")), + test_server.get(&format!("/field-values/{}/access-policies", &dummy_field_value.id)) + .add_query_param("query", format!("SELECT * FROM access_policies WHERE action_id = {}", get_access_policies_action.id)) + ]; + + for request in bad_requests { + + let response = request + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) + .await; + + assert_eq!(response.status_code(), StatusCode::BAD_REQUEST); + + } + + let unprocessable_entity_requests = vec![ + test_server.get(&format!("/field-values/{}/access-policies", &dummy_field_value.id)) + .add_query_param("query", format!("action_ied = {}", get_access_policies_action.id)), + test_server.get(&format!("/field-values/{}/access-policies", &dummy_field_value.id)) + .add_query_param("query", format!("1 = 1")) + ]; + + for request in unprocessable_entity_requests { + + let response = request + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) + .await; + + assert_eq!(response.status_code(), StatusCode::UNPROCESSABLE_ENTITY); + + } + + return Ok(()); + +} + +/// Verifies that the server returns a 401 status code when the user lacks permissions and is unauthenticated. +#[tokio::test] +async fn verify_authentication_when_listing_access_policies() -> Result<(), TestSlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + initialize_required_tables(&test_environment.database_pool).await?; + initialize_predefined_actions(&test_environment.database_pool).await?; + initialize_predefined_roles(&test_environment.database_pool).await?; + initialize_predefined_configurations(&test_environment.database_pool).await?; + + // Create a dummy action. + let dummy_field_value = test_environment.create_random_field_value().await?; + + // Set up the server and send the request. + let state = AppState { + database_pool: test_environment.database_pool.clone(), + }; + let router = super::get_router(state.clone()) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + let response = test_server.get(&format!("/field-values/{}/access-policies", &dummy_field_value.id)) + .await; + + // Verify the response. + assert_eq!(response.status_code(), StatusCode::UNAUTHORIZED); + + return Ok(()); + +} + +/// Verifies that the server returns a 403 status code when the user lacks permissions and is authenticated. +#[tokio::test] +async fn verify_permission_when_listing_access_policies() -> Result<(), TestSlashstepServerError> { + + let test_environment = TestEnvironment::new().await?; + initialize_required_tables(&test_environment.database_pool).await?; + initialize_predefined_actions(&test_environment.database_pool).await?; + initialize_predefined_roles(&test_environment.database_pool).await?; + initialize_predefined_configurations(&test_environment.database_pool).await?; + + // Create the user and the session. + let user = test_environment.create_random_user().await?; + let session = test_environment.create_random_session(Some(&user.id)).await?; + let json_web_token_private_key = get_json_web_token_private_key().await?; + let session_token = session.generate_json_web_token(&json_web_token_private_key).await?; + + // Create a dummy action. + let dummy_field_value = test_environment.create_random_field_value().await?; + + // Set up the server and send the request. + let state = AppState { + database_pool: test_environment.database_pool.clone(), + }; + let router = super::get_router(state.clone()) + .with_state(state) + .into_make_service_with_connect_info::(); + let test_server = TestServer::new(router)?; + let response = test_server.get(&format!("/field-values/{}/access-policies", &dummy_field_value.id)) + .add_cookie(Cookie::new("sessionToken", format!("Bearer {}", session_token))) + .await; + + assert_eq!(response.status_code(), StatusCode::FORBIDDEN); + + return Ok(()); + +} \ No newline at end of file diff --git a/src/routes/field-values/{field_value_id}/mod.rs b/src/routes/field-values/{field_value_id}/mod.rs index 7441874..35b4ef5 100644 --- a/src/routes/field-values/{field_value_id}/mod.rs +++ b/src/routes/field-values/{field_value_id}/mod.rs @@ -22,8 +22,8 @@ use crate::{ utilities::{reusable_route_handlers::delete_resource, route_handler_utilities::{AuthenticatedPrincipal, get_action_by_name, get_action_log_entry_expiration_timestamp, get_app_by_id, get_authenticated_principal, get_field_value_by_id, get_resource_by_id, get_resource_hierarchy, get_uuid_from_string, verify_delegate_permissions, verify_principal_permissions}} }; -// #[path = "./access-policies/mod.rs"] -// mod access_policies; +#[path = "./access-policies/mod.rs"] +mod access_policies; #[cfg(test)] mod tests; @@ -189,7 +189,8 @@ pub fn get_router(state: AppState) -> Router { // .route("/field-values/{field_value_id}", axum::routing::patch(handle_patch_app_request)) .layer(axum::middleware::from_fn_with_state(state.clone(), authentication_middleware::authenticate_user)) .layer(axum::middleware::from_fn_with_state(state.clone(), authentication_middleware::authenticate_app)) - .layer(axum::middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)); + .layer(axum::middleware::from_fn_with_state(state.clone(), http_request_middleware::create_http_request)) + .merge(access_policies::get_router(state.clone())); return router; }