networks
This commit is contained in:
parent
90653796cc
commit
f54d8a11a1
|
@ -3473,13 +3473,16 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"actix-request-identifier",
|
||||
"actix-web",
|
||||
"base64 0.21.0",
|
||||
"chacha20poly1305",
|
||||
"chrono",
|
||||
"hex",
|
||||
"log",
|
||||
"once_cell",
|
||||
"rand",
|
||||
"sea-orm",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"simple_logger",
|
||||
"toml 0.7.3",
|
||||
"totp-rs",
|
||||
|
|
|
@ -10,9 +10,10 @@ actix-web = "4" # Web framework
|
|||
actix-request-identifier = "4" # Web framework
|
||||
|
||||
serde = { version = "1", features = ["derive"] } # Serialization and deserialization
|
||||
serde_json = "1.0.95" # Serialization and deserialization (cursors)
|
||||
|
||||
once_cell = "1" # Config
|
||||
toml = "0.7" # Config
|
||||
toml = "0.7" # Config / Serialization and deserialization
|
||||
|
||||
log = "0.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.
|
||||
hex = "0.4" # 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
|
||||
chacha20poly1305 = "0.10.1" # Cryptography
|
|
@ -12,7 +12,8 @@ use crate::timers::expired;
|
|||
pub enum TokenInfo {
|
||||
SessionToken(SessionTokenInfo),
|
||||
AuthToken(AuthTokenInfo),
|
||||
ApiToken(ApiTokenInfo)
|
||||
ApiToken(ApiTokenInfo),
|
||||
NotPresent
|
||||
}
|
||||
|
||||
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 magic_link;
|
||||
pub mod auth_tokens;
|
||||
pub mod cursor;
|
||||
|
||||
pub struct AppState {
|
||||
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::verify_totp_authenticators::verify_totp_authenticators_request)
|
||||
.service(routes::v1::auth::totp::totp_request)
|
||||
.service(routes::v1::networks::get_networks)
|
||||
}).bind(CONFIG.server.bind)?.run().await?;
|
||||
|
||||
Ok(())
|
||||
|
|
|
@ -1,4 +1,16 @@
|
|||
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)]
|
||||
pub struct GetNetworksResponse {
|
||||
|
@ -15,7 +27,7 @@ pub struct GetNetworksResponseData {
|
|||
#[serde(rename = "signingCAID")]
|
||||
pub signing_ca_id: String,
|
||||
#[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,
|
||||
#[serde(rename = "lighthousesAsRelays")]
|
||||
pub lighthouses_as_relays: bool
|
||||
|
@ -24,23 +36,214 @@ pub struct GetNetworksResponseData {
|
|||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct GetNetworksResponseMetadata {
|
||||
#[serde(rename = "totalCount")]
|
||||
pub total_count: i64,
|
||||
pub total_count: u64,
|
||||
#[serde(rename = "hasNextPage")]
|
||||
pub has_next_page: bool,
|
||||
#[serde(rename = "hasPrevPage")]
|
||||
pub has_prev_page: bool,
|
||||
#[serde(default, skip_serializing_if = "is_none", rename = "prevCursor")]
|
||||
#[serde(default, rename = "prevCursor")]
|
||||
pub prev_cursor: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_none", rename = "nextCursor")]
|
||||
#[serde(default, rename = "nextCursor")]
|
||||
pub next_cursor: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "is_none")]
|
||||
#[serde(default)]
|
||||
pub page: Option<GetNetworksResponseMetadataPage>
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
pub struct GetNetworksResponseMetadataPage {
|
||||
pub count: i64,
|
||||
pub start: i64
|
||||
pub count: u64,
|
||||
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