networks
This commit is contained in:
parent
90653796cc
commit
f54d8a11a1
|
@ -3473,13 +3473,16 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"actix-request-identifier",
|
"actix-request-identifier",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
|
"base64 0.21.0",
|
||||||
"chacha20poly1305",
|
"chacha20poly1305",
|
||||||
|
"chrono",
|
||||||
"hex",
|
"hex",
|
||||||
"log",
|
"log",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rand",
|
"rand",
|
||||||
"sea-orm",
|
"sea-orm",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_json",
|
||||||
"simple_logger",
|
"simple_logger",
|
||||||
"toml 0.7.3",
|
"toml 0.7.3",
|
||||||
"totp-rs",
|
"totp-rs",
|
||||||
|
|
|
@ -10,9 +10,10 @@ actix-web = "4" # Web framework
|
||||||
actix-request-identifier = "4" # Web framework
|
actix-request-identifier = "4" # Web framework
|
||||||
|
|
||||||
serde = { version = "1", features = ["derive"] } # Serialization and deserialization
|
serde = { version = "1", features = ["derive"] } # Serialization and deserialization
|
||||||
|
serde_json = "1.0.95" # Serialization and deserialization (cursors)
|
||||||
|
|
||||||
once_cell = "1" # Config
|
once_cell = "1" # Config
|
||||||
toml = "0.7" # Config
|
toml = "0.7" # Config / Serialization and deserialization
|
||||||
|
|
||||||
log = "0.4" # Logging
|
log = "0.4" # Logging
|
||||||
simple_logger = "4" # Logging
|
simple_logger = "4" # Logging
|
||||||
|
@ -24,6 +25,8 @@ trifid_api_entities = { version = "0.1.0", path = "trifid_api_entities" }
|
||||||
rand = "0.8" # Misc.
|
rand = "0.8" # Misc.
|
||||||
hex = "0.4" # Misc.
|
hex = "0.4" # Misc.
|
||||||
totp-rs = { version = "5.0.1", features = ["gen_secret", "otpauth"] } # Misc.
|
totp-rs = { version = "5.0.1", features = ["gen_secret", "otpauth"] } # Misc.
|
||||||
|
base64 = "0.21.0" # Misc.
|
||||||
|
chrono = "0.4.24" # Misc.
|
||||||
|
|
||||||
trifid-pki = { version = "0.1.9" } # Cryptography
|
trifid-pki = { version = "0.1.9" } # Cryptography
|
||||||
chacha20poly1305 = "0.10.1" # Cryptography
|
chacha20poly1305 = "0.10.1" # Cryptography
|
|
@ -12,7 +12,8 @@ use crate::timers::expired;
|
||||||
pub enum TokenInfo {
|
pub enum TokenInfo {
|
||||||
SessionToken(SessionTokenInfo),
|
SessionToken(SessionTokenInfo),
|
||||||
AuthToken(AuthTokenInfo),
|
AuthToken(AuthTokenInfo),
|
||||||
ApiToken(ApiTokenInfo)
|
ApiToken(ApiTokenInfo),
|
||||||
|
NotPresent
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct SessionTokenInfo {
|
pub struct SessionTokenInfo {
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use base64::Engine;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct Cursor {
|
||||||
|
pub page: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<Cursor> for String {
|
||||||
|
type Error = Box<dyn Error>;
|
||||||
|
|
||||||
|
fn try_from(value: Cursor) -> Result<Self, Self::Error> {
|
||||||
|
// Serialize it to json
|
||||||
|
let json_str = serde_json::to_string(&value)?;
|
||||||
|
// Then base64-encode the json
|
||||||
|
let base64_str = base64::engine::general_purpose::STANDARD.encode(json_str);
|
||||||
|
Ok(base64_str)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TryFrom<String> for Cursor {
|
||||||
|
type Error = Box<dyn Error>;
|
||||||
|
|
||||||
|
fn try_from(value: String) -> Result<Self, Self::Error> {
|
||||||
|
if value.is_empty() {
|
||||||
|
// If empty, it's page 0
|
||||||
|
return Ok(Cursor {
|
||||||
|
page: 0
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// Base64-decode the value
|
||||||
|
let json_bytes = base64::engine::general_purpose::STANDARD.decode(value)?;
|
||||||
|
// Convert it into a string
|
||||||
|
let json_str = String::from_utf8(json_bytes)?;
|
||||||
|
// Deserialize it from json
|
||||||
|
let cursor = serde_json::from_str(&json_str)?;
|
||||||
|
Ok(cursor)
|
||||||
|
}
|
||||||
|
}
|
|
@ -17,6 +17,7 @@ pub mod tokens;
|
||||||
pub mod timers;
|
pub mod timers;
|
||||||
pub mod magic_link;
|
pub mod magic_link;
|
||||||
pub mod auth_tokens;
|
pub mod auth_tokens;
|
||||||
|
pub mod cursor;
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub conn: DatabaseConnection
|
pub conn: DatabaseConnection
|
||||||
|
@ -68,6 +69,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
|
||||||
.service(routes::v1::totp_authenticators::totp_authenticators_request)
|
.service(routes::v1::totp_authenticators::totp_authenticators_request)
|
||||||
.service(routes::v1::verify_totp_authenticators::verify_totp_authenticators_request)
|
.service(routes::v1::verify_totp_authenticators::verify_totp_authenticators_request)
|
||||||
.service(routes::v1::auth::totp::totp_request)
|
.service(routes::v1::auth::totp::totp_request)
|
||||||
|
.service(routes::v1::networks::get_networks)
|
||||||
}).bind(CONFIG.server.bind)?.run().await?;
|
}).bind(CONFIG.server.bind)?.run().await?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
@ -1,4 +1,16 @@
|
||||||
use serde::{Serialize, Deserialize};
|
use serde::{Serialize, Deserialize};
|
||||||
|
use actix_web::{get, HttpRequest, HttpResponse};
|
||||||
|
use actix_web::web::{Data, Query};
|
||||||
|
use chacha20poly1305::consts::P1;
|
||||||
|
use chrono::{TimeZone, Utc};
|
||||||
|
use log::error;
|
||||||
|
use sea_orm::{ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, QueryOrder};
|
||||||
|
use crate::AppState;
|
||||||
|
use crate::auth_tokens::{enforce_2fa, enforce_api_token, enforce_session, TokenInfo};
|
||||||
|
use crate::error::{APIError, APIErrorsResponse};
|
||||||
|
use trifid_api_entities::entity::organization;
|
||||||
|
use trifid_api_entities::entity::network;
|
||||||
|
use crate::cursor::Cursor;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct GetNetworksResponse {
|
pub struct GetNetworksResponse {
|
||||||
|
@ -15,7 +27,7 @@ pub struct GetNetworksResponseData {
|
||||||
#[serde(rename = "signingCAID")]
|
#[serde(rename = "signingCAID")]
|
||||||
pub signing_ca_id: String,
|
pub signing_ca_id: String,
|
||||||
#[serde(rename = "createdAt")]
|
#[serde(rename = "createdAt")]
|
||||||
pub created_at: String, // 2023-03-22T18:55:47.009Z
|
pub created_at: String, // 2023-03-22T18:55:47.009Z, %Y-%m-%dT%H-%M-%S.%.3fZ
|
||||||
pub name: String,
|
pub name: String,
|
||||||
#[serde(rename = "lighthousesAsRelays")]
|
#[serde(rename = "lighthousesAsRelays")]
|
||||||
pub lighthouses_as_relays: bool
|
pub lighthouses_as_relays: bool
|
||||||
|
@ -24,23 +36,214 @@ pub struct GetNetworksResponseData {
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct GetNetworksResponseMetadata {
|
pub struct GetNetworksResponseMetadata {
|
||||||
#[serde(rename = "totalCount")]
|
#[serde(rename = "totalCount")]
|
||||||
pub total_count: i64,
|
pub total_count: u64,
|
||||||
#[serde(rename = "hasNextPage")]
|
#[serde(rename = "hasNextPage")]
|
||||||
pub has_next_page: bool,
|
pub has_next_page: bool,
|
||||||
#[serde(rename = "hasPrevPage")]
|
#[serde(rename = "hasPrevPage")]
|
||||||
pub has_prev_page: bool,
|
pub has_prev_page: bool,
|
||||||
#[serde(default, skip_serializing_if = "is_none", rename = "prevCursor")]
|
#[serde(default, rename = "prevCursor")]
|
||||||
pub prev_cursor: Option<String>,
|
pub prev_cursor: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "is_none", rename = "nextCursor")]
|
#[serde(default, rename = "nextCursor")]
|
||||||
pub next_cursor: Option<String>,
|
pub next_cursor: Option<String>,
|
||||||
#[serde(default, skip_serializing_if = "is_none")]
|
#[serde(default)]
|
||||||
pub page: Option<GetNetworksResponseMetadataPage>
|
pub page: Option<GetNetworksResponseMetadataPage>
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
pub struct GetNetworksResponseMetadataPage {
|
pub struct GetNetworksResponseMetadataPage {
|
||||||
pub count: i64,
|
pub count: u64,
|
||||||
pub start: i64
|
pub start: u64
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_none<T>(o: &Option<T>) -> bool { o.is_none() }
|
fn u64_25() -> u64 { 25 }
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
|
pub struct GetNetworksQueryParams {
|
||||||
|
#[serde(default, rename = "includeCounts")]
|
||||||
|
pub include_counts: bool,
|
||||||
|
#[serde(default)]
|
||||||
|
pub cursor: String,
|
||||||
|
#[serde(default = "u64_25", rename = "pageSize")]
|
||||||
|
pub page_size: u64
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/v1/networks")]
|
||||||
|
pub async fn get_networks(opts: Query<GetNetworksQueryParams>, req_info: HttpRequest, db: Data<AppState>) -> HttpResponse {
|
||||||
|
// For this endpoint, you either need to be a fully authenticated user OR a token with networks:list
|
||||||
|
let session_info = enforce_2fa(&req_info, &db.conn).await.unwrap_or(TokenInfo::NotPresent);
|
||||||
|
let api_token_info = enforce_api_token(&req_info, &["networks: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 networks: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 = network::Entity::find().filter(network::Column::Organization.eq(org)).order_by_asc(network::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 models_mapped = models.iter().map(|u| {
|
||||||
|
GetNetworksResponseData {
|
||||||
|
id: u.id.clone(),
|
||||||
|
cidr: u.cidr.clone(),
|
||||||
|
organization_id: u.organization.clone(),
|
||||||
|
signing_ca_id: u.signing_ca.clone(),
|
||||||
|
created_at: Utc.timestamp_opt(u.created_at, 0).unwrap().format("%Y-%m-%dT%H-%M-%S.%.3fZ").to_string(),
|
||||||
|
name: u.name.clone(),
|
||||||
|
lighthouses_as_relays: u.lighthouses_as_relays,
|
||||||
|
}
|
||||||
|
}).collect();
|
||||||
|
|
||||||
|
HttpResponse::Ok().json(GetNetworksResponse {
|
||||||
|
data: models_mapped,
|
||||||
|
metadata: GetNetworksResponseMetadata {
|
||||||
|
total_count: total,
|
||||||
|
has_next_page: cursor.page != 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 != pages {
|
||||||
|
match (Cursor { page: cursor.page + 1 }).try_into() {
|
||||||
|
Ok(r) => Some(r),
|
||||||
|
Err(_) => None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
|
page: if opts.include_counts {
|
||||||
|
Some(GetNetworksResponseMetadataPage {
|
||||||
|
count: opts.page_size,
|
||||||
|
start: opts.page_size * cursor.page,
|
||||||
|
})
|
||||||
|
} else { None },
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
Loading…
Reference in New Issue