// trifid-api, an open source reimplementation of the Defined Networking nebula management server. // Copyright (C) 2023 c0repwn3r // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . // //#POST /v1/roles t+parity:full t+type:documented t+status:done t+feature:definednetworking // 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. // This endpoint requires the `definednetworking` extension to be enabled to be used. // //#GET /v1/roles t+parity:full t+type:documented t+status:done t+feature:definednetworking // 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. // This endpoint requires the `definednetworking` extension to be enabled to be used. // //#GET /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. // //#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 crate::auth_tokens::{enforce_2fa, enforce_api_token, TokenInfo}; use crate::cursor::Cursor; use crate::error::{APIError, APIErrorsResponse}; use crate::timers::TIME_FORMAT; use crate::tokens::random_id; use crate::AppState; use actix_web::delete; use actix_web::web::{Data, Json, Path, Query}; use actix_web::{get, post, put, HttpRequest, HttpResponse}; use chrono::{TimeZone, Utc}; use log::error; use sea_orm::ActiveValue::Set; use sea_orm::{ ActiveModelTrait, ColumnTrait, EntityTrait, IntoActiveModel, ModelTrait, PaginatorTrait, QueryFilter, QueryOrder, }; use serde::{Deserialize, Serialize}; use std::time::{SystemTime, UNIX_EPOCH}; use trifid_api_entities::entity::firewall_rule; use trifid_api_entities::entity::organization; use trifid_api_entities::entity::role; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct CreateRoleRequest { #[serde(default)] pub name: String, #[serde(default)] pub description: String, #[serde(default, rename = "firewallRules")] 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, #[serde(default)] pub description: String, #[serde(rename = "allowedRoleID")] pub allowed_role_id: Option, // Option is intentional here to prevent having to convert it anyway for SeaORM's types #[serde(rename = "portRange")] pub port_range: Option, // Option is intentional here, because we handle the null case in a way other than just "default 0" } #[derive(Serialize, Deserialize, Debug, Clone)] pub enum RoleProtocol { #[serde(rename = "ANY")] Any, #[serde(rename = "TCP")] Tcp, #[serde(rename = "UDP")] Udp, #[serde(rename = "ICMP")] Icmp, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct RolePortRange { pub from: u16, pub to: u16, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct RoleCreateResponse { pub data: RoleResponse, pub metadata: RoleCreateResponseMetadata, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct RoleResponse { pub id: Option, pub name: Option, pub description: Option, #[serde(rename = "firewallRules")] pub firewall_rules: Vec, #[serde(rename = "createdAt")] pub created_at: String, #[serde(rename = "modifiedAt")] pub modified_at: String, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct RoleCreateResponseMetadata {} #[post("/v1/roles")] pub async fn create_role_request( 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 role: Option = match role::Entity::find() .filter(role::Column::Name.eq(&req.name)) .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 role.is_some() { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_DUPLICATE_VALUE".to_string(), message: "value already exists".to_string(), path: Some("name".to_string()) } ] }) } for (id, rule) in req.firewall_rules.iter().enumerate() { if let Some(pr) = &rule.port_range { if pr.from < pr.to { return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![ APIError { code: "ERR_INVALID_VALUE".to_string(), message: "from must be less than or equal to to".to_string(), path: Some(format!("firewallRules[{}].portRange", id)) } ] }); } } } let new_role_model = role::Model { id: random_id("role"), 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.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, }], }); } } 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(TIME_FORMAT) .to_string(), modified_at: Utc .timestamp_opt(new_role_model_clone.modified_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), }, metadata: RoleCreateResponseMetadata {}, }) } impl ToString for RoleProtocol { fn to_string(&self) -> String { match self { RoleProtocol::Any => "ANY".to_string(), RoleProtocol::Tcp => "TCP".to_string(), RoleProtocol::Udp => "UDP".to_string(), RoleProtocol::Icmp => "ICMP".to_string(), } } } #[derive(Serialize, Deserialize)] pub struct ListRolesRequestOpts { #[serde(default, rename = "includeCounts")] pub include_counts: bool, #[serde(default)] pub cursor: String, #[serde(default = "page_default", rename = "pageSize")] pub page_size: u64, } fn page_default() -> u64 { 25 } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct GetRolesResponse { pub data: Vec, pub metadata: GetRolesResponseMetadata, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct GetRolesResponseMetadata { #[serde(rename = "totalCount")] pub total_count: u64, #[serde(rename = "hasNextPage")] pub has_next_page: bool, #[serde(rename = "hasPrevPage")] pub has_prev_page: bool, #[serde(default, rename = "prevCursor")] pub prev_cursor: Option, #[serde(default, rename = "nextCursor")] pub next_cursor: Option, #[serde(default)] pub page: Option, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct GetRolesResponseMetadataPage { pub count: u64, pub start: u64, } #[get("/v1/roles")] pub async fn get_roles( opts: Query, req_info: HttpRequest, db: Data, ) -> HttpResponse { // For this endpoint, you either need to be a fully authenticated user OR a token with roles:list let session_info = enforce_2fa(&req_info, &db.conn) .await .unwrap_or(TokenInfo::NotPresent); let api_token_info = enforce_api_token(&req_info, &["roles:list"], &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:list 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 cursor: Cursor = match opts.cursor.clone().try_into() { Ok(r) => r, Err(e) => { error!("invalid cursor: {}", e); return HttpResponse::BadRequest().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_INVALID_CURSOR".to_string(), message: "The provided cursor was invalid, please try again later.".to_string(), path: None, }], }); } }; let network_pages = role::Entity::find() .filter(role::Column::Organization.eq(org)) .order_by_asc(role::Column::CreatedAt) .paginate(&db.conn, opts.page_size); let total = match network_pages.num_items().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 pages = match network_pages.num_pages().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 models = match network_pages.fetch_page(cursor.page).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 mut models_mapped: Vec = vec![]; for u in models { // fetch firewall rules let rules = match firewall_rule::Entity::find() .filter(firewall_rule::Column::Role.eq(&u.id)) .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, } ], }); } }; let rules: Vec = rules .iter() .map(|r| { let protocol = match r.protocol.as_str() { "ANY" => RoleProtocol::Any, "TCP" => RoleProtocol::Tcp, "UDP" => RoleProtocol::Udp, "ICMP" => RoleProtocol::Icmp, _ => unreachable!("database has been corrupted or manually edited"), }; let port_range = if r.port_range_from == 0 && r.port_range_to == 65535 || matches!(protocol, RoleProtocol::Icmp) { None } else { Some(RolePortRange { from: r.port_range_from as u16, to: r.port_range_to as u16, }) }; RoleFirewallRule { protocol, description: r.description.clone(), allowed_role_id: r.allowed_role_id.clone(), port_range, } }) .collect(); models_mapped.push(RoleResponse { id: Some(u.id.clone()), name: Some(u.name), description: Some(u.description), firewall_rules: rules, created_at: Utc .timestamp_opt(u.created_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), modified_at: Utc .timestamp_opt(u.modified_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), }) } let count = models_mapped.len() as u64; HttpResponse::Ok().json(GetRolesResponse { data: models_mapped, metadata: GetRolesResponseMetadata { total_count: total, has_next_page: cursor.page + 1 != pages, has_prev_page: cursor.page != 0, prev_cursor: if cursor.page != 0 { match (Cursor { page: cursor.page - 1, }) .try_into() { Ok(r) => Some(r), Err(_) => None, } } else { None }, next_cursor: if cursor.page + 1 != pages { match (Cursor { page: cursor.page + 1, }) .try_into() { Ok(r) => Some(r), Err(_) => None, } } else { None }, page: if opts.include_counts { Some(GetRolesResponseMetadataPage { count, start: opts.page_size * cursor.page, }) } else { None }, }, }) } #[get("/v1/roles/{role_id}")] pub async fn get_role( net: Path, req_info: HttpRequest, db: Data, ) -> HttpResponse { // For this endpoint, you either need to be a fully authenticated user OR a token with roles:read let session_info = enforce_2fa(&req_info, &db.conn) .await .unwrap_or(TokenInfo::NotPresent); let api_token_info = enforce_api_token(&req_info, &["roles:read"], &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:read 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 role: Option = match role::Entity::find() .filter(role::Column::Id.eq(net.into_inner())) .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(role) = role { // fetch firewall rules let rules = match firewall_rule::Entity::find() .filter(firewall_rule::Column::Role.eq(&role.id)) .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, } ], }); } }; let rules: Vec = rules .iter() .map(|r| { let protocol = match r.protocol.as_str() { "ANY" => RoleProtocol::Any, "TCP" => RoleProtocol::Tcp, "UDP" => RoleProtocol::Udp, "ICMP" => RoleProtocol::Icmp, _ => unreachable!("database has been corrupted or manually edited"), }; let port_range = if r.port_range_from == 0 && r.port_range_to == 65535 || matches!(protocol, RoleProtocol::Icmp) { None } else { Some(RolePortRange { from: r.port_range_from as u16, to: r.port_range_to as u16, }) }; RoleFirewallRule { protocol, description: r.description.clone(), allowed_role_id: r.allowed_role_id.clone(), port_range, } }) .collect(); HttpResponse::Ok().json(GetRoleResponse { data: RoleResponse { id: Some(role.id.clone()), name: Some(role.name.clone()), description: Some(role.description.clone()), firewall_rules: rules, created_at: Utc .timestamp_opt(role.created_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), modified_at: Utc .timestamp_opt(role.modified_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), }, metadata: GetRoleResponseMetadata {}, }) } else { HttpResponse::NotFound().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_NOT_FOUND".to_string(), message: "resource not found".to_string(), path: None, }], }) } } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct GetRoleResponse { pub data: RoleResponse, pub metadata: GetRoleResponseMetadata, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct GetRoleResponseMetadata {} #[delete("/v1/roles/{role_id}")] 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); // 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:delete 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 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); 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(role) = role { match role.delete(&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 performing the database request, please try again later.".to_string(), path: None, } ], }); } }; HttpResponse::Ok().json(RoleDeleteResponse { data: RoleDeleteResponseData {}, metadata: RoleDeleteResponseMetadata {}, }) } else { HttpResponse::NotFound().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_NOT_FOUND".to_string(), message: "resource not found".to_string(), path: None, }], }) } } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct RoleDeleteResponse { data: RoleDeleteResponseData, metadata: RoleDeleteResponseMetadata, } #[derive(Serialize, Deserialize, Debug, Clone)] pub struct RoleDeleteResponseData {} #[derive(Serialize, Deserialize, Debug, Clone)] pub struct RoleDeleteResponseMetadata {} #[derive(Serialize, Deserialize)] pub struct RoleUpdateRequest { pub description: String, #[serde(rename = "firewallRules")] pub firewall_rules: Vec, } #[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 } ], }); } match api_token_info { TokenInfo::ApiToken(_) => (), _ => { // 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 org.is_none() { 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 role = match role::Entity::find() .filter(role::Column::Id.eq(role.as_str())) .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, } ], }); } }; let role = match role { Some(r) => r, None => { return HttpResponse::NotFound().json(APIErrorsResponse { errors: vec![APIError { code: "ERR_NOT_FOUND".to_string(), message: "This resource does not exist or you do not have permission to access it." .to_string(), path: None, }], }) } }; let existing_rules: Vec = match firewall_rule::Entity::find() .filter(firewall_rule::Column::Role.eq(role.id.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 mut role_active_model = role.clone().into_active_model(); role_active_model.modified_at = Set(SystemTime::now() .duration_since(UNIX_EPOCH) .expect("Time went backwards") .as_secs() as i64); role_active_model.description = Set(req.description.clone()); let role = match role_active_model.update(&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 firewall_rules: Vec = req .firewall_rules .iter() .map(|i| firewall_rule::Model { id: random_id("rule"), role: role.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 firewall_rules_clone = firewall_rules.clone(); 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(role.id.clone()), name: Some(role.name.clone()), description: Some(role.description), firewall_rules: req.firewall_rules.clone(), created_at: Utc .timestamp_opt(role.created_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), modified_at: Utc .timestamp_opt(role.modified_at, 0) .unwrap() .format(TIME_FORMAT) .to_string(), }, metadata: RoleCreateResponseMetadata {}, }) }