use colorful::Colorful;
use ockam::identity::models::ChangeHistory;
use ockam::identity::{Identifier, Identity};
use ockam_core::errcode::{Kind, Origin};
use ockam_core::Error;
use ockam_vault::{HandleToSecret, SigningSecretKeyHandle};
use crate::cli_state::{random_name, CliState, Result};
use crate::colors::color_primary;
use crate::{fmt_log, fmt_ok};
impl CliState {
#[instrument(skip_all, fields(name = %name, vault_name = %vault_name))]
pub async fn create_identity_with_name_and_vault(
&self,
name: &str,
vault_name: &str,
) -> Result<NamedIdentity> {
if let Ok(identity) = self.get_named_identity(name).await {
return Ok(identity);
};
let vault = self.get_named_vault(vault_name).await?;
let identities = self.make_identities(self.make_vault(vault).await?).await?;
let identity = identities.identities_creation().create_identity().await?;
let named_identity = self
.store_named_identity(&identity, name, vault_name)
.await?;
self.notify_message(fmt_ok!(
"Generated a new Identity named {}.",
color_primary(named_identity.name())
));
self.notify_message(fmt_log!(
"{} has Identifier {}",
color_primary(named_identity.name()),
color_primary(named_identity.identifier().to_string())
));
if named_identity.is_default() {
self.notify_message(fmt_ok!(
"Marked {} as your default Identity, on this machine.\n",
color_primary(named_identity.name())
));
}
Ok(named_identity)
}
#[instrument(skip_all, fields(name = %name))]
pub async fn create_identity_with_name(&self, name: &str) -> Result<NamedIdentity> {
let vault = self.get_or_create_default_named_vault().await?;
self.create_identity_with_name_and_vault(name, &vault.name())
.await
}
#[instrument(skip_all, fields(name = %name, vault_name = %vault_name, key_id = %key_id))]
pub async fn create_identity_with_key_id(
&self,
name: &str,
vault_name: &str,
key_id: &str,
) -> Result<NamedIdentity> {
let vault = self.get_named_vault(vault_name).await?;
if !vault.use_aws_kms() {
return Err(Error::new(
Origin::Api,
Kind::Misuse,
format!("Vault {vault_name} is not a KMS vault"),
))?;
};
let handle = SigningSecretKeyHandle::ECDSASHA256CurveP256(HandleToSecret::new(
key_id.as_bytes().to_vec(),
));
let identities = self.make_identities(self.make_vault(vault).await?).await?;
let identifier = identities
.identities_creation()
.identity_builder()
.with_existing_key(handle)
.build()
.await?
.clone();
self.store_named_identity(&identifier, name, vault_name)
.await
}
}
impl CliState {
#[instrument(skip_all)]
pub async fn get_named_identities(&self) -> Result<Vec<NamedIdentity>> {
Ok(self.identities_repository().get_named_identities().await?)
}
#[instrument(skip_all, fields(name = %name))]
pub async fn get_named_identity(&self, name: &str) -> Result<NamedIdentity> {
let repository = self.identities_repository();
match repository.get_named_identity(name).await? {
Some(identity) => Ok(identity),
None => {
let error_message = format!(
"{} {} {}.\n{} {}\n",
"There is no Identity with name",
color_primary(name),
"on this machine",
"To see a list of Identities on this machine, please run",
color_primary("ockam identity list")
);
Err(Error::new(Origin::Api, Kind::NotFound, error_message))?
}
}
}
#[instrument(skip_all, fields(name = name.clone()))]
pub async fn get_named_identity_or_default(
&self,
name: &Option<String>,
) -> Result<NamedIdentity> {
match name {
Some(name) => self.get_named_identity(name).await,
None => self.get_or_create_default_named_identity().await,
}
}
pub async fn get_identifier_by_name(&self, name: &str) -> Result<Identifier> {
Ok(self.get_named_identity(name).await?.identifier())
}
#[instrument(skip_all, fields(name = name.clone()))]
pub async fn get_identifier_by_optional_name(
&self,
name: &Option<String>,
) -> Result<Identifier> {
let repository = self.identities_repository();
let result = match name {
Some(name) => repository.get_identifier(name).await?,
None => repository
.get_default_named_identity()
.await?
.map(|i| i.identifier()),
};
Ok(result.ok_or_else(|| Self::missing_identifier(name))?)
}
#[instrument(skip_all, fields(name = name.clone()))]
pub async fn get_identity_by_optional_name(&self, name: &Option<String>) -> Result<Identity> {
let named_identity = match name {
Some(name) => {
self.identities_repository()
.get_named_identity(name)
.await?
}
None => {
self.identities_repository()
.get_default_named_identity()
.await?
}
};
match named_identity {
Some(identity) => {
let change_history = self.get_change_history(&identity.identifier()).await?;
let named_vault = self.get_named_vault(&identity.vault_name).await?;
let identity_vault = self.make_vault(named_vault).await?;
Ok(Identity::import_from_change_history(
Some(&identity.identifier()),
change_history,
identity_vault.verifying_vault,
)
.await?)
}
None => Err(Self::missing_identifier(name))?,
}
}
#[instrument(skip_all, fields(identifier = %identifier))]
pub async fn get_identity(&self, identifier: &Identifier) -> Result<Identity> {
match self
.change_history_repository()
.get_change_history(identifier)
.await?
{
Some(change_history) => {
Ok(Identity::create_from_change_history(&change_history).await?)
}
None => Err(Error::new(
Origin::Api,
Kind::NotFound,
format!("no identity found for identifier {identifier}"),
))?,
}
}
#[instrument(skip_all)]
pub async fn get_default_identity_name(&self) -> Result<String> {
Ok(self.get_or_create_default_named_identity().await?.name())
}
#[instrument(skip_all)]
pub async fn get_or_create_default_named_identity(&self) -> Result<NamedIdentity> {
match self
.identities_repository()
.get_default_named_identity()
.await?
{
Some(named_identity) => Ok(named_identity),
None => {
self.notify_message(fmt_log!(
"There is no default Identity on this machine, generating one...\n"
));
self.create_identity_with_name(&random_name()).await
}
}
}
#[instrument(skip_all, fields(name = name.clone()))]
pub async fn get_identity_name_or_default(&self, name: &Option<String>) -> Result<String> {
match name {
Some(name) => Ok(name.clone()),
None => self.get_default_identity_name().await,
}
}
#[instrument(skip_all, fields(identifier = %identifier))]
pub async fn get_named_identity_by_identifier(
&self,
identifier: &Identifier,
) -> Result<NamedIdentity> {
match self
.identities_repository()
.get_named_identity_by_identifier(identifier)
.await?
{
Some(named_identity) => Ok(named_identity),
None => Err(Error::new(
Origin::Api,
Kind::NotFound,
format!("no named identity found for identifier {identifier}"),
))?,
}
}
#[instrument(skip_all, fields(name = %name))]
pub async fn is_default_identity_by_name(&self, name: &str) -> Result<bool> {
Ok(self
.identities_repository()
.get_named_identity(name)
.await?
.map(|n| n.is_default())
.unwrap_or(false))
}
}
impl CliState {
#[instrument(skip_all, fields(name = %name))]
pub async fn set_as_default_identity(&self, name: &str) -> Result<()> {
Ok(self.identities_repository().set_as_default(name).await?)
}
#[instrument(skip_all, fields(name = %name))]
pub async fn delete_identity_by_name(&self, name: &str) -> Result<()> {
let nodes = self.get_nodes_by_identity_name(name).await?;
if nodes.is_empty() {
if let Some(identifier) = self.identities_repository().delete_identity(name).await? {
self.change_history_repository()
.delete_change_history(&identifier)
.await?;
};
Ok(())
} else {
let node_names: Vec<String> = nodes.iter().map(|n| n.name()).collect();
Err(Error::new(
Origin::Api,
Kind::Invalid,
format!(
"The identity named {name} cannot be deleted because it is used by the node(s): {}",
node_names.join(", ")
),
))?
}
}
#[instrument(skip_all, fields(identifier = %identifier, name = %name))]
pub async fn update_named_identity_name(
&self,
identifier: &Identifier,
name: &str,
) -> Result<()> {
Ok(self
.identities_repository()
.update_name(identifier, name)
.await?)
}
}
impl CliState {
#[instrument(skip_all, fields(name = %name, identifier = %identifier, vault_name = %vault_name))]
pub async fn store_named_identity(
&self,
identifier: &Identifier,
name: &str,
vault_name: &str,
) -> Result<NamedIdentity> {
let repository = self.identities_repository();
let is_default_identity = repository.get_default_named_identity().await?.is_none();
let mut named_identity = repository
.store_named_identity(identifier, name, vault_name)
.await?;
if is_default_identity {
repository
.set_as_default_by_identifier(&named_identity.identifier())
.await?;
named_identity = named_identity.set_as_default();
}
Ok(named_identity)
}
#[instrument(skip_all, fields(identifier = %identifier))]
async fn get_change_history(&self, identifier: &Identifier) -> Result<ChangeHistory> {
match self
.change_history_repository()
.get_change_history(identifier)
.await?
{
Some(change_history) => Ok(change_history),
None => Err(Error::new(
Origin::Core,
Kind::NotFound,
format!("identity not found for identifier {}", identifier),
))?,
}
}
fn missing_identifier(name: &Option<String>) -> Error {
let message = name
.clone()
.map_or("no default identifier found".to_string(), |n| {
format!("no identifier found with name {}", n)
});
Error::new(Origin::Api, Kind::NotFound, message)
}
}
#[derive(Debug, PartialEq, Eq, Clone, serde::Serialize, serde::Deserialize)]
pub struct NamedIdentity {
identifier: Identifier,
name: String,
vault_name: String,
is_default: bool,
}
impl NamedIdentity {
pub fn new(identifier: Identifier, name: String, vault_name: String, is_default: bool) -> Self {
Self {
identifier,
name,
vault_name,
is_default,
}
}
pub fn identifier(&self) -> Identifier {
self.identifier.clone()
}
pub fn name(&self) -> String {
self.name.clone()
}
pub fn vault_name(&self) -> String {
self.vault_name.clone()
}
pub fn is_default(&self) -> bool {
self.is_default
}
pub fn set_as_default(&self) -> NamedIdentity {
let mut result = self.clone();
result.is_default = true;
result
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_create_identity_with_a_vault() -> Result<()> {
let cli = CliState::test().await?;
let vault_name = "vault-name";
let _ = cli.get_or_create_named_vault(vault_name).await?;
let identity_name = "identity-name";
let identity = cli
.create_identity_with_name_and_vault(identity_name, vault_name)
.await?;
let expected = cli.get_named_identity(identity_name).await?;
assert_eq!(identity, expected);
let _ = cli
.create_identity_with_name_and_vault(identity_name, vault_name)
.await?;
let identities = cli.get_named_identities().await?;
assert_eq!(identities.len(), 1);
Ok(())
}
#[tokio::test]
async fn test_create_identity_with_the_default_vault() -> Result<()> {
let cli = CliState::test().await?;
let identity_name = "identity-name";
let identity = cli.create_identity_with_name(identity_name).await?;
let expected = cli.get_named_identity(identity_name).await?;
assert_eq!(identity, expected);
let default_vault = cli.get_or_create_default_named_vault().await?;
assert_eq!(identity.vault_name, default_vault.name());
Ok(())
}
#[tokio::test]
async fn test_get_default_identity() -> Result<()> {
let cli = CliState::test().await?;
let identity = cli.get_or_create_default_named_identity().await?;
let result = cli.get_change_history(&identity.identifier()).await;
assert!(result.is_ok());
let result = cli.get_named_identity(&identity.name()).await;
assert!(result.is_ok());
let default_vault = cli.get_or_create_default_named_vault().await?;
assert_eq!(identity.vault_name, default_vault.name());
Ok(())
}
#[tokio::test]
async fn test_delete_identity() -> Result<()> {
let cli = CliState::test().await?;
let identity = cli.create_identity_with_name("name").await?;
let result = cli.get_change_history(&identity.identifier()).await;
assert!(result.is_ok());
let result = cli.get_named_identity(&identity.name()).await;
assert!(result.is_ok());
cli.delete_identity_by_name(&identity.name()).await?;
let result = cli.get_change_history(&identity.identifier()).await;
assert!(result.is_err());
let result = cli.get_named_identity(&identity.name()).await;
assert!(result.is_err());
Ok(())
}
}