diff --git a/trifid-api/src/routes/v1/mod.rs b/trifid-api/src/routes/v1/mod.rs index 248e152..8b72c27 100644 --- a/trifid-api/src/routes/v1/mod.rs +++ b/trifid-api/src/routes/v1/mod.rs @@ -3,4 +3,5 @@ pub mod signup; pub mod totp_authenticators; pub mod verify_totp_authenticators; pub mod networks; -pub mod organization; \ No newline at end of file +pub mod organization; +pub mod roles; \ No newline at end of file diff --git a/trifid-api/src/routes/v1/roles.rs b/trifid-api/src/routes/v1/roles.rs new file mode 100644 index 0000000..85768c9 --- /dev/null +++ b/trifid-api/src/routes/v1/roles.rs @@ -0,0 +1,165 @@ +// 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:in_progress +// This endpoint has full parity with the original API. It has been recreated from the original API documentation. +// This endpoint is in-progress and should not be expected to work, sometimes at all. + +use actix_web::{HttpRequest, HttpResponse, post}; +use actix_web::web::{Data, Json}; +use log::error; +use serde::{Deserialize, Serialize}; +use crate::AppState; +use crate::auth_tokens::{enforce_2fa, enforce_api_token, TokenInfo}; +use crate::error::{APIError, APIErrorsResponse}; +use trifid_api_entities::entity::organization; +use sea_orm::{EntityTrait, QueryFilter, ColumnTrait}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct CreateRoleRequest { + pub name: String, + #[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: RoleCreateResponseData, + pub metadata: RoleCreateResponseMetadata +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct RoleCreateResponseData { + 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 + } + ], + }) + } + } + }; + + HttpResponse::Ok().finish() +} \ No newline at end of file diff --git a/trifid-api/trifid_api_entities/src/entity/role.rs b/trifid-api/trifid_api_entities/src/entity/role.rs index aa3f61f..b31b820 100644 --- a/trifid-api/trifid_api_entities/src/entity/role.rs +++ b/trifid-api/trifid_api_entities/src/entity/role.rs @@ -7,9 +7,12 @@ use sea_orm::entity::prelude::*; pub struct Model { #[sea_orm(primary_key, auto_increment = false)] pub id: String, + #[sea_orm(unique)] pub name: String, pub description: String, pub organization: String, + pub created_at: i64, + pub modified_at: i64, } #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] diff --git a/trifid-api/trifid_api_migration/src/m20230404_133809_create_table_roles.rs b/trifid-api/trifid_api_migration/src/m20230404_133809_create_table_roles.rs index 93dc9ad..1cecc8a 100644 --- a/trifid-api/trifid_api_migration/src/m20230404_133809_create_table_roles.rs +++ b/trifid-api/trifid_api_migration/src/m20230404_133809_create_table_roles.rs @@ -11,9 +11,11 @@ impl MigrationTrait for Migration { Table::create() .table(Role::Table) .col(ColumnDef::new(Role::Id).string().not_null().primary_key()) - .col(ColumnDef::new(Role::Name).string().not_null()) + .col(ColumnDef::new(Role::Name).string().not_null().unique_key()) .col(ColumnDef::new(Role::Description).string().not_null()) .col(ColumnDef::new(Role::Organization).string().not_null()) + .col(ColumnDef::new(Role::CreatedAt).big_integer().not_null()) + .col(ColumnDef::new(Role::ModifiedAt).big_integer().not_null()) .foreign_key( ForeignKey::create() .from(Role::Table, Role::Organization) @@ -36,5 +38,7 @@ pub enum Role { Id, Name, Description, - Organization + Organization, + CreatedAt, + ModifiedAt }