This commit is contained in:
core 2023-04-03 18:39:49 -04:00
parent 90653796cc
commit f54d8a11a1
Signed by: core
GPG Key ID: FDBF740DADDCEECF
6 changed files with 262 additions and 10 deletions

3
Cargo.lock generated
View File

@ -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",

View File

@ -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

View File

@ -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 {

40
trifid-api/src/cursor.rs Normal file
View File

@ -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)
}
}

View File

@ -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(())

View File

@ -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 },
},
})
}