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
}