1202 lines
42 KiB
Rust
1202 lines
42 KiB
Rust
// 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 <https://www.gnu.org/licenses/>.
|
|
//
|
|
//#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<RoleFirewallRule>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub struct UpdateRoleRequest {
|
|
pub name: Option<String>,
|
|
#[serde(default)]
|
|
pub description: String,
|
|
#[serde(default, rename = "firewallRules")]
|
|
pub firewall_rules: Vec<RoleFirewallRule>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub struct RoleFirewallRule {
|
|
pub protocol: RoleProtocol,
|
|
#[serde(default)]
|
|
pub description: String,
|
|
#[serde(rename = "allowedRoleID")]
|
|
pub allowed_role_id: Option<String>, // Option is intentional here to prevent having to convert it anyway for SeaORM's types
|
|
#[serde(rename = "portRange")]
|
|
pub port_range: Option<RolePortRange>, // 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<String>,
|
|
pub name: Option<String>,
|
|
pub description: Option<String>,
|
|
#[serde(rename = "firewallRules")]
|
|
pub firewall_rules: Vec<RoleFirewallRule>,
|
|
#[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<CreateRoleRequest>,
|
|
req_info: HttpRequest,
|
|
db: Data<AppState>,
|
|
) -> 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<role::Model> = 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<firewall_rule::Model> = 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<RoleResponse>,
|
|
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<String>,
|
|
#[serde(default, rename = "nextCursor")]
|
|
pub next_cursor: Option<String>,
|
|
#[serde(default)]
|
|
pub page: Option<GetRolesResponseMetadataPage>,
|
|
}
|
|
|
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
|
pub struct GetRolesResponseMetadataPage {
|
|
pub count: u64,
|
|
pub start: u64,
|
|
}
|
|
|
|
#[get("/v1/roles")]
|
|
pub async fn get_roles(
|
|
opts: Query<ListRolesRequestOpts>,
|
|
req_info: HttpRequest,
|
|
db: Data<AppState>,
|
|
) -> 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<RoleResponse> = 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<RoleFirewallRule> = 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<String>,
|
|
req_info: HttpRequest,
|
|
db: Data<AppState>,
|
|
) -> 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<role::Model> = 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<RoleFirewallRule> = 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<String>,
|
|
req_info: HttpRequest,
|
|
db: Data<AppState>,
|
|
) -> 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<role::Model> = 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<RoleFirewallRule>,
|
|
}
|
|
|
|
#[put("/v1/roles/{role_id}")]
|
|
pub async fn update_role_request(
|
|
role: Path<String>,
|
|
req: Json<RoleUpdateRequest>,
|
|
req_info: HttpRequest,
|
|
db: Data<AppState>,
|
|
) -> 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<firewall_rule::Model> = 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<firewall_rule::Model> = 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 {},
|
|
})
|
|
}
|