From 458cd5751930f887e2ce84b2e00b8ee35181ba47 Mon Sep 17 00:00:00 2001 From: c0repwn3r Date: Thu, 27 Apr 2023 12:53:10 -0400 Subject: [PATCH] PUT /v1/roles/roleid --- trifid-api/src/main.rs | 1 + trifid-api/src/routes/v1/roles.rs | 202 +++++++++++++++++++++++++++++- 2 files changed, 199 insertions(+), 4 deletions(-) diff --git a/trifid-api/src/main.rs b/trifid-api/src/main.rs index 94754f0..05a68bf 100644 --- a/trifid-api/src/main.rs +++ b/trifid-api/src/main.rs @@ -93,6 +93,7 @@ async fn main() -> Result<(), Box> { .service(routes::v1::roles::get_roles) .service(routes::v1::roles::get_role) .service(routes::v1::roles::delete_role) + .service(routes::v1::roles::update_role_request) .service(routes::v1::trifid::trifid_extensions) }).bind(CONFIG.server.bind)?.run().await?; diff --git a/trifid-api/src/routes/v1/roles.rs b/trifid-api/src/routes/v1/roles.rs index 3c21569..fd9d500 100644 --- a/trifid-api/src/routes/v1/roles.rs +++ b/trifid-api/src/routes/v1/roles.rs @@ -31,9 +31,12 @@ //#DELETE /v1/roles/{role_id} t+parity:full t+type:documented t+status:done // This endpoint has full parity with the original API. It has been recreated from the original API documentation. // This endpoint is considered done. No major features should be added or removed, unless it fixes bugs. +//#PUT /v1/roles/{role_id} t+parity:full t+type:documented t+status:done +// This endpoint has full parity with the original API. It has been recreated from the original API documentation. +// This endpoint is considered done. No major features should be added or removed, unless it fixes bugs. use std::time::{SystemTime, UNIX_EPOCH}; -use actix_web::{get, HttpRequest, HttpResponse, post}; +use actix_web::{get, HttpRequest, HttpResponse, post, put}; use actix_web::web::{Data, Json, Path, Query}; use chrono::{TimeZone, Utc}; use log::error; @@ -51,6 +54,7 @@ use actix_web::delete; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct CreateRoleRequest { + #[serde(default)] pub name: String, #[serde(default)] pub description: String, @@ -58,6 +62,15 @@ pub struct CreateRoleRequest { pub firewall_rules: Vec } +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct UpdateRoleRequest { + pub name: Option, + #[serde(default)] + pub description: String, + #[serde(default, rename = "firewallRules")] + pub firewall_rules: Vec +} + #[derive(Serialize, Deserialize, Debug, Clone)] pub struct RoleFirewallRule { pub protocol: RoleProtocol, @@ -663,7 +676,7 @@ pub struct GetRoleResponse { pub struct GetRoleResponseMetadata {} #[delete("/v1/roles/{role_id}")] -pub async fn delete_role(net: Path, req_info: HttpRequest, db: Data) -> HttpResponse { +pub async fn delete_role(role: Path, req_info: HttpRequest, db: Data) -> HttpResponse { // For this endpoint, you either need to be a fully authenticated user OR a token with roles:delete let session_info = enforce_2fa(&req_info, &db.conn).await.unwrap_or(TokenInfo::NotPresent); let api_token_info = enforce_api_token(&req_info, &["roles:delete"], &db.conn).await.unwrap_or(TokenInfo::NotPresent); @@ -694,7 +707,7 @@ pub async fn delete_role(net: Path, req_info: HttpRequest, db: Data = match role::Entity::find().filter(role::Column::Id.eq(net.into_inner())).one(&db.conn).await { + let role: Option = match role::Entity::find().filter(role::Column::Id.eq(role.into_inner())).one(&db.conn).await { Ok(r) => r, Err(e) => { error!("database error: {}", e); @@ -754,4 +767,185 @@ pub struct RoleDeleteResponse { pub struct RoleDeleteResponseData {} #[derive(Serialize, Deserialize, Debug, Clone)] -pub struct RoleDeleteResponseMetadata {} \ No newline at end of file +pub struct RoleDeleteResponseMetadata {} + + +#[put("/v1/roles/{role_id}")] +pub async fn update_role_request(role: Path, req: Json, req_info: HttpRequest, db: Data) -> HttpResponse { + // For this endpoint, you either need to be a fully authenticated user OR a token with roles:create + let session_info = enforce_2fa(&req_info, &db.conn).await.unwrap_or(TokenInfo::NotPresent); + let api_token_info = enforce_api_token(&req_info, &["roles:create"], &db.conn).await.unwrap_or(TokenInfo::NotPresent); + + // If neither are present, throw an error + if matches!(session_info, TokenInfo::NotPresent) && matches!(api_token_info, TokenInfo::NotPresent) { + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_UNAUTHORIZED".to_string(), + message: "This endpoint requires either a fully authenticated user or a token with the roles:create scope".to_string(), + path: None, + } + ], + }) + } + + // If both are present, throw an error + if matches!(session_info, TokenInfo::AuthToken(_)) && matches!(api_token_info, TokenInfo::ApiToken(_)) { + return HttpResponse::BadRequest().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_AMBIGUOUS_AUTHENTICATION".to_string(), + message: "Both a user token and an API token with the proper scope was provided. Please only provide one.".to_string(), + path: None + } + ], + }) + } + + let org = match api_token_info { + TokenInfo::ApiToken(tkn) => tkn.organization, + _ => { + // we have a session token, which means we have to do a db request to get the organization that this user owns + let user = match session_info { + TokenInfo::AuthToken(tkn) => tkn.session_info.user, + _ => unreachable!() + }; + + let org = match organization::Entity::find().filter(organization::Column::Owner.eq(user.id)).one(&db.conn).await { + Ok(r) => r, + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error performing the database request, please try again later.".to_string(), + path: None, + } + ], + }); + } + }; + + if let Some(org) = org { + org.id + } else { + return HttpResponse::Unauthorized().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_NO_ORG".to_string(), + message: "This user does not own any organizations. Try using an API token instead.".to_string(), + path: None + } + ], + }) + } + } + }; + + let existing_rules: Vec = match firewall_rule::Entity::find().filter(firewall_rule::Column::Role.eq(role.clone())).all(&db.conn).await { + Ok(r) => r, + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error performing the database request, please try again later.".to_string(), + path: None, + } + ], + }); + } + }; + + for rule in &existing_rules { + match rule.clone().delete(&db.conn).await { + Ok(r) => r, + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error performing the database request, please try again later.".to_string(), + path: None, + } + ], + }); + } + }; + } + + let new_role_model = role::Model { + id: role.into_inner(), + name: req.name.clone(), + description: req.description.clone(), + organization: org, + created_at: SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() as i64, + modified_at: SystemTime::now().duration_since(UNIX_EPOCH).expect("Time went backwards").as_secs() as i64, + }; + + let firewall_rules: Vec = req.firewall_rules.iter().map(|i| { + firewall_rule::Model { + id: random_id("rule"), + role: new_role_model.id.clone(), + protocol: i.protocol.to_string(), + description: i.description.clone(), + allowed_role_id: i.allowed_role_id.clone(), + port_range_from: i.port_range.as_ref().unwrap_or(&RolePortRange { from: 0, to: 65535 }).from as i32, + port_range_to: i.port_range.as_ref().unwrap_or(&RolePortRange { from: 0, to: 65535 }).to as i32, + } + }).collect(); + + let new_role_model_clone = new_role_model.clone(); + let firewall_rules_clone = firewall_rules.clone(); + + let new_role_active_model = new_role_model.into_active_model(); + match new_role_active_model.update(&db.conn).await { + Ok(_) => (), + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error creating the new role. Please try again later".to_string(), + path: None + } + ], + }) + } + } + + for rule in &firewall_rules_clone { + let active_model = rule.clone().into_active_model(); + match active_model.insert(&db.conn).await { + Ok(_) => (), + Err(e) => { + error!("database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorsResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error creating the new role. Please try again later".to_string(), + path: None + } + ], + }) + } + } + } + + HttpResponse::Ok().json(RoleCreateResponse { + data: RoleResponse { + id: Some(new_role_model_clone.id.clone()), + name: Some(new_role_model_clone.name.clone()), + description: Some(new_role_model_clone.description), + firewall_rules: req.firewall_rules.clone(), + created_at: Utc.timestamp_opt(new_role_model_clone.created_at, 0).unwrap().format("%Y-%m-%dT%H-%M-%S%.3fZ").to_string(), + modified_at: Utc.timestamp_opt(new_role_model_clone.modified_at, 0).unwrap().format("%Y-%m-%dT%H-%M-%S%.3fZ").to_string(), + }, + metadata: RoleCreateResponseMetadata {}, + }) +}