use serde::Serialize;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
use crate::colors::{color_primary, color_warn};
use crate::error::ApiError;
use crate::orchestrator::enroll::auth0::UserInfo;
use crate::orchestrator::project::models::ProjectModel;
use crate::orchestrator::share::RoleInShare;
use crate::output::Output;
use crate::terminal::fmt;
use crate::{ConnectionStatus, TransportRouteResolver};
use ockam::identity::{Identifier, Identity, Vault};
use ockam_core::compat::collections::HashSet;
use ockam_core::errcode::{Kind, Origin};
use ockam_core::{Error, Result};
use ockam_multiaddr::MultiAddr;
use ockam_node::tokio;
#[derive(Debug, Clone, Serialize)]
pub struct Project {
#[serde(flatten)]
model: ProjectModel,
#[serde(skip)]
project_identity: Option<Identity>,
#[serde(skip)]
project_multiaddr: Option<MultiAddr>,
#[serde(rename = "access_address")]
project_socket_addr: Option<String>,
#[serde(skip)]
authority_identity: Option<Identity>,
#[serde(skip)]
authority_multiaddr: Option<MultiAddr>,
#[serde(rename = "authority_access_address")]
authority_socket_addr: Option<String>,
egress_allow_list: Vec<String>,
}
impl Project {
pub async fn import(model: ProjectModel) -> Result<Self> {
let project_identity = match &model.project_change_history {
Some(project_change_history) => Some(
Identity::import_from_string(
model.identity.as_ref(),
project_change_history.as_str(),
Vault::create_verifying_vault(),
)
.await?,
),
None => None,
};
let mut egress_allow_list = HashSet::new();
let project_socket_addr;
let project_multiaddr;
if model.access_route.is_empty() {
project_socket_addr = None;
project_multiaddr = None;
} else {
let multiaddr = MultiAddr::from_str(&model.access_route)
.map_err(|e| ApiError::core(e.to_string()))?;
let socket_addr = TransportRouteResolver::default()
.allow_tcp()
.socket_address(&multiaddr)?;
project_socket_addr = Some(socket_addr.clone());
egress_allow_list.insert(socket_addr);
project_multiaddr = Some(multiaddr);
}
let authority_identity = match &model.authority_identity {
Some(authority_change_history) => Some(
Identity::import_from_string(
None,
authority_change_history.as_str(),
Vault::create_verifying_vault(),
)
.await?,
),
None => None,
};
let authority_socket_addr;
let authority_multiaddr;
match &model.authority_access_route {
Some(authority_access_route) => {
let multiaddr = MultiAddr::from_str(authority_access_route)
.map_err(|e| ApiError::core(e.to_string()))?;
let socket_addr = TransportRouteResolver::default()
.allow_tcp()
.socket_address(&multiaddr)?;
authority_socket_addr = Some(socket_addr.clone());
egress_allow_list.insert(socket_addr);
authority_multiaddr = Some(multiaddr)
}
None => {
authority_socket_addr = None;
authority_multiaddr = None;
}
};
let s = Self {
model,
project_identity,
project_multiaddr,
project_socket_addr,
authority_identity,
authority_multiaddr,
authority_socket_addr,
egress_allow_list: egress_allow_list.into_iter().collect(),
};
Ok(s)
}
pub fn model(&self) -> &ProjectModel {
&self.model
}
pub fn name(&self) -> &str {
self.model.name.as_str()
}
pub fn project_id(&self) -> &str {
self.model.id.as_str()
}
pub fn project_identity(&self) -> Option<&Identity> {
self.project_identity.as_ref()
}
pub fn project_identifier(&self) -> Option<Identifier> {
self.model.identity.clone()
}
pub fn project_multiaddr(&self) -> Result<&MultiAddr> {
match &self.project_multiaddr {
Some(project_multiaddr) => Ok(project_multiaddr),
None => Err(Error::new(
Origin::Api,
Kind::NotFound,
format!(
"no project multiaddr has been set for the project {}",
self.model.name
),
)),
}
}
pub fn project_name(&self) -> &str {
self.model.name.as_str()
}
pub fn authority_identity(&self) -> Option<&Identity> {
self.authority_identity.as_ref()
}
pub fn authority_identifier(&self) -> Option<Identifier> {
self.authority_identity().map(|i| i.identifier().clone())
}
pub fn authority_socket_addr(&self) -> Option<&String> {
self.authority_socket_addr.as_ref()
}
pub fn authority_multiaddr(&self) -> Result<&MultiAddr> {
match &self.authority_multiaddr {
Some(authority_multiaddr) => Ok(authority_multiaddr),
None => Err(Error::new(
Origin::Api,
Kind::NotFound,
format!(
"no authority route has been configured for the project {}",
self.model.name
),
)),
}
}
pub fn space_id(&self) -> &str {
&self.model.space_id
}
pub fn space_name(&self) -> &str {
&self.model.space_name
}
}
impl Project {
pub fn is_admin(&self, user: &UserInfo) -> bool {
self.model
.user_roles
.iter()
.any(|ur| ur.role == RoleInShare::Admin && ur.email == user.email)
}
pub fn is_ready(&self) -> bool {
self.project_multiaddr.is_some()
&& self.project_identity.is_some()
&& self.authority_multiaddr.is_some()
&& self.authority_identity.is_some()
}
pub async fn try_connect_tcp(&self) -> Result<bool> {
match &self.project_socket_addr {
None => Ok(false),
Some(project_socket_addr) => Ok(tokio::net::TcpStream::connect(project_socket_addr)
.await
.is_ok()),
}
}
pub fn override_name(&mut self, new_name: String) {
self.model.name = new_name;
}
}
impl Display for Project {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", color_primary(self.name()))?;
writeln!(f, ":")?;
let status = ConnectionStatus::from(self.model().running.unwrap_or(false));
writeln!(f, "{}The project is {status}", fmt::INDENTATION,)?;
writeln!(f, "{}Id: {}", fmt::INDENTATION, self.project_id())?;
writeln!(
f,
"{}Space: {}",
fmt::INDENTATION,
color_warn(self.space_name())
)?;
writeln!(
f,
"{}Route: {}",
fmt::INDENTATION,
color_primary(
self.project_multiaddr()
.map(|m| m.to_string())
.unwrap_or("N/A".to_string())
)
)?;
writeln!(
f,
"{}Identifier: {}",
fmt::INDENTATION,
self.project_identifier()
.map(|i| i.to_string())
.unwrap_or("N/A".to_string())
)?;
writeln!(
f,
"{}Authority route: {}",
fmt::INDENTATION,
self.authority_multiaddr()
.map(|m| m.to_string())
.unwrap_or("N/A".to_string())
)?;
writeln!(
f,
"{}Authority identifier: {}",
fmt::INDENTATION,
self.authority_identifier()
.map(|i| i.to_string())
.unwrap_or("N/A".to_string())
)?;
writeln!(
f,
"{}Egress allow list: {}",
fmt::INDENTATION,
color_primary(self.egress_allow_list.join(", "))
)?;
writeln!(
f,
"{}Version: {}",
fmt::INDENTATION,
self.model().version.as_deref().unwrap_or("N/A")
)?;
Ok(())
}
}
impl Output for Project {
fn item(&self) -> crate::Result<String> {
Ok(self.padded_display())
}
}
#[cfg(test)]
mod tests {
use crate::orchestrator::enroll::auth0::UserInfo;
use crate::orchestrator::project::models::{ProjectModel, ProjectUserRole};
use crate::orchestrator::project::Project;
use crate::orchestrator::share::{RoleInShare, ShareScope};
use quickcheck::{Arbitrary, Gen};
#[tokio::test]
async fn convert_access_route_to_socket_addr() {
let mut g = Gen::new(100);
let mut p = ProjectModel::arbitrary(&mut g);
p.access_route = "/dnsaddr/node.dnsaddr.com/tcp/4000/service/api".into();
p.authority_access_route = None;
let p = Project::import(p).await.unwrap();
let socket_addr = p.project_socket_addr;
assert_eq!(socket_addr, Some("node.dnsaddr.com:4000".to_string()));
}
#[tokio::test]
async fn test_is_admin() {
let mut g = Gen::new(100);
let mut project = ProjectModel::arbitrary(&mut g);
project.access_route = "".to_string();
project.authority_access_route = None;
project.user_roles = vec![create_admin("test@ockam.io")];
let project = Project::import(project).await.unwrap();
assert!(project.is_admin(&create_user("test@ockam.io")));
assert!(project.is_admin(&create_user("tEst@ockam.io")));
assert!(project.is_admin(&create_user("test@Ockam.io")));
assert!(project.is_admin(&create_user("TEST@OCKAM.IO")));
}
fn create_admin(email: &str) -> ProjectUserRole {
ProjectUserRole {
email: email.try_into().unwrap(),
id: 1,
role: RoleInShare::Admin,
scope: ShareScope::Project,
}
}
fn create_user(email: &str) -> UserInfo {
UserInfo {
sub: "name".to_string(),
nickname: "nickname".to_string(),
name: "name".to_string(),
picture: "picture".to_string(),
updated_at: "noon".to_string(),
email: email.try_into().unwrap(),
email_verified: false,
}
}
}