diff --git a/examples/rust/get_started/examples/06-credentials-exchange-issuer.rs b/examples/rust/get_started/examples/06-credentials-exchange-issuer.rs index dec290ef007..90373135218 100644 --- a/examples/rust/get_started/examples/06-credentials-exchange-issuer.rs +++ b/examples/rust/get_started/examples/06-credentials-exchange-issuer.rs @@ -54,7 +54,16 @@ async fn main(ctx: Context) -> Result<()> { // // For a different application this attested attribute set can be different and // distinct for each identifier, but for this example we'll keep things simple. - let credential_issuer = CredentialIssuerWorker::new(members.clone(), node.credentials(), &issuer, None, None, None); + let credential_issuer = CredentialIssuerWorker::new( + members.clone(), + node.identities_attributes(), + node.credentials(), + &issuer, + "test".to_string(), + None, + None, + true, + ); let mut pre_trusted_identities = BTreeMap::::new(); let attributes = PreTrustedIdentity::new( diff --git a/examples/rust/get_started/examples/11-attribute-based-authentication-control-plane.rs b/examples/rust/get_started/examples/11-attribute-based-authentication-control-plane.rs index 8b992084ba3..9fa24eddd0a 100644 --- a/examples/rust/get_started/examples/11-attribute-based-authentication-control-plane.rs +++ b/examples/rust/get_started/examples/11-attribute-based-authentication-control-plane.rs @@ -11,7 +11,7 @@ use ockam::{node, Context, Result, TcpOutletOptions, TcpTransportExtension}; use ockam_api::authenticator::enrollment_tokens::TokenAcceptor; use ockam_api::authenticator::one_time_code::OneTimeCode; use ockam_api::nodes::NodeManager; -use ockam_api::{multiaddr_to_route, multiaddr_to_transport_route, DefaultAddress}; +use ockam_api::{multiaddr_to_route, multiaddr_to_transport_route}; use ockam_core::AsyncTryClone; use ockam_multiaddr::MultiAddr; @@ -80,11 +80,11 @@ async fn start_node(ctx: Context, project_information_path: &str, token: OneTime node.context().async_try_clone().await?, Arc::new(tcp.clone()), node.secure_channels(), - RemoteCredentialRetrieverInfo::new( + RemoteCredentialRetrieverInfo::create_for_project_member( project.authority_identifier(), project_authority_route, - DefaultAddress::CREDENTIAL_ISSUER.into(), ), + "test".to_string(), )); // 3. create an access control policy checking the value of the "component" attribute of the caller diff --git a/examples/rust/get_started/examples/11-attribute-based-authentication-edge-plane.rs b/examples/rust/get_started/examples/11-attribute-based-authentication-edge-plane.rs index 4d48291f168..a015649ac45 100644 --- a/examples/rust/get_started/examples/11-attribute-based-authentication-edge-plane.rs +++ b/examples/rust/get_started/examples/11-attribute-based-authentication-edge-plane.rs @@ -9,7 +9,7 @@ use ockam::{route, Context, Result}; use ockam_api::authenticator::enrollment_tokens::TokenAcceptor; use ockam_api::authenticator::one_time_code::OneTimeCode; use ockam_api::nodes::NodeManager; -use ockam_api::{multiaddr_to_route, multiaddr_to_transport_route, DefaultAddress}; +use ockam_api::{multiaddr_to_route, multiaddr_to_transport_route}; use ockam_core::compat::sync::Arc; use ockam_core::AsyncTryClone; use ockam_multiaddr::MultiAddr; @@ -80,11 +80,11 @@ async fn start_node(ctx: Context, project_information_path: &str, token: OneTime node.context().async_try_clone().await?, Arc::new(tcp.clone()), node.secure_channels(), - RemoteCredentialRetrieverInfo::new( + RemoteCredentialRetrieverInfo::create_for_project_member( project.authority_identifier(), project_authority_route, - DefaultAddress::CREDENTIAL_ISSUER.into(), ), + "test".to_string(), )); // 3. create an access control policy checking the value of the "component" attribute of the caller diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/common.rs b/implementations/rust/ockam/ockam_api/src/authenticator/common.rs index 2a1465d7158..b4d14aaa7f3 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/common.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/common.rs @@ -1,6 +1,6 @@ use crate::authenticator::direct::{OCKAM_ROLE_ATTRIBUTE_ENROLLER_VALUE, OCKAM_ROLE_ATTRIBUTE_KEY}; use crate::authenticator::AuthorityMembersRepository; -use ockam::identity::Identifier; +use ockam::identity::{Identifier, IdentitiesAttributes}; use ockam_core::Result; use std::collections::BTreeMap; use std::sync::Arc; @@ -39,12 +39,11 @@ impl EnrollerAccessControlChecks { false } - pub(crate) async fn check_identifier( + pub(crate) async fn check_is_member( members: Arc, identifier: &Identifier, - account_authority: &Option, ) -> Result { - let mut r = match members.get_member(identifier).await? { + let r = match members.get_member(identifier).await? { Some(member) => { let is_enroller = Self::check_bin_attributes_is_enroller(member.attributes()); EnrollerCheckResult { @@ -61,9 +60,20 @@ impl EnrollerAccessControlChecks { is_pre_trusted: false, }, }; + + Ok(r) + } + + pub(crate) async fn check_identifier( + members: Arc, + identities_attributes: Arc, + identifier: &Identifier, + account_authority: &Option, + ) -> Result { + let mut r = Self::check_is_member(members, identifier).await?; + if let Some(info) = account_authority { - if let Some(attrs) = info - .identities_attributes() + if let Some(attrs) = identities_attributes .get_attributes(identifier, info.account_authority()) .await? { diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/credential_issuer/credential_issuer.rs b/implementations/rust/ockam/ockam_api/src/authenticator/credential_issuer/credential_issuer.rs index 4018ae1fbde..288c99aa22b 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/credential_issuer/credential_issuer.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/credential_issuer/credential_issuer.rs @@ -4,7 +4,7 @@ use crate::authenticator::direct::AccountAuthorityInfo; use crate::authenticator::AuthorityMembersRepository; use ockam::identity::models::{CredentialAndPurposeKey, CredentialSchemaIdentifier}; use ockam::identity::utils::AttributesBuilder; -use ockam::identity::{Attributes, Credentials, Identifier}; +use ockam::identity::{Attributes, Credentials, Identifier, IdentitiesAttributes}; use ockam_core::compat::sync::Arc; use ockam_core::Result; @@ -20,6 +20,7 @@ pub const DEFAULT_CREDENTIAL_VALIDITY: Duration = Duration::from_secs(30 * 24 * /// This struct runs as a Worker to issue credentials based on a request/response protocol pub struct CredentialIssuer { members: Arc, + identities_attributes: Arc, credentials: Arc, issuer: Identifier, subject_attributes: Attributes, @@ -30,17 +31,20 @@ pub struct CredentialIssuer { impl CredentialIssuer { /// Create a new credentials issuer + #[allow(clippy::too_many_arguments)] #[instrument(skip_all, fields(issuer = %issuer, project_identifier = project_identifier.clone(), credential_ttl = credential_ttl.map_or("n/a".to_string(), |d| d.as_secs().to_string())))] pub fn new( members: Arc, + identities_attributes: Arc, credentials: Arc, issuer: &Identifier, - project_identifier: Option, // Legacy value, should be removed when all clients are updated to the latest version + project_identifier: String, credential_ttl: Option, account_authority: Option, + disable_trust_context_id: bool, ) -> Self { let subject_attributes = AttributesBuilder::with_schema(PROJECT_MEMBER_SCHEMA); - let subject_attributes = if let Some(project_identifier) = project_identifier { + let subject_attributes = if !disable_trust_context_id { // Legacy value, should be removed when all clients are updated to the latest version subject_attributes.with_attribute( TRUST_CONTEXT_ID.to_vec(), @@ -53,6 +57,7 @@ impl CredentialIssuer { Self { members, + identities_attributes, credentials, issuer: issuer.clone(), subject_attributes, @@ -68,8 +73,8 @@ impl CredentialIssuer { ) -> Result> { // Check if it has a valid project admin credential if let Some(info) = self.account_authority.as_ref() { - if let Some(attrs) = info - .identities_attributes() + if let Some(attrs) = self + .identities_attributes .get_attributes(subject, info.account_authority()) .await? { diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/credential_issuer/credential_issuer_worker.rs b/implementations/rust/ockam/ockam_api/src/authenticator/credential_issuer/credential_issuer_worker.rs index c52e3791a44..61a8d5ad33b 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/credential_issuer/credential_issuer_worker.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/credential_issuer/credential_issuer_worker.rs @@ -5,7 +5,9 @@ use tracing::trace; use crate::authenticator::credential_issuer::CredentialIssuer; use crate::authenticator::direct::AccountAuthorityInfo; use crate::authenticator::AuthorityMembersRepository; -use ockam::identity::{Credentials, Identifier, IdentitySecureChannelLocalInfo}; +use ockam::identity::{ + Credentials, Identifier, IdentitiesAttributes, IdentitySecureChannelLocalInfo, +}; use ockam_core::api::{Method, RequestHeader, Response}; use ockam_core::compat::boxed::Box; use ockam_core::compat::sync::Arc; @@ -20,22 +22,27 @@ pub struct CredentialIssuerWorker { impl CredentialIssuerWorker { /// Create a new credentials issuer + #[allow(clippy::too_many_arguments)] pub fn new( members: Arc, + identities_attributes: Arc, credentials: Arc, issuer: &Identifier, - project_identifier: Option, // Legacy value, should be removed when all clients are updated to the latest version + project_identifier: String, credential_ttl: Option, account_authority: Option, + disable_trust_context_id: bool, ) -> Self { Self { credential_issuer: CredentialIssuer::new( members, + identities_attributes, credentials, issuer, project_identifier, credential_ttl, account_authority, + disable_trust_context_id, ), } } diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/direct/direct_authenticator.rs b/implementations/rust/ockam/ockam_api/src/authenticator/direct/direct_authenticator.rs index ebccf265fbd..4db0bf173d0 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/direct/direct_authenticator.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/direct/direct_authenticator.rs @@ -1,10 +1,9 @@ use either::Either; -use ockam::identity::IdentitiesAttributes; use std::collections::{BTreeMap, HashMap}; use ockam::identity::utils::now; -use ockam::identity::AttributesEntry; use ockam::identity::Identifier; +use ockam::identity::{AttributesEntry, IdentitiesAttributes}; use ockam_core::compat::sync::Arc; use ockam_core::Result; @@ -24,11 +23,11 @@ pub type DirectAuthenticatorResult = Either; pub struct DirectAuthenticator { members: Arc, + identities_attributes: Arc, account_authority: Option, } #[derive(Clone)] pub struct AccountAuthorityInfo { - identities_attributes: Arc, account_authority: Identifier, project_identifier: String, enforce_admin_checks: bool, @@ -36,22 +35,17 @@ pub struct AccountAuthorityInfo { impl AccountAuthorityInfo { pub fn new( - identities_attributes: Arc, account_authority: Identifier, project_identifier: String, enforce_admin_checks: bool, ) -> Self { Self { - identities_attributes, account_authority, project_identifier, enforce_admin_checks, } } - pub fn identities_attributes(&self) -> Arc { - self.identities_attributes.clone() - } pub fn account_authority(&self) -> &Identifier { &self.account_authority } @@ -66,10 +60,12 @@ impl AccountAuthorityInfo { impl DirectAuthenticator { pub fn new( members: Arc, + identities_attributes: Arc, account_authority: Option, ) -> Self { Self { members, + identities_attributes, account_authority, } } @@ -83,6 +79,7 @@ impl DirectAuthenticator { ) -> Result> { let check = EnrollerAccessControlChecks::check_identifier( self.members.clone(), + self.identities_attributes.clone(), enroller, &self.account_authority, ) @@ -143,6 +140,7 @@ impl DirectAuthenticator { ) -> Result>> { let check = EnrollerAccessControlChecks::check_identifier( self.members.clone(), + self.identities_attributes.clone(), enroller, &self.account_authority, ) @@ -179,6 +177,7 @@ impl DirectAuthenticator { ) -> Result> { let check_enroller = EnrollerAccessControlChecks::check_identifier( self.members.clone(), + self.identities_attributes.clone(), enroller, &self.account_authority, ) @@ -196,6 +195,7 @@ impl DirectAuthenticator { let check_member = EnrollerAccessControlChecks::check_identifier( self.members.clone(), + self.identities_attributes.clone(), identifier, &self.account_authority, ) diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/direct/direct_authenticator_worker.rs b/implementations/rust/ockam/ockam_api/src/authenticator/direct/direct_authenticator_worker.rs index e943c81c070..0af9d0bdda1 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/direct/direct_authenticator_worker.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/direct/direct_authenticator_worker.rs @@ -2,7 +2,7 @@ use either::Either; use minicbor::Decoder; use tracing::trace; -use ockam::identity::{Identifier, IdentitySecureChannelLocalInfo}; +use ockam::identity::{Identifier, IdentitiesAttributes, IdentitySecureChannelLocalInfo}; use ockam_core::api::{Method, RequestHeader, Response}; use ockam_core::compat::sync::Arc; use ockam_core::{Result, Routed, Worker}; @@ -21,10 +21,15 @@ pub struct DirectAuthenticatorWorker { impl DirectAuthenticatorWorker { pub fn new( members: Arc, + identities_attributes: Arc, account_authority: Option, ) -> Self { Self { - authenticator: DirectAuthenticator::new(members, account_authority), + authenticator: DirectAuthenticator::new( + members, + identities_attributes, + account_authority, + ), } } } diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/acceptor.rs b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/acceptor.rs index 604b278de3f..82f375517af 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/acceptor.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/acceptor.rs @@ -34,8 +34,7 @@ impl EnrollmentTokenAcceptor { from: &Identifier, ) -> Result> { let check = - EnrollerAccessControlChecks::check_identifier(self.members.clone(), from, &None) - .await?; + EnrollerAccessControlChecks::check_is_member(self.members.clone(), from).await?; // Not allow updating existing members if check.is_member { diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/issuer.rs b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/issuer.rs index 4e6ef366ea7..273cc3bbb67 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/issuer.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/issuer.rs @@ -4,7 +4,7 @@ use rand::Rng; use std::collections::BTreeMap; use ockam::identity::utils::now; -use ockam::identity::Identifier; +use ockam::identity::{Identifier, IdentitiesAttributes}; use ockam_core::compat::sync::Arc; use ockam_core::compat::time::Duration; use ockam_core::Result; @@ -25,6 +25,7 @@ pub type EnrollmentTokenIssuerResult = Either; pub struct EnrollmentTokenIssuer { pub(super) tokens: Arc, pub(super) members: Arc, + pub(super) identities_attributes: Arc, pub(super) account_authority: Option, } @@ -32,11 +33,13 @@ impl EnrollmentTokenIssuer { pub fn new( tokens: Arc, members: Arc, + identities_attributes: Arc, account_authority: Option, ) -> Self { Self { tokens, members, + identities_attributes, account_authority, } } @@ -51,6 +54,7 @@ impl EnrollmentTokenIssuer { ) -> Result> { let check = EnrollerAccessControlChecks::check_identifier( self.members.clone(), + self.identities_attributes.clone(), enroller, &self.account_authority, ) diff --git a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/issuer_worker.rs b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/issuer_worker.rs index 62b636be832..aa8ed44bae2 100644 --- a/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/issuer_worker.rs +++ b/implementations/rust/ockam/ockam_api/src/authenticator/enrollment_tokens/issuer_worker.rs @@ -2,7 +2,7 @@ use either::Either; use minicbor::Decoder; use tracing::trace; -use ockam::identity::IdentitySecureChannelLocalInfo; +use ockam::identity::{IdentitiesAttributes, IdentitySecureChannelLocalInfo}; use ockam_core::api::{Method, RequestHeader, Response}; use ockam_core::compat::sync::Arc; use ockam_core::compat::time::Duration; @@ -22,10 +22,16 @@ impl EnrollmentTokenIssuerWorker { pub fn new( tokens: Arc, members: Arc, + identities_attributes: Arc, account_authority: Option, ) -> Self { Self { - issuer: EnrollmentTokenIssuer::new(tokens, members, account_authority), + issuer: EnrollmentTokenIssuer::new( + tokens, + members, + identities_attributes, + account_authority, + ), } } } diff --git a/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs b/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs index c4b6e87c9ef..a1129386cc2 100644 --- a/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs +++ b/implementations/rust/ockam/ockam_api/src/authority_node/authority.rs @@ -74,7 +74,6 @@ impl Authority { let identities = Identities::create(database).build(); - let identity_attrs = identities.identities_attributes().clone(); let secure_channels = SecureChannels::from_identities(identities.clone()); let identifier = configuration.identifier(); @@ -87,7 +86,6 @@ impl Authority { .import_from_change_history(None, change_history) .await?; Some(AccountAuthorityInfo::new( - identity_attrs, acc_authority_identifier, configuration.project_identifier(), configuration.enforce_admin_checks, @@ -158,8 +156,11 @@ impl Authority { return Ok(()); } - let direct = - DirectAuthenticatorWorker::new(self.members.clone(), self.account_authority.clone()); + let direct = DirectAuthenticatorWorker::new( + self.members.clone(), + self.secure_channels.identities().identities_attributes(), + self.account_authority.clone(), + ); let name = configuration.authenticator_name(); ctx.flow_controls() @@ -185,6 +186,7 @@ impl Authority { let issuer = EnrollmentTokenIssuerWorker::new( self.tokens.clone(), self.members.clone(), + self.secure_channels.identities().identities_attributes(), self.account_authority.clone(), ); let acceptor = @@ -226,11 +228,13 @@ impl Authority { // create and start a credential issuer worker let issuer = CredentialIssuerWorker::new( self.members.clone(), + self.secure_channels.identities().identities_attributes(), self.secure_channels.identities().credentials(), &self.identifier, - Some(configuration.project_identifier()), + configuration.project_identifier(), ttl, self.account_authority.clone(), + configuration.disable_trust_context_id, ); let address = DefaultAddress::CREDENTIAL_ISSUER.to_string(); diff --git a/implementations/rust/ockam/ockam_api/src/authority_node/configuration.rs b/implementations/rust/ockam/ockam_api/src/authority_node/configuration.rs index 0f2afe6c684..8afed24f45c 100644 --- a/implementations/rust/ockam/ockam_api/src/authority_node/configuration.rs +++ b/implementations/rust/ockam/ockam_api/src/authority_node/configuration.rs @@ -52,6 +52,10 @@ pub struct Configuration { /// Differentiate between admins and enrollers pub enforce_admin_checks: bool, + + /// Will not include trust_context_id and project id into credential + /// Set to true after old clients are updated + pub disable_trust_context_id: bool, } /// Local and private functions for the authority configuration diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/enrollments.rs b/implementations/rust/ockam/ockam_api/src/cli_state/enrollments.rs index 886da3f2df6..63762e71dd6 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/enrollments.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/enrollments.rs @@ -7,6 +7,7 @@ use ockam::identity::Identifier; use crate::cli_state::Result; use crate::cli_state::{CliState, CliStateError}; +use crate::cloud::email_address::EmailAddress; use crate::cloud::project::models::ProjectModel; use crate::error::ApiError; @@ -47,10 +48,14 @@ impl CliState { } #[instrument(skip_all, fields(identifier = %identifier))] - pub async fn set_identifier_as_enrolled(&self, identifier: &Identifier) -> Result<()> { + pub async fn set_identifier_as_enrolled( + &self, + identifier: &Identifier, + email: &EmailAddress, + ) -> Result<()> { Ok(self .enrollment_repository() - .set_as_enrolled(identifier) + .set_as_enrolled(identifier, email) .await?) } @@ -117,6 +122,7 @@ impl Display for EnrollmentStatus { pub struct IdentityEnrollment { identifier: Identifier, name: Option, + email: Option, is_default: bool, enrolled_at: Option, } @@ -125,23 +131,30 @@ impl IdentityEnrollment { pub fn new( identifier: Identifier, name: Option, + email: Option, is_default: bool, enrolled_at: Option, ) -> Self { Self { identifier, name, + email, is_default, enrolled_at, } } - pub fn identifier(&self) -> Identifier { - self.identifier.clone() + pub fn identifier(&self) -> &Identifier { + &self.identifier + } + + #[allow(dead_code)] + pub fn name(&self) -> &Option { + &self.name } #[allow(dead_code)] - pub fn name(&self) -> Option { - self.name.clone() + pub fn email(&self) -> &Option { + &self.email } #[allow(dead_code)] diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs b/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs index 640335a7966..91155bf9a04 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/projects.rs @@ -1,4 +1,4 @@ -use ockam::identity::IdentitiesVerification; +use ockam::identity::{Identifier, IdentitiesVerification}; use std::collections::HashMap; use std::sync::Arc; @@ -7,9 +7,11 @@ use ockam_core::Error; use ockam_vault::SoftwareVaultForVerifyingSignatures; use crate::cli_state::CliState; +use crate::cloud::email_address::EmailAddress; use crate::cloud::project::models::ProjectModel; use crate::cloud::project::Project; -use crate::ProjectsRepository; +use crate::cloud::share::RoleInShare; +use crate::{EnrollmentStatus, ProjectsRepository}; use super::Result; @@ -165,6 +167,35 @@ impl Projects { } impl CliState { + pub async fn is_project_admin( + &self, + caller_identifier: &Identifier, + project: &Project, + ) -> Result { + let enrolled = self + .get_identity_enrollments(EnrollmentStatus::Enrolled) + .await?; + + let emails: Vec = enrolled + .iter() + .flat_map(|x| { + if x.identifier() == caller_identifier { + x.email().clone() + } else { + None + } + }) + .collect(); + + let is_project_admin = project + .model() + .user_roles + .iter() + .any(|u| u.role == RoleInShare::Admin && emails.contains(&u.email)); + + Ok(is_project_admin) + } + pub fn projects(&self) -> Projects { let identities_verification = IdentitiesVerification::new( self.change_history_repository(), diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository.rs index ac32279cda4..7df1efab00d 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository.rs @@ -3,6 +3,7 @@ use ockam_core::async_trait; use ockam_core::Result; use crate::cli_state::enrollments::IdentityEnrollment; +use crate::cloud::email_address::EmailAddress; /// This trait stores the enrollment status for local identities /// If an identity has been enrolled it is possible to retrieve: @@ -15,7 +16,7 @@ use crate::cli_state::enrollments::IdentityEnrollment; #[async_trait] pub trait EnrollmentsRepository: Send + Sync + 'static { /// Set the identifier as enrolled, and set a timestamp to record the information - async fn set_as_enrolled(&self, identifier: &Identifier) -> Result<()>; + async fn set_as_enrolled(&self, identifier: &Identifier, email: &EmailAddress) -> Result<()>; /// Get the list of enrolled identities async fn get_enrolled_identities(&self) -> Result>; diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository_sql.rs b/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository_sql.rs index f0913548b70..84c97988ca7 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository_sql.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/storage/enrollments_repository_sql.rs @@ -12,6 +12,7 @@ use ockam_core::Result; use crate::cli_state::enrollments::IdentityEnrollment; use crate::cli_state::EnrollmentsRepository; +use crate::cloud::email_address::EmailAddress; #[derive(Clone)] pub struct EnrollmentsSqlxDatabase { @@ -33,10 +34,11 @@ impl EnrollmentsSqlxDatabase { #[async_trait] impl EnrollmentsRepository for EnrollmentsSqlxDatabase { - async fn set_as_enrolled(&self, identifier: &Identifier) -> Result<()> { - let query = query("INSERT OR REPLACE INTO identity_enrollment VALUES (?, ?)") + async fn set_as_enrolled(&self, identifier: &Identifier, email: &EmailAddress) -> Result<()> { + let query = query("INSERT OR REPLACE INTO identity_enrollment(identifier, enrolled_at, email) VALUES (?, ?, ?)") .bind(identifier.to_sql()) - .bind(OffsetDateTime::now_utc().to_sql()); + .bind(OffsetDateTime::now_utc().to_sql()) + .bind(email.to_sql()); Ok(query.execute(&*self.database.pool).await.void()?) } @@ -45,7 +47,7 @@ impl EnrollmentsRepository for EnrollmentsSqlxDatabase { r#" SELECT identity.identifier, named_identity.name, named_identity.is_default, - identity_enrollment.enrolled_at + identity_enrollment.enrolled_at, identity_enrollment.email FROM identity INNER JOIN identity_enrollment ON identity.identifier = identity_enrollment.identifier @@ -66,7 +68,7 @@ impl EnrollmentsRepository for EnrollmentsSqlxDatabase { r#" SELECT identity.identifier, named_identity.name, named_identity.is_default, - identity_enrollment.enrolled_at + identity_enrollment.enrolled_at, identity_enrollment.email FROM identity LEFT JOIN identity_enrollment ON identity.identifier = identity_enrollment.identifier @@ -130,6 +132,7 @@ impl EnrollmentsRepository for EnrollmentsSqlxDatabase { pub struct EnrollmentRow { identifier: String, name: Option, + email: Option, is_default: bool, enrolled_at: Option, } @@ -137,9 +140,16 @@ pub struct EnrollmentRow { impl EnrollmentRow { fn identity_enrollment(&self) -> Result { let identifier = Identifier::from_str(self.identifier.as_str())?; + let email = self + .email + .as_ref() + .map(|e| EmailAddress::parse(e.as_str())) + .transpose()?; + Ok(IdentityEnrollment::new( identifier, self.name.clone(), + email, self.is_default, self.enrolled_at(), )) @@ -170,8 +180,12 @@ mod tests { let identity1 = create_identity(db.clone(), "identity1").await?; create_identity(db.clone(), "identity2").await?; + let email = EmailAddress::parse("test@example.com")?; + // an identity can be enrolled - repository.set_as_enrolled(identity1.identifier()).await?; + repository + .set_as_enrolled(identity1.identifier(), &email) + .await?; // retrieve the identities and their enrollment status let result = repository.get_all_identities_enrollments().await?; @@ -180,6 +194,7 @@ mod tests { // retrieve only the enrolled identities let result = repository.get_enrolled_identities().await?; assert_eq!(result.len(), 1); + assert_eq!(result[0].email(), &Some(email)); // the first identity must be seen as enrolled let result = repository.is_identity_enrolled("identity1").await?; diff --git a/implementations/rust/ockam/ockam_api/src/cli_state/trust.rs b/implementations/rust/ockam/ockam_api/src/cli_state/trust.rs index 3aab2fed08e..74262238d5f 100644 --- a/implementations/rust/ockam/ockam_api/src/cli_state/trust.rs +++ b/implementations/rust/ockam/ockam_api/src/cli_state/trust.rs @@ -1,5 +1,9 @@ -use crate::nodes::service::{NodeManagerCredentialRetrieverOptions, NodeManagerTrustOptions}; -use crate::{multiaddr_to_transport_route, CliState, DefaultAddress}; +use crate::cloud::project::Project; +use crate::nodes::service::{ + CredentialScope, NodeManagerCredentialRetrieverOptions, NodeManagerTrustOptions, +}; +use crate::nodes::NodeManager; +use crate::{multiaddr_to_transport_route, CliState}; use ockam::identity::{IdentitiesVerification, RemoteCredentialRetrieverInfo}; use ockam_core::errcode::{Kind, Origin}; use ockam_core::{Error, Result}; @@ -7,6 +11,167 @@ use ockam_multiaddr::MultiAddr; use ockam_vault::SoftwareVaultForVerifyingSignatures; impl CliState { + async fn retrieve_trust_options_explicit_project_authority( + &self, + authority_identity: &str, + authority_route: &Option, + credential_scope: &Option, + ) -> Result { + let identities_verification = IdentitiesVerification::new( + self.change_history_repository(), + SoftwareVaultForVerifyingSignatures::create(), + ); + + let authority_identity = hex::decode(authority_identity).map_err(|_e| { + Error::new( + Origin::Api, + Kind::NotFound, + "Invalid authority identity hex", + ) + })?; + let authority_identifier = identities_verification + .import(None, &authority_identity) + .await?; + + if let Some(authority_multiaddr) = authority_route { + let scope = match credential_scope { + Some(scope) => scope.clone(), + None => { + return Err(Error::new( + Origin::Api, + Kind::NotFound, + "Authority address was provided but credential scope was not provided", + )) + } + }; + + let authority_route = + multiaddr_to_transport_route(authority_multiaddr).ok_or(Error::new( + Origin::Api, + Kind::NotFound, + format!("Invalid authority route: {}", &authority_multiaddr), + ))?; + let info = RemoteCredentialRetrieverInfo::create_for_project_member( + authority_identifier.clone(), + authority_route, + ); + + let trust_options = NodeManagerTrustOptions::new( + NodeManagerCredentialRetrieverOptions::Remote { info, scope }, + NodeManagerCredentialRetrieverOptions::None, + Some(authority_identifier.clone()), + NodeManagerCredentialRetrieverOptions::None, + ); + + info!( + "TrustOptions configured: Authority: {}. Credentials retrieved from Remote Authority: {}", + authority_identifier, authority_multiaddr + ); + + return Ok(trust_options); + } + + if let Some(credential_scope) = credential_scope { + let trust_options = NodeManagerTrustOptions::new( + NodeManagerCredentialRetrieverOptions::CacheOnly { + issuer: authority_identifier.clone(), + scope: credential_scope.clone(), + }, + NodeManagerCredentialRetrieverOptions::None, + Some(authority_identifier.clone()), + NodeManagerCredentialRetrieverOptions::None, + ); + + info!( + "TrustOptions configured: Authority: {}. Expect credentials in cache", + authority_identifier + ); + + return Ok(trust_options); + } + + let trust_options = NodeManagerTrustOptions::new( + NodeManagerCredentialRetrieverOptions::None, + NodeManagerCredentialRetrieverOptions::None, + Some(authority_identifier.clone()), + NodeManagerCredentialRetrieverOptions::None, + ); + + info!( + "TrustOptions configured: Authority: {}. Only verifying credentials", + authority_identifier + ); + + Ok(trust_options) + } + + async fn retrieve_trust_options_with_project( + &self, + project: Project, + ) -> Result { + let authority_identifier = project.authority_identifier()?; + let authority_multiaddr = project.authority_multiaddr()?; + let authority_route = + multiaddr_to_transport_route(authority_multiaddr).ok_or(Error::new( + Origin::Api, + Kind::NotFound, + format!("Invalid authority route: {}", &authority_multiaddr), + ))?; + + let project_id = project.project_id().to_string(); + let project_member_retriever = NodeManagerCredentialRetrieverOptions::Remote { + info: RemoteCredentialRetrieverInfo::create_for_project_member( + authority_identifier.clone(), + authority_route, + ), + scope: CredentialScope::ProjectMember { + project_id: project_id.clone(), + } + .to_string(), + }; + + let controller_identifier = NodeManager::load_controller_identifier()?; + let controller_transport_route = NodeManager::controller_route().await?; + + let project_admin_retriever = NodeManagerCredentialRetrieverOptions::Remote { + info: RemoteCredentialRetrieverInfo::create_for_project_admin( + controller_identifier.clone(), + controller_transport_route.clone(), + project_id.clone(), + ), + scope: CredentialScope::ProjectAdmin { + project_id: project_id.clone(), + } + .to_string(), + }; + + let account_admin_retriever = NodeManagerCredentialRetrieverOptions::Remote { + info: RemoteCredentialRetrieverInfo::create_for_account_admin( + controller_identifier.clone(), + controller_transport_route, + ), + scope: CredentialScope::AccountAdmin { + // TODO: Should be account id, which is now known at this point, but it's not used + // yet anywhere + account_id: project_id.clone(), + } + .to_string(), + }; + + let trust_options = NodeManagerTrustOptions::new( + project_member_retriever, + project_admin_retriever, + Some(authority_identifier.clone()), + account_admin_retriever, + ); + + info!( + "TrustOptions configured: Authority: {}. Credentials retrieved from project: {}", + authority_identifier, authority_multiaddr + ); + Ok(trust_options) + } + /// Create [`NodeManagerTrustOptions`] depending on what trust information we possess /// 1. Either we explicitly know the Authority identity that we trust, and optionally route to its node to request /// a new credential @@ -17,7 +182,7 @@ impl CliState { project_name: &Option, authority_identity: &Option, authority_route: &Option, - expect_cached_credential: bool, + credential_scope: &Option, ) -> Result { if project_name.is_some() && (authority_identity.is_some() || authority_route.is_some()) { return Err(Error::new( @@ -35,82 +200,15 @@ impl CliState { )); } - if authority_route.is_some() && expect_cached_credential { - return Err(Error::new( - Origin::Api, - Kind::NotFound, - "Authority address was provided but expect_cached_credential is true", - )); - } - + // We're using explicitly specified authority instead of a project if let Some(authority_identity) = authority_identity { - let identities_verification = IdentitiesVerification::new( - self.change_history_repository(), - SoftwareVaultForVerifyingSignatures::create(), - ); - - let authority_identity = hex::decode(authority_identity).map_err(|_e| { - Error::new( - Origin::Api, - Kind::NotFound, - "Invalid authority identity hex", - ) - })?; - let authority_identifier = identities_verification - .import(None, &authority_identity) - .await?; - - let trust_options = if let Some(authority_multiaddr) = authority_route { - let authority_route = - multiaddr_to_transport_route(authority_multiaddr).ok_or(Error::new( - Origin::Api, - Kind::NotFound, - format!("Invalid authority route: {}", &authority_multiaddr), - ))?; - let info = RemoteCredentialRetrieverInfo::new( - authority_identifier.clone(), + return self + .retrieve_trust_options_explicit_project_authority( + authority_identity, authority_route, - DefaultAddress::CREDENTIAL_ISSUER.into(), - ); - - let trust_options = NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::Remote(info), - Some(authority_identifier.clone()), - ); - - info!( - "TrustOptions configured: Authority: {}. Credentials retrieved from Remote Authority: {}", - authority_identifier, authority_multiaddr - ); - - trust_options - } else if expect_cached_credential { - let trust_options = NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::CacheOnly(authority_identifier.clone()), - Some(authority_identifier.clone()), - ); - - info!( - "TrustOptions configured: Authority: {}. Expect credentials in cache", - authority_identifier - ); - - trust_options - } else { - let trust_options = NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::None, - Some(authority_identifier.clone()), - ); - - info!( - "TrustOptions configured: Authority: {}. Only verifying credentials", - authority_identifier - ); - - trust_options - }; - - return Ok(trust_options); + credential_scope, + ) + .await; } let project = match project_name { @@ -123,35 +221,14 @@ impl CliState { None => { info!("TrustOptions configured: No Authority. No Credentials"); return Ok(NodeManagerTrustOptions::new( + NodeManagerCredentialRetrieverOptions::None, NodeManagerCredentialRetrieverOptions::None, None, + NodeManagerCredentialRetrieverOptions::None, )); } }; - let authority_identifier = project.authority_identifier()?; - let authority_multiaddr = project.authority_multiaddr()?; - let authority_route = - multiaddr_to_transport_route(authority_multiaddr).ok_or(Error::new( - Origin::Api, - Kind::NotFound, - format!("Invalid authority route: {}", &authority_multiaddr), - ))?; - let info = RemoteCredentialRetrieverInfo::new( - authority_identifier.clone(), - authority_route, - DefaultAddress::CREDENTIAL_ISSUER.into(), - ); - - let trust_options = NodeManagerTrustOptions::new( - NodeManagerCredentialRetrieverOptions::Remote(info), - Some(authority_identifier.clone()), - ); - - info!( - "TrustOptions configured: Authority: {}. Credentials retrieved from project: {}", - authority_identifier, authority_multiaddr - ); - Ok(trust_options) + self.retrieve_trust_options_with_project(project).await } } diff --git a/implementations/rust/ockam/ockam_api/src/cloud/secure_clients.rs b/implementations/rust/ockam/ockam_api/src/cloud/secure_clients.rs index 45bd33cc804..4640006ac79 100644 --- a/implementations/rust/ockam/ockam_api/src/cloud/secure_clients.rs +++ b/implementations/rust/ockam/ockam_api/src/cloud/secure_clients.rs @@ -85,10 +85,12 @@ impl NodeManager { project_identifier: &Identifier, project_multiaddr: &MultiAddr, caller_identifier: &Identifier, + // TODO: Currently admin authenticates as a member on the Project node, but we may choose to + // use project admin credentials in the future credentials_enabled: CredentialsEnabled, ) -> Result { let credential_retriever_creator = match credentials_enabled { - CredentialsEnabled::On => self.credential_retriever_creator.clone(), + CredentialsEnabled::On => self.credential_retriever_creators.project_member.clone(), CredentialsEnabled::Off => None, }; @@ -246,7 +248,7 @@ impl NodeManager { get_env_with_default::(OCKAM_CONTROLLER_ADDR, default_addr).unwrap() } - async fn controller_route() -> Result { + pub async fn controller_route() -> Result { let multiaddr = Self::controller_multiaddr(); multiaddr_to_transport_route(&multiaddr).ok_or_else(|| { ApiError::core(format!( diff --git a/implementations/rust/ockam/ockam_api/src/kafka/integration_test.rs b/implementations/rust/ockam/ockam_api/src/kafka/integration_test.rs index d50b467ac84..8c251261192 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/integration_test.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/integration_test.rs @@ -71,12 +71,16 @@ mod test { listener_address: Address, outlet_address: Address, ) -> ockam::Result { - let authority = handler.node_manager.node_manager.authority().unwrap(); + let project_authority = handler + .node_manager + .node_manager + .project_authority() + .unwrap(); let secure_channel_controller = KafkaSecureChannelControllerImpl::new_extended( handler.secure_channels.clone(), ConsumerNodeAddr::Relay(MultiAddr::try_from("/service/api")?), Some(HopRelayCreator {}), - authority, + project_authority, ); let mut interceptor_multiaddr = MultiAddr::default(); diff --git a/implementations/rust/ockam/ockam_api/src/kafka/portal_worker.rs b/implementations/rust/ockam/ockam_api/src/kafka/portal_worker.rs index 14818b034f9..77736dfca1a 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/portal_worker.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/portal_worker.rs @@ -802,12 +802,16 @@ mod test { context: &mut Context, ) -> ockam::Result<()> { let handle = crate::test_utils::start_manager_for_tests(context, None, None).await?; - let authority = handle.node_manager.node_manager.authority().unwrap(); + let project_authority = handle + .node_manager + .node_manager + .project_authority() + .unwrap(); let secure_channel_controller = KafkaSecureChannelControllerImpl::new( handle.secure_channels.clone(), ConsumerNodeAddr::Relay(MultiAddr::default()), - authority, + project_authority, ) .into_trait(); diff --git a/implementations/rust/ockam/ockam_api/src/kafka/secure_channel_map.rs b/implementations/rust/ockam/ockam_api/src/kafka/secure_channel_map.rs index 232598436ba..8caa56cdb65 100644 --- a/implementations/rust/ockam/ockam_api/src/kafka/secure_channel_map.rs +++ b/implementations/rust/ockam/ockam_api/src/kafka/secure_channel_map.rs @@ -241,7 +241,12 @@ impl KafkaSecureChannelControllerImpl { .send_and_receive( route![NODEMANAGER_ADDR], Request::post("/node/secure_channel") - .body(CreateSecureChannelRequest::new(&destination, None, None)) + .body(CreateSecureChannelRequest::new( + &destination, + None, + None, + None, + )) .to_vec()?, ) .await?; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/connection/project.rs b/implementations/rust/ockam/ockam_api/src/nodes/connection/project.rs index 1232373d3aa..fbe31e0ee72 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/connection/project.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/connection/project.rs @@ -69,6 +69,7 @@ impl Instantiator for ProjectInstantiator { tcp.route, &self.identifier.clone(), Some(vec![project_identifier]), + None, self.timeout, ) .await?; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/connection/secure.rs b/implementations/rust/ockam/ockam_api/src/nodes/connection/secure.rs index e273141da59..47c34d29f84 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/connection/secure.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/connection/secure.rs @@ -58,6 +58,7 @@ impl Instantiator for SecureChannelInstantiator { route![transport_route, route], &self.identifier, self.authorized_identities.clone(), + None, self.timeout, ) .await?; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/mod.rs b/implementations/rust/ockam/ockam_api/src/nodes/mod.rs index 78263c30981..be706e7c0c8 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/mod.rs @@ -7,7 +7,7 @@ pub use service::background_node_client::*; pub use service::in_memory_node::*; pub use service::policy::*; /// The main node-manager service running on remote nodes -pub use service::{IdentityOverride, NodeManager, NodeManagerWorker}; +pub use service::{NodeManager, NodeManagerWorker}; /// A const address to bind and send messages to pub const NODEMANAGER_ADDR: &str = "_internal.nodemanager"; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/models/secure_channel.rs b/implementations/rust/ockam/ockam_api/src/nodes/models/secure_channel.rs index 0eead8fc640..288373c054d 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/models/secure_channel.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/models/secure_channel.rs @@ -3,6 +3,7 @@ use std::time::Duration; use minicbor::{Decode, Encode}; use serde::Serialize; +use ockam::identity::models::CredentialAndPurposeKey; use ockam::identity::{Identifier, SecureChannel, DEFAULT_TIMEOUT}; use ockam_core::flow_control::FlowControlId; use ockam_core::{route, Address, Result}; @@ -23,6 +24,7 @@ pub struct CreateSecureChannelRequest { #[n(2)] pub authorized_identifiers: Option>, #[n(4)] pub timeout: Option, #[n(5)] pub identity_name: Option, + #[n(6)] pub credential: Option, } impl CreateSecureChannelRequest { @@ -30,12 +32,14 @@ impl CreateSecureChannelRequest { addr: &MultiAddr, authorized_identifiers: Option>, identity_name: Option, + credential: Option, ) -> Self { Self { addr: addr.to_owned(), authorized_identifiers, timeout: Some(DEFAULT_TIMEOUT), identity_name, + credential, } } } diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service.rs b/implementations/rust/ockam/ockam_api/src/nodes/service.rs index f4e1d2031d9..9f87b566db8 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service.rs @@ -1,46 +1,10 @@ //! Node Manager (Node Man, the superhero that we deserve) -use std::collections::BTreeMap; -use std::error::Error as _; -use std::net::SocketAddr; -use std::path::PathBuf; -use std::time::Duration; +use minicbor::Encode; -use miette::IntoDiagnostic; -use minicbor::{Decoder, Encode}; - -use ockam::identity::models::CredentialAndPurposeKey; -use ockam::identity::{ - CachedCredentialRetrieverCreator, CredentialRetrieverCreator, MemoryCredentialRetrieverCreator, - RemoteCredentialRetrieverCreator, RemoteCredentialRetrieverInfo, -}; -use ockam::identity::{Identifier, SecureChannels}; -use ockam::{ - Address, Context, RelayService, RelayServiceOptions, Result, Routed, TcpTransport, Worker, -}; -use ockam_abac::expr::str; -use ockam_abac::{Action, Env, Expr, Resource}; -use ockam_core::api::{Method, RequestHeader, Response}; -use ockam_core::compat::{string::String, sync::Arc}; -use ockam_core::flow_control::FlowControlId; -use ockam_core::{AllowAll, AsyncTryClone, IncomingAccessControl}; -use ockam_multiaddr::MultiAddr; - -use crate::cli_state::CliState; -use crate::cloud::{AuthorityNodeClient, CredentialsEnabled, ProjectNodeClient}; -use crate::nodes::connection::{ - Connection, ConnectionBuilder, PlainTcpInstantiator, ProjectInstantiator, - SecureChannelInstantiator, -}; -use crate::nodes::models::policies::SetPolicyRequest; -use crate::nodes::models::portal::{OutletList, OutletStatus}; -use crate::nodes::models::transport::{TransportMode, TransportType}; -use crate::nodes::registry::KafkaServiceKind; -use crate::nodes::service::default_address::DefaultAddress; -use crate::nodes::{InMemoryNode, NODEMANAGER_ADDR}; -use crate::session::MedicHandle; - -use super::registry::Registry; +use ockam::{Address, Result}; +use ockam_core::api::{RequestHeader, Response}; +use ockam_core::compat::string::String; pub(crate) mod background_node_client; pub mod default_address; @@ -57,6 +21,14 @@ mod secure_channel; mod transport; pub mod workers; +mod manager; +mod trust; +mod worker; + +pub use manager::*; +pub use trust::*; +pub use worker::*; + const TARGET: &str = "ockam_api::nodemanager::service"; /// Generate a new alias for some user created extension @@ -77,729 +49,3 @@ pub(crate) fn encode_response>( Ok(v) } - -/// Node manager provides high-level operations to -/// - send messages -/// - create secure channels, inlet, outlet -/// - configure the trust -/// - manage persistent data -pub struct NodeManager { - pub(crate) cli_state: CliState, - node_name: String, - node_identifier: Identifier, - api_transport_flow_control_id: FlowControlId, - pub(crate) tcp_transport: TcpTransport, - pub(crate) secure_channels: Arc, - pub(crate) credential_retriever_creator: Option>, - authority: Option, - pub(crate) registry: Arc, - pub(crate) medic_handle: MedicHandle, -} - -impl NodeManager { - pub fn identifier(&self) -> Identifier { - self.node_identifier.clone() - } - - pub(crate) async fn get_identifier_by_name( - &self, - identity_name: Option, - ) -> Result { - if let Some(name) = identity_name { - Ok(self.cli_state.get_identifier_by_name(name.as_ref()).await?) - } else { - Ok(self.identifier()) - } - } - - pub fn credential_retriever_creator(&self) -> Option> { - self.credential_retriever_creator.clone() - } - - pub fn authority(&self) -> Option { - self.authority.clone() - } - - pub fn node_name(&self) -> String { - self.node_name.clone() - } - - pub fn tcp_transport(&self) -> &TcpTransport { - &self.tcp_transport - } - - pub async fn list_outlets(&self) -> OutletList { - OutletList::new( - self.registry - .outlets - .entries() - .await - .iter() - .map(|(_, info)| { - OutletStatus::new(info.socket_addr, info.worker_addr.clone(), None) - }) - .collect(), - ) - } - - /// Delete the current node data - pub async fn delete_node(&self) -> Result<()> { - self.cli_state.remove_node(&self.node_name).await?; - Ok(()) - } -} - -impl NodeManager { - pub async fn create_authority_client( - &self, - authority_identifier: &Identifier, - authority_route: &MultiAddr, - caller_identity_name: Option, - credential_retriever_creator: Option>, - ) -> miette::Result { - self.make_authority_node_client( - authority_identifier, - authority_route, - &self - .get_identifier_by_name(caller_identity_name) - .await - .into_diagnostic()?, - credential_retriever_creator, - ) - .await - .into_diagnostic() - } - - pub async fn create_project_client( - &self, - project_identifier: &Identifier, - project_multiaddr: &MultiAddr, - caller_identity_name: Option, - credentials_enabled: CredentialsEnabled, - ) -> miette::Result { - self.make_project_node_client( - project_identifier, - project_multiaddr, - &self - .get_identifier_by_name(caller_identity_name) - .await - .into_diagnostic()?, - credentials_enabled, - ) - .await - .into_diagnostic() - } -} - -#[derive(Clone)] -pub struct NodeManagerWorker { - pub node_manager: Arc, -} - -impl NodeManagerWorker { - pub fn new(node_manager: Arc) -> Self { - NodeManagerWorker { node_manager } - } - - pub async fn stop(&self, ctx: &Context) -> Result<()> { - self.node_manager.stop(ctx).await?; - ctx.stop_worker(NODEMANAGER_ADDR).await?; - Ok(()) - } -} - -pub struct IdentityOverride { - pub identity: Vec, - pub vault_path: PathBuf, -} - -impl NodeManager { - async fn access_control( - &self, - authority: Option, - resource: Resource, - action: Action, - expression: Option, - ) -> Result> { - let resource_name_str = resource.resource_name.as_str(); - let resource_type_str = resource.resource_type.to_string(); - let action_str = action.as_ref(); - if let Some(authority) = authority { - // Populate environment with known attributes: - let mut env = Env::new(); - env.put("resource.id", str(resource_name_str)); - env.put("action.id", str(action_str)); - - // Store policy for the given resource and action - let policies = self.cli_state.policies(); - if let Some(expression) = expression { - policies - .store_policy_for_resource_name(&resource.resource_name, &action, &expression) - .await?; - } - self.cli_state.store_resource(&resource).await?; - - // Create the policy access control - let policy_access_control = policies - .make_policy_access_control( - self.cli_state.identities_attributes(), - resource, - action, - env, - authority, - ) - .await?; - - cfg_if::cfg_if! { - if #[cfg(feature = "std")] { - let cached_policy_access_control = ockam_core::access_control::CachedIncomingAccessControl::new( - Box::new(policy_access_control)); - Ok(Arc::new(cached_policy_access_control)) - } else { - Ok(Arc::new(policy_access_control)) - } - } - } else { - warn! { - resource_name = resource_name_str, - resource_type = resource_type_str, - action = action_str, - "no policy access control set" - } - Ok(Arc::new(AllowAll)) - } - } -} - -#[derive(Debug)] -pub struct NodeManagerGeneralOptions { - cli_state: CliState, - node_name: String, - start_default_services: bool, - persistent: bool, -} - -impl NodeManagerGeneralOptions { - pub fn new( - cli_state: CliState, - node_name: String, - start_default_services: bool, - persistent: bool, - ) -> Self { - Self { - cli_state, - node_name, - start_default_services, - persistent, - } - } -} - -#[derive(Clone)] -/// Transport to build connection -pub struct ApiTransport { - /// Type of transport being requested - pub tt: TransportType, - /// Mode of transport being requested - pub tm: TransportMode, - /// Socket address - pub socket_address: SocketAddr, - /// Worker address - pub worker_address: String, - /// Processor address - pub processor_address: String, - /// FlowControlId - pub flow_control_id: FlowControlId, -} - -#[derive(Debug)] -pub struct NodeManagerTransportOptions { - api_transport_flow_control_id: FlowControlId, - tcp_transport: TcpTransport, -} - -impl NodeManagerTransportOptions { - pub fn new(api_transport_flow_control_id: FlowControlId, tcp_transport: TcpTransport) -> Self { - Self { - api_transport_flow_control_id, - tcp_transport, - } - } -} - -#[derive(Debug)] -pub enum NodeManagerCredentialRetrieverOptions { - None, - CacheOnly(Identifier), - Remote(RemoteCredentialRetrieverInfo), - InMemory(CredentialAndPurposeKey), -} - -pub struct NodeManagerTrustOptions { - credential_retriever_options: NodeManagerCredentialRetrieverOptions, - authority: Option, -} - -impl NodeManagerTrustOptions { - pub fn new( - credential_retriever_options: NodeManagerCredentialRetrieverOptions, - authority: Option, - ) -> Self { - Self { - credential_retriever_options, - authority, - } - } -} - -impl NodeManager { - /// Create a new NodeManager with the node name from the ockam CLI - #[instrument(name = "create_node_manager", skip_all, fields(node_name = general_options.node_name))] - pub async fn create( - ctx: &Context, - general_options: NodeManagerGeneralOptions, - transport_options: NodeManagerTransportOptions, - trust_options: NodeManagerTrustOptions, - ) -> Result { - debug!("create transports"); - let api_transport_id = random_alias(); - let mut transports = BTreeMap::new(); - transports.insert( - api_transport_id.clone(), - transport_options.api_transport_flow_control_id.clone(), - ); - - let mut cli_state = general_options.cli_state; - cli_state.set_node_name(general_options.node_name.clone()); - - let secure_channels = cli_state - .secure_channels(&general_options.node_name) - .await?; - - let registry = Arc::new(Registry::default()); - debug!("start the medic"); - let medic_handle = MedicHandle::start_medic(ctx, registry.clone()).await?; - - debug!("retrieve the node identifier"); - let node_identifier = cli_state - .get_node(&general_options.node_name) - .await? - .identifier(); - - debug!("create default resource type policies"); - cli_state - .policies() - .store_default_resource_type_policies() - .await?; - - let credential_retriever_creator: Option> = - match trust_options.credential_retriever_options { - NodeManagerCredentialRetrieverOptions::None => None, - NodeManagerCredentialRetrieverOptions::CacheOnly(issuer) => { - Some(Arc::new(CachedCredentialRetrieverCreator::new( - issuer.clone(), - secure_channels.identities().cached_credentials_repository(), - ))) - } - NodeManagerCredentialRetrieverOptions::Remote(info) => { - Some(Arc::new(RemoteCredentialRetrieverCreator::new( - ctx.async_try_clone().await?, - Arc::new(transport_options.tcp_transport.clone()), - secure_channels.clone(), - info.clone(), - ))) - } - NodeManagerCredentialRetrieverOptions::InMemory(credential) => { - Some(Arc::new(MemoryCredentialRetrieverCreator::new(credential))) - } - }; - - let mut s = Self { - cli_state, - node_name: general_options.node_name, - node_identifier, - api_transport_flow_control_id: transport_options.api_transport_flow_control_id, - tcp_transport: transport_options.tcp_transport, - secure_channels, - credential_retriever_creator, - authority: trust_options.authority, - registry, - medic_handle, - }; - - debug!("retrieve the node identifier"); - s.initialize_services(ctx, general_options.start_default_services) - .await?; - info!("created a node manager for the node: {}", s.node_name); - - Ok(s) - } - - async fn initialize_default_services( - &self, - ctx: &Context, - api_flow_control_id: &FlowControlId, - ) -> Result<()> { - // Start services - ctx.flow_controls() - .add_consumer(DefaultAddress::UPPERCASE_SERVICE, api_flow_control_id); - self.start_uppercase_service_impl(ctx, DefaultAddress::UPPERCASE_SERVICE.into()) - .await?; - - RelayService::create( - ctx, - DefaultAddress::RELAY_SERVICE, - RelayServiceOptions::new() - .service_as_consumer(api_flow_control_id) - .relay_as_consumer(api_flow_control_id), - ) - .await?; - - self.create_secure_channel_listener( - DefaultAddress::SECURE_CHANNEL_LISTENER.into(), - None, // Not checking identifiers here in favor of credential check - None, - ctx, - ) - .await?; - - Ok(()) - } - - async fn initialize_services( - &mut self, - ctx: &Context, - start_default_services: bool, - ) -> Result<()> { - let api_flow_control_id = self.api_transport_flow_control_id.clone(); - - if start_default_services { - self.initialize_default_services(ctx, &api_flow_control_id) - .await?; - } - - // Always start the echoer service as ockam_api::Medic assumes it will be - // started unconditionally on every node. It's used for liveliness checks. - ctx.flow_controls() - .add_consumer(DefaultAddress::ECHO_SERVICE, &api_flow_control_id); - self.start_echoer_service(ctx, DefaultAddress::ECHO_SERVICE.into()) - .await?; - - Ok(()) - } - - pub async fn make_connection( - &self, - ctx: Arc, - addr: &MultiAddr, - identifier: Identifier, - authorized: Option, - timeout: Option, - ) -> Result { - let authorized = authorized.map(|authorized| vec![authorized]); - self.connect(ctx, addr, identifier, authorized, timeout) - .await - } - - /// Resolve project ID (if any), create secure channel (if needed) and create a tcp connection - /// Returns [`Connection`] - async fn connect( - &self, - ctx: Arc, - addr: &MultiAddr, - identifier: Identifier, - authorized: Option>, - timeout: Option, - ) -> Result { - debug!(?timeout, "connecting to {}", &addr); - let connection = ConnectionBuilder::new(addr.clone()) - .instantiate( - ctx.clone(), - self, - ProjectInstantiator::new(identifier.clone(), timeout), - ) - .await? - .instantiate(ctx.clone(), self, PlainTcpInstantiator::new()) - .await? - .instantiate( - ctx.clone(), - self, - SecureChannelInstantiator::new(&identifier, timeout, authorized), - ) - .await? - .build(); - connection.add_default_consumers(ctx); - - debug!("connected to {connection:?}"); - Ok(connection) - } - - pub(crate) async fn resolve_project(&self, name: &str) -> Result<(MultiAddr, Identifier)> { - let project = self.cli_state.projects().get_project_by_name(name).await?; - Ok(( - project.project_multiaddr()?.clone(), - project.project_identifier()?, - )) - } -} - -impl NodeManagerWorker { - //////// Request matching and response handling //////// - - #[instrument(skip_all, fields(method = ?req.method(), path = req.path()))] - async fn handle_request( - &mut self, - ctx: &mut Context, - req: &RequestHeader, - dec: &mut Decoder<'_>, - ) -> Result> { - debug! { - target: TARGET, - id = %req.id(), - method = ?req.method(), - path = %req.path(), - body = %req.has_body(), - "request" - } - - use Method::*; - let path = req.path(); - let path_segments = req.path_segments::<5>(); - let method = match req.method() { - Some(m) => m, - None => todo!(), - }; - - let r = match (method, path_segments.as_slice()) { - // ==*== Basic node information ==*== - // TODO: create, delete, destroy remote nodes - (Get, ["node"]) => encode_response(req, self.get_node_status(ctx).await)?, - - // ==*== Tcp Connection ==*== - (Get, ["node", "tcp", "connection"]) => self.get_tcp_connections(req).await.to_vec()?, - (Get, ["node", "tcp", "connection", address]) => { - encode_response(req, self.get_tcp_connection(address.to_string()).await)? - } - (Post, ["node", "tcp", "connection"]) => { - encode_response(req, self.create_tcp_connection(ctx, dec.decode()?).await)? - } - (Delete, ["node", "tcp", "connection"]) => { - encode_response(req, self.delete_tcp_connection(dec.decode()?).await)? - } - - // ==*== Tcp Listeners ==*== - (Get, ["node", "tcp", "listener"]) => self.get_tcp_listeners(req).await.to_vec()?, - (Get, ["node", "tcp", "listener", address]) => { - encode_response(req, self.get_tcp_listener(address.to_string()).await)? - } - (Post, ["node", "tcp", "listener"]) => { - encode_response(req, self.create_tcp_listener(dec.decode()?).await)? - } - (Delete, ["node", "tcp", "listener"]) => { - encode_response(req, self.delete_tcp_listener(dec.decode()?).await)? - } - - // ==*== Secure channels ==*== - (Get, ["node", "secure_channel"]) => { - encode_response(req, self.list_secure_channels().await)? - } - (Get, ["node", "secure_channel_listener"]) => { - encode_response(req, self.list_secure_channel_listener().await)? - } - (Post, ["node", "secure_channel"]) => { - encode_response(req, self.create_secure_channel(dec.decode()?, ctx).await)? - } - (Delete, ["node", "secure_channel"]) => { - encode_response(req, self.delete_secure_channel(dec.decode()?, ctx).await)? - } - (Get, ["node", "show_secure_channel"]) => { - encode_response(req, self.show_secure_channel(dec.decode()?).await)? - } - (Post, ["node", "secure_channel_listener"]) => encode_response( - req, - self.create_secure_channel_listener(dec.decode()?, ctx) - .await, - )?, - (Delete, ["node", "secure_channel_listener"]) => encode_response( - req, - self.delete_secure_channel_listener(dec.decode()?, ctx) - .await, - )?, - (Get, ["node", "show_secure_channel_listener"]) => { - encode_response(req, self.show_secure_channel_listener(dec.decode()?).await)? - } - - // ==*== Services ==*== - (Post, ["node", "services", DefaultAddress::UPPERCASE_SERVICE]) => { - encode_response(req, self.start_uppercase_service(ctx, dec.decode()?).await)? - } - (Post, ["node", "services", DefaultAddress::ECHO_SERVICE]) => { - encode_response(req, self.start_echoer_service(ctx, dec.decode()?).await)? - } - (Post, ["node", "services", DefaultAddress::HOP_SERVICE]) => { - encode_response(req, self.start_hop_service(ctx, dec.decode()?).await)? - } - (Post, ["node", "services", DefaultAddress::KAFKA_OUTLET]) => encode_response( - req, - self.start_kafka_outlet_service(ctx, dec.decode()?).await, - )?, - (Delete, ["node", "services", DefaultAddress::KAFKA_OUTLET]) => encode_response( - req, - self.delete_kafka_service(ctx, dec.decode()?, KafkaServiceKind::Outlet) - .await, - )?, - (Post, ["node", "services", DefaultAddress::KAFKA_CONSUMER]) => encode_response( - req, - self.start_kafka_consumer_service(ctx, dec.decode()?).await, - )?, - (Delete, ["node", "services", DefaultAddress::KAFKA_CONSUMER]) => encode_response( - req, - self.delete_kafka_service(ctx, dec.decode()?, KafkaServiceKind::Consumer) - .await, - )?, - (Post, ["node", "services", DefaultAddress::KAFKA_PRODUCER]) => encode_response( - req, - self.start_kafka_producer_service(ctx, dec.decode()?).await, - )?, - (Delete, ["node", "services", DefaultAddress::KAFKA_PRODUCER]) => encode_response( - req, - self.delete_kafka_service(ctx, dec.decode()?, KafkaServiceKind::Producer) - .await, - )?, - (Post, ["node", "services", DefaultAddress::KAFKA_DIRECT]) => encode_response( - req, - self.start_kafka_direct_service(ctx, dec.decode()?).await, - )?, - (Delete, ["node", "services", DefaultAddress::KAFKA_DIRECT]) => encode_response( - req, - self.delete_kafka_service(ctx, dec.decode()?, KafkaServiceKind::Direct) - .await, - )?, - (Get, ["node", "services"]) => encode_response(req, self.list_services().await)?, - (Get, ["node", "services", service_type]) => { - encode_response(req, self.list_services_of_type(service_type).await)? - } - - // ==*== Relay commands ==*== - (Get, ["node", "relay", alias]) => { - encode_response(req, self.show_relay(req, alias).await)? - } - (Get, ["node", "relay"]) => encode_response(req, self.get_relays(req).await)?, - (Delete, ["node", "relay", alias]) => { - encode_response(req, self.delete_relay(req, alias).await)? - } - (Post, ["node", "relay"]) => { - encode_response(req, self.create_relay(ctx, req, dec.decode()?).await)? - } - - // ==*== Inlets & Outlets ==*== - (Get, ["node", "inlet"]) => encode_response(req, self.get_inlets().await)?, - (Get, ["node", "inlet", alias]) => encode_response(req, self.show_inlet(alias).await)?, - (Get, ["node", "outlet"]) => self.get_outlets(req).await.to_vec()?, - (Get, ["node", "outlet", addr]) => { - let addr: Address = addr.to_string().into(); - encode_response(req, self.show_outlet(&addr).await)? - } - (Post, ["node", "inlet"]) => { - encode_response(req, self.create_inlet(ctx, dec.decode()?).await)? - } - (Post, ["node", "outlet"]) => { - encode_response(req, self.create_outlet(ctx, dec.decode()?).await)? - } - (Delete, ["node", "outlet", addr]) => { - let addr: Address = addr.to_string().into(); - encode_response(req, self.delete_outlet(&addr).await)? - } - (Delete, ["node", "inlet", alias]) => { - encode_response(req, self.delete_inlet(alias).await)? - } - (Delete, ["node", "portal"]) => todo!(), - - // ==*== Flow Controls ==*== - (Post, ["node", "flow_controls", "add_consumer"]) => { - encode_response(req, self.add_consumer(ctx, dec.decode()?).await)? - } - - // ==*== Workers ==*== - (Get, ["node", "workers"]) => encode_response(req, self.list_workers(ctx).await)?, - - // ==*== Policies ==*== - (Post, ["policy", action]) => { - let payload: SetPolicyRequest = dec.decode()?; - encode_response( - req, - self.add_policy(action, payload.resource, payload.expression) - .await, - )? - } - (Get, ["policy", action]) => { - encode_response(req, self.get_policy(action, dec.decode()?).await)? - } - (Get, ["policy"]) => encode_response(req, self.list_policies(dec.decode()?).await)?, - (Delete, ["policy", action]) => { - encode_response(req, self.delete_policy(action, dec.decode()?).await)? - } - - // ==*== Messages ==*== - (Post, ["v0", "message"]) => { - encode_response(req, self.send_message(ctx, dec.decode()?).await)? - } - - // ==*== Catch-all for Unimplemented APIs ==*== - _ => { - warn!(%method, %path, "Called invalid endpoint"); - Response::bad_request(req, &format!("Invalid endpoint: {} {}", method, path)) - .to_vec()? - } - }; - Ok(r) - } -} - -#[ockam::worker] -impl Worker for NodeManagerWorker { - type Message = Vec; - type Context = Context; - - async fn shutdown(&mut self, ctx: &mut Self::Context) -> Result<()> { - self.node_manager.medic_handle.stop_medic(ctx).await - } - - async fn handle_message(&mut self, ctx: &mut Context, msg: Routed>) -> Result<()> { - let return_route = msg.return_route(); - let body = msg.into_body()?; - let mut dec = Decoder::new(&body); - let req: RequestHeader = match dec.decode() { - Ok(r) => r, - Err(e) => { - error!("Failed to decode request: {:?}", e); - return Ok(()); - } - }; - - let r = match self.handle_request(ctx, &req, &mut dec).await { - Ok(r) => r, - Err(err) => { - error! { - target: TARGET, - re = %req.id(), - method = ?req.method(), - path = %req.path(), - code = %err.code(), - cause = ?err.source(), - "failed to handle request" - } - Response::internal_error(&req, &format!("failed to handle request: {err} {req:?}")) - .to_vec()? - } - }; - debug! { - target: TARGET, - re = %req.id(), - method = ?req.method(), - path = %req.path(), - "responding" - } - ctx.send(return_route, r).await - } -} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs index 4da8ee6e6ac..873ad390710 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/in_memory_node.rs @@ -145,7 +145,7 @@ impl InMemoryNode { .into_diagnostic()?; let trust_options = cli_state - .retrieve_trust_options(&project_name, &authority_identity, &authority_route, false) + .retrieve_trust_options(&project_name, &authority_identity, &authority_route, &None) .await .into_diagnostic()?; diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/kafka_services.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/kafka_services.rs index b6971d63b9e..293b7368405 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/kafka_services.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/kafka_services.rs @@ -171,8 +171,8 @@ impl InMemoryNode { ApiError::core("Unable to get flow control for secure channel listener") })?; - let authority_identifier = self - .authority + let project_authority = self + .project_authority .clone() .ok_or(ApiError::core("NodeManager has no authority"))?; @@ -195,7 +195,7 @@ impl InMemoryNode { OutletManagerService::create( context, self.secure_channels.clone(), - authority_identifier.clone(), + project_authority.clone(), default_secure_channel_listener_flow_control_id, outlet_policy_expression.clone(), ) @@ -218,7 +218,7 @@ impl InMemoryNode { let secure_channel_controller = KafkaSecureChannelControllerImpl::new( secure_channels, consumer_node_addr, - authority_identifier, + project_authority, ); let inlet_controller = KafkaInletController::new( @@ -287,8 +287,8 @@ impl InMemoryNode { outlet_node_multiaddr.to_string() ); - let authority_identifier = self - .authority + let project_authority = self + .project_authority .clone() .ok_or(ApiError::core("NodeManager has no authority"))?; @@ -296,7 +296,7 @@ impl InMemoryNode { let secure_channel_controller = KafkaSecureChannelControllerImpl::new( secure_channels, ConsumerNodeAddr::Relay(outlet_node_multiaddr.clone()), - authority_identifier, + project_authority, ); let inlet_policy_expression = if let Some(project) = outlet_node_multiaddr @@ -387,8 +387,8 @@ impl NodeManager { ) .await?; - let authority_id = self - .authority + let project_authority = self + .project_authority .clone() .ok_or(ApiError::core("NodeManager has no authority"))?; let outlet_policy_expression = None; @@ -396,7 +396,7 @@ impl NodeManager { OutletManagerService::create( context, self.secure_channels.clone(), - authority_id, + project_authority, default_secure_channel_listener_flow_control_id, outlet_policy_expression.clone(), ) diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs new file mode 100644 index 00000000000..0e0288f3484 --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/manager.rs @@ -0,0 +1,498 @@ +use crate::cloud::project::Project; +use crate::cloud::{AuthorityNodeClient, CredentialsEnabled, ProjectNodeClient}; +use crate::nodes::connection::{ + Connection, ConnectionBuilder, PlainTcpInstantiator, ProjectInstantiator, + SecureChannelInstantiator, +}; +use crate::nodes::models::portal::{OutletList, OutletStatus}; +use crate::nodes::models::transport::{TransportMode, TransportType}; +use crate::nodes::registry::Registry; +use crate::nodes::service::{ + random_alias, CredentialRetrieverCreators, NodeManagerCredentialRetrieverOptions, + NodeManagerTrustOptions, +}; +use crate::session::MedicHandle; +use crate::{CliState, DefaultAddress}; +use miette::IntoDiagnostic; +use ockam::identity::{ + CachedCredentialRetrieverCreator, CredentialRetrieverCreator, Identifier, + MemoryCredentialRetrieverCreator, RemoteCredentialRetrieverCreator, SecureChannels, +}; +use ockam::{RelayService, RelayServiceOptions}; +use ockam_abac::expr::str; +use ockam_abac::{Action, Env, Expr, Resource}; +use ockam_core::flow_control::FlowControlId; +use ockam_core::{AllowAll, AsyncTryClone, IncomingAccessControl}; +use ockam_multiaddr::MultiAddr; +use ockam_node::Context; +use ockam_transport_tcp::TcpTransport; +use std::collections::BTreeMap; +use std::net::SocketAddr; +use std::sync::Arc; +use std::time::Duration; + +/// Node manager provides high-level operations to +/// - send messages +/// - create secure channels, inlet, outlet +/// - configure the trust +/// - manage persistent data +pub struct NodeManager { + pub(crate) cli_state: CliState, + pub(super) node_name: String, + pub(super) node_identifier: Identifier, + pub(super) api_transport_flow_control_id: FlowControlId, + pub(crate) tcp_transport: TcpTransport, + pub(crate) secure_channels: Arc, + pub(crate) credential_retriever_creators: CredentialRetrieverCreators, + pub(super) project_authority: Option, + pub(crate) registry: Arc, + pub(crate) medic_handle: MedicHandle, +} + +impl NodeManager { + pub fn identifier(&self) -> Identifier { + self.node_identifier.clone() + } + + pub(crate) async fn get_identifier_by_name( + &self, + identity_name: Option, + ) -> ockam_core::Result { + if let Some(name) = identity_name { + Ok(self.cli_state.get_identifier_by_name(name.as_ref()).await?) + } else { + Ok(self.identifier()) + } + } + + pub fn credential_retriever_creators(&self) -> CredentialRetrieverCreators { + self.credential_retriever_creators.clone() + } + + pub fn project_authority(&self) -> Option { + self.project_authority.clone() + } + + pub fn node_name(&self) -> String { + self.node_name.clone() + } + + pub fn tcp_transport(&self) -> &TcpTransport { + &self.tcp_transport + } + + pub async fn list_outlets(&self) -> OutletList { + OutletList::new( + self.registry + .outlets + .entries() + .await + .iter() + .map(|(_, info)| { + OutletStatus::new(info.socket_addr, info.worker_addr.clone(), None) + }) + .collect(), + ) + } + + /// Delete the current node data + pub async fn delete_node(&self) -> ockam_core::Result<()> { + self.cli_state.remove_node(&self.node_name).await?; + Ok(()) + } +} + +impl NodeManager { + pub async fn create_authority_client( + &self, + project: &Project, + caller_identity_name: Option, + ) -> miette::Result { + let caller_identifier = self + .get_identifier_by_name(caller_identity_name) + .await + .into_diagnostic()?; + + let is_project_admin = self + .cli_state + .is_project_admin(&caller_identifier, project) + .await + .into_diagnostic()?; + + let credential_retriever_creator = if is_project_admin { + self.credential_retriever_creators.project_admin.clone() + } else { + None + }; + + self.make_authority_node_client( + &project.authority_identifier().into_diagnostic()?, + project.authority_multiaddr().into_diagnostic()?, + &caller_identifier, + credential_retriever_creator, + ) + .await + .into_diagnostic() + } + + pub async fn create_project_client( + &self, + project_identifier: &Identifier, + project_multiaddr: &MultiAddr, + caller_identity_name: Option, + credentials_enabled: CredentialsEnabled, + ) -> miette::Result { + self.make_project_node_client( + project_identifier, + project_multiaddr, + &self + .get_identifier_by_name(caller_identity_name) + .await + .into_diagnostic()?, + credentials_enabled, + ) + .await + .into_diagnostic() + } +} + +impl NodeManager { + pub(super) async fn access_control( + &self, + authority: Option, + resource: Resource, + action: Action, + expression: Option, + ) -> ockam_core::Result> { + let resource_name_str = resource.resource_name.as_str(); + let resource_type_str = resource.resource_type.to_string(); + let action_str = action.as_ref(); + if let Some(authority) = authority { + // Populate environment with known attributes: + let mut env = Env::new(); + env.put("resource.id", str(resource_name_str)); + env.put("action.id", str(action_str)); + + // Store policy for the given resource and action + let policies = self.cli_state.policies(); + if let Some(expression) = expression { + policies + .store_policy_for_resource_name(&resource.resource_name, &action, &expression) + .await?; + } + self.cli_state.store_resource(&resource).await?; + + // Create the policy access control + let policy_access_control = policies + .make_policy_access_control( + self.cli_state.identities_attributes(), + resource, + action, + env, + authority, + ) + .await?; + cfg_if::cfg_if! { + if #[cfg(feature = "std")] { + let cached_policy_access_control = ockam_core::access_control::CachedIncomingAccessControl::new( + Box::new(policy_access_control)); + Ok(Arc::new(cached_policy_access_control)) + } else { + Ok(Arc::new(policy_access_control)) + } + } + } else { + warn! { + resource_name = resource_name_str, + resource_type = resource_type_str, + action = action_str, + "no policy access control set" + } + Ok(Arc::new(AllowAll)) + } + } +} + +#[derive(Debug)] +pub struct NodeManagerGeneralOptions { + pub(super) cli_state: CliState, + pub(super) node_name: String, + pub(super) start_default_services: bool, + pub(super) persistent: bool, +} + +impl NodeManagerGeneralOptions { + pub fn new( + cli_state: CliState, + node_name: String, + start_default_services: bool, + persistent: bool, + ) -> Self { + Self { + cli_state, + node_name, + start_default_services, + persistent, + } + } +} + +#[derive(Clone)] +/// Transport to build connection +pub struct ApiTransport { + /// Type of transport being requested + pub tt: TransportType, + /// Mode of transport being requested + pub tm: TransportMode, + /// Socket address + pub socket_address: SocketAddr, + /// Worker address + pub worker_address: String, + /// Processor address + pub processor_address: String, + /// FlowControlId + pub flow_control_id: FlowControlId, +} + +#[derive(Debug)] +pub struct NodeManagerTransportOptions { + api_transport_flow_control_id: FlowControlId, + tcp_transport: TcpTransport, +} + +impl NodeManagerTransportOptions { + pub fn new(api_transport_flow_control_id: FlowControlId, tcp_transport: TcpTransport) -> Self { + Self { + api_transport_flow_control_id, + tcp_transport, + } + } +} + +impl NodeManager { + /// Create a new NodeManager with the node name from the ockam CLI + #[instrument(name = "create_node_manager", skip_all, fields(node_name = general_options.node_name))] + pub async fn create( + ctx: &Context, + general_options: NodeManagerGeneralOptions, + transport_options: NodeManagerTransportOptions, + trust_options: NodeManagerTrustOptions, + ) -> ockam_core::Result { + debug!("create transports"); + let api_transport_id = random_alias(); + let mut transports = BTreeMap::new(); + transports.insert( + api_transport_id.clone(), + transport_options.api_transport_flow_control_id.clone(), + ); + + let mut cli_state = general_options.cli_state; + cli_state.set_node_name(general_options.node_name.clone()); + + let secure_channels = cli_state + .secure_channels(&general_options.node_name) + .await?; + + let registry = Arc::new(Registry::default()); + debug!("start the medic"); + let medic_handle = MedicHandle::start_medic(ctx, registry.clone()).await?; + + debug!("retrieve the node identifier"); + let node_identifier = cli_state + .get_node(&general_options.node_name) + .await? + .identifier(); + + debug!("create default resource type policies"); + cli_state + .policies() + .store_default_resource_type_policies() + .await?; + + let project_member_credential_retriever_creator: Option< + Arc, + > = match trust_options.project_member_credential_retriever_options { + NodeManagerCredentialRetrieverOptions::None => None, + NodeManagerCredentialRetrieverOptions::CacheOnly { issuer, scope } => { + Some(Arc::new(CachedCredentialRetrieverCreator::new( + issuer.clone(), + scope, + secure_channels.identities().cached_credentials_repository(), + ))) + } + NodeManagerCredentialRetrieverOptions::Remote { info, scope } => { + Some(Arc::new(RemoteCredentialRetrieverCreator::new( + ctx.async_try_clone().await?, + Arc::new(transport_options.tcp_transport.clone()), + secure_channels.clone(), + info.clone(), + scope, + ))) + } + NodeManagerCredentialRetrieverOptions::InMemory(credential) => { + Some(Arc::new(MemoryCredentialRetrieverCreator::new(credential))) + } + }; + + let project_admin_credential_retriever_creator: Option< + Arc, + > = match trust_options.project_admin_credential_retriever_options { + NodeManagerCredentialRetrieverOptions::None => None, + NodeManagerCredentialRetrieverOptions::CacheOnly { issuer, scope } => { + Some(Arc::new(CachedCredentialRetrieverCreator::new( + issuer.clone(), + scope, + secure_channels.identities().cached_credentials_repository(), + ))) + } + NodeManagerCredentialRetrieverOptions::Remote { info, scope } => { + Some(Arc::new(RemoteCredentialRetrieverCreator::new( + ctx.async_try_clone().await?, + Arc::new(transport_options.tcp_transport.clone()), + secure_channels.clone(), + info.clone(), + scope, + ))) + } + NodeManagerCredentialRetrieverOptions::InMemory(credential) => { + Some(Arc::new(MemoryCredentialRetrieverCreator::new(credential))) + } + }; + + let credential_retriever_creators = CredentialRetrieverCreators { + project_member: project_member_credential_retriever_creator, + project_admin: project_admin_credential_retriever_creator, + _account_admin: None, + }; + + let mut s = Self { + cli_state, + node_name: general_options.node_name, + node_identifier, + api_transport_flow_control_id: transport_options.api_transport_flow_control_id, + tcp_transport: transport_options.tcp_transport, + secure_channels, + credential_retriever_creators, + project_authority: trust_options.project_authority, + registry, + medic_handle, + }; + + debug!("retrieve the node identifier"); + s.initialize_services(ctx, general_options.start_default_services) + .await?; + info!("created a node manager for the node: {}", s.node_name); + + Ok(s) + } + + async fn initialize_default_services( + &self, + ctx: &Context, + api_flow_control_id: &FlowControlId, + ) -> ockam_core::Result<()> { + // Start services + ctx.flow_controls() + .add_consumer(DefaultAddress::UPPERCASE_SERVICE, api_flow_control_id); + self.start_uppercase_service_impl(ctx, DefaultAddress::UPPERCASE_SERVICE.into()) + .await?; + + RelayService::create( + ctx, + DefaultAddress::RELAY_SERVICE, + RelayServiceOptions::new() + .service_as_consumer(api_flow_control_id) + .relay_as_consumer(api_flow_control_id), + ) + .await?; + + self.create_secure_channel_listener( + DefaultAddress::SECURE_CHANNEL_LISTENER.into(), + None, // Not checking identifiers here in favor of credential check + None, + ctx, + ) + .await?; + + Ok(()) + } + + async fn initialize_services( + &mut self, + ctx: &Context, + start_default_services: bool, + ) -> ockam_core::Result<()> { + let api_flow_control_id = self.api_transport_flow_control_id.clone(); + + if start_default_services { + self.initialize_default_services(ctx, &api_flow_control_id) + .await?; + } + + // Always start the echoer service as ockam_api::Medic assumes it will be + // started unconditionally on every node. It's used for liveliness checks. + ctx.flow_controls() + .add_consumer(DefaultAddress::ECHO_SERVICE, &api_flow_control_id); + self.start_echoer_service(ctx, DefaultAddress::ECHO_SERVICE.into()) + .await?; + + Ok(()) + } + + pub async fn make_connection( + &self, + ctx: Arc, + addr: &MultiAddr, + identifier: Identifier, + authorized: Option, + timeout: Option, + ) -> ockam_core::Result { + let authorized = authorized.map(|authorized| vec![authorized]); + self.connect(ctx, addr, identifier, authorized, timeout) + .await + } + + /// Resolve project ID (if any), create secure channel (if needed) and create a tcp connection + /// Returns [`Connection`] + async fn connect( + &self, + ctx: Arc, + addr: &MultiAddr, + identifier: Identifier, + authorized: Option>, + timeout: Option, + ) -> ockam_core::Result { + debug!(?timeout, "connecting to {}", &addr); + let connection = ConnectionBuilder::new(addr.clone()) + .instantiate( + ctx.clone(), + self, + ProjectInstantiator::new(identifier.clone(), timeout), + ) + .await? + .instantiate(ctx.clone(), self, PlainTcpInstantiator::new()) + .await? + .instantiate( + ctx.clone(), + self, + SecureChannelInstantiator::new(&identifier, timeout, authorized), + ) + .await? + .build(); + connection.add_default_consumers(ctx); + + debug!("connected to {connection:?}"); + Ok(connection) + } + + pub(crate) async fn resolve_project( + &self, + name: &str, + ) -> ockam_core::Result<(MultiAddr, Identifier)> { + let project = self.cli_state.projects().get_project_by_name(name).await?; + Ok(( + project.project_multiaddr()?.clone(), + project.project_identifier()?, + )) + } +} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs index 6382621f235..3e6c9379c6d 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/node_services.rs @@ -196,7 +196,7 @@ impl NodeManager { let ac = self .access_control( - self.authority(), + self.project_authority(), Resource::new(addr.address(), ResourceType::Echoer), Action::HandleMessage, None, diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs index 494c7ce236b..a7a698a05ab 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/portals.rs @@ -209,7 +209,7 @@ impl NodeManager { OutletAccessControl::IncomingAccessControl(iac) => iac, OutletAccessControl::PolicyExpression(expression) => { self.access_control( - self.authority(), + self.project_authority(), Resource::new(worker_addr.address(), ResourceType::TcpOutlet), Action::HandleMessage, expression, @@ -220,7 +220,7 @@ impl NodeManager { let options = { let options = TcpOutletOptions::new().with_incoming_access_control(access_control); - let options = if self.authority().is_none() { + let options = if self.project_authority().is_none() { options.as_consumer(&self.api_transport_flow_control_id) } else { options @@ -625,7 +625,7 @@ impl SessionReplacer for InletSessionReplacer { None } } - .or(self.node_manager.authority()); + .or(self.node_manager.project_authority()); self.node_manager .access_control( diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs index 1a6e367f89d..4a576489d92 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/relay.rs @@ -3,6 +3,7 @@ use std::time::Duration; use miette::IntoDiagnostic; +use ockam::identity::models::CredentialAndPurposeKey; use ockam::identity::Identifier; use ockam::remote::{RemoteRelay, RemoteRelayOptions}; use ockam::Result; @@ -375,6 +376,7 @@ pub trait SecureChannelsCreation { addr: &MultiAddr, authorized: Identifier, identity_name: Option, + credential: Option, timeout: Option, ) -> miette::Result
; } @@ -387,6 +389,7 @@ impl SecureChannelsCreation for InMemoryNode { addr: &MultiAddr, authorized: Identifier, identity_name: Option, + credential: Option, timeout: Option, ) -> miette::Result
{ self.node_manager @@ -395,6 +398,7 @@ impl SecureChannelsCreation for InMemoryNode { addr.clone(), identity_name, Some(vec![authorized]), + credential, timeout, ) .await @@ -411,9 +415,15 @@ impl SecureChannelsCreation for BackgroundNodeClient { addr: &MultiAddr, authorized: Identifier, identity_name: Option, + credential: Option, timeout: Option, ) -> miette::Result
{ - let body = CreateSecureChannelRequest::new(addr, Some(vec![authorized]), identity_name); + let body = CreateSecureChannelRequest::new( + addr, + Some(vec![authorized]), + identity_name, + credential, + ); let request = Request::post("/node/secure_channel").body(body); let response: CreateSecureChannelResponse = if let Some(t) = timeout { self.ask_with_timeout(ctx, request, t).await? diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs index 3bb50207434..7afb8d0ff37 100644 --- a/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/secure_channel.rs @@ -1,5 +1,6 @@ use std::time::Duration; +use ockam::identity::models::CredentialAndPurposeKey; use ockam::identity::TrustEveryonePolicy; use ockam::identity::Vault; use ockam::identity::{ @@ -46,12 +47,20 @@ impl NodeManagerWorker { authorized_identifiers, timeout, identity_name: identity, + credential, .. } = create_secure_channel; let response = self .node_manager - .create_secure_channel(ctx, addr, identity, authorized_identifiers, timeout) + .create_secure_channel( + ctx, + addr, + identity, + authorized_identifiers, + credential, + timeout, + ) .await .map(|secure_channel| { Response::ok().body(CreateSecureChannelResponse::new(secure_channel)) @@ -163,6 +172,7 @@ impl NodeManager { addr: MultiAddr, identity_name: Option, authorized_identifiers: Option>, + credential: Option, timeout: Option, ) -> Result { let identifier = self.get_identifier_by_name(identity_name.clone()).await?; @@ -177,6 +187,7 @@ impl NodeManager { connection.route()?, &identifier, authorized_identifiers, + credential, timeout, ) .await?; @@ -191,6 +202,7 @@ impl NodeManager { sc_route: Route, identifier: &Identifier, authorized_identifiers: Option>, + credential: Option, timeout: Option, ) -> Result { debug!(%sc_route, "Creating secure channel"); @@ -202,15 +214,18 @@ impl NodeManager { options }; - let options = match self.authority() { - Some(authority) => options.with_authority(authority), + let options = match self.project_authority() { + Some(project_authority) => options.with_authority(project_authority), None => options, }; - let options = match self.credential_retriever_creator.as_ref() { - None => options, - Some(credential_retriever_creator) => { - options.with_credential_retriever_creator(credential_retriever_creator.clone())? + let options = if let Some(credential) = credential { + options.with_credential(credential)? + } else { + match self.credential_retriever_creators.project_member.as_ref() { + None => options, + Some(credential_retriever_creator) => options + .with_credential_retriever_creator(credential_retriever_creator.clone())?, } }; @@ -310,12 +325,12 @@ impl NodeManager { None => options.with_trust_policy(TrustEveryonePolicy), }; - let options = match self.authority() { - Some(authority) => options.with_authority(authority), + let options = match self.project_authority() { + Some(project_authority) => options.with_authority(project_authority), None => options, }; - let options = match self.credential_retriever_creator.as_ref() { + let options = match self.credential_retriever_creators.project_member.as_ref() { None => options, Some(credential_retriever_creator) => { options.with_credential_retriever_creator(credential_retriever_creator.clone())? diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/trust.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/trust.rs new file mode 100644 index 00000000000..f389d03f58e --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/trust.rs @@ -0,0 +1,108 @@ +use ockam::identity::models::CredentialAndPurposeKey; +use ockam::identity::{CredentialRetrieverCreator, Identifier, RemoteCredentialRetrieverInfo}; +use ockam_core::errcode::{Kind, Origin}; +use std::fmt::Display; +use std::str::FromStr; +use std::sync::Arc; + +pub const PROJECT_MEMBER_SCOPE_PREFIX: &str = "project-member-"; +pub const PROJECT_ADMIN_SCOPE_PREFIX: &str = "project-admin-"; +pub const ACCOUNT_ADMIN_SCOPE_PREFIX: &str = "account-admin-"; + +#[derive(Clone)] +pub struct CredentialRetrieverCreators { + pub(crate) project_member: Option>, + pub(crate) project_admin: Option>, + pub(crate) _account_admin: Option>, +} + +#[derive(Debug)] +pub enum CredentialScope { + ProjectMember { project_id: String }, + ProjectAdmin { project_id: String }, + AccountAdmin { account_id: String }, +} + +impl FromStr for CredentialScope { + type Err = ockam_core::Error; + + fn from_str(s: &str) -> Result { + if let Some(project_id) = s.strip_prefix(PROJECT_MEMBER_SCOPE_PREFIX) { + return Ok(CredentialScope::ProjectMember { + project_id: project_id.to_string(), + }); + } + + if let Some(project_id) = s.strip_prefix(PROJECT_ADMIN_SCOPE_PREFIX) { + return Ok(CredentialScope::ProjectAdmin { + project_id: project_id.to_string(), + }); + } + + if let Some(account_id) = s.strip_prefix(ACCOUNT_ADMIN_SCOPE_PREFIX) { + return Ok(CredentialScope::AccountAdmin { + account_id: account_id.to_string(), + }); + } + + Err(ockam_core::Error::new( + Origin::Api, + Kind::Invalid, + "Invalid credential scope format", + )) + } +} + +impl Display for CredentialScope { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + CredentialScope::ProjectMember { project_id } => { + format!("{}{}", PROJECT_MEMBER_SCOPE_PREFIX, project_id) + } + CredentialScope::ProjectAdmin { project_id } => { + format!("{}{}", PROJECT_ADMIN_SCOPE_PREFIX, project_id) + } + CredentialScope::AccountAdmin { account_id } => { + format!("{}{}", ACCOUNT_ADMIN_SCOPE_PREFIX, account_id) + } + }; + write!(f, "{}", str) + } +} + +#[derive(Debug)] +pub enum NodeManagerCredentialRetrieverOptions { + None, + CacheOnly { + issuer: Identifier, + scope: String, + }, + Remote { + info: RemoteCredentialRetrieverInfo, + scope: String, + }, + InMemory(CredentialAndPurposeKey), +} + +pub struct NodeManagerTrustOptions { + pub(super) project_member_credential_retriever_options: NodeManagerCredentialRetrieverOptions, + pub(super) project_authority: Option, + pub(super) project_admin_credential_retriever_options: NodeManagerCredentialRetrieverOptions, + pub(super) _account_admin_credential_retriever_options: NodeManagerCredentialRetrieverOptions, +} + +impl NodeManagerTrustOptions { + pub fn new( + project_member_credential_retriever_options: NodeManagerCredentialRetrieverOptions, + project_admin_credential_retriever_options: NodeManagerCredentialRetrieverOptions, + project_authority: Option, + account_admin_credential_retriever_options: NodeManagerCredentialRetrieverOptions, + ) -> Self { + Self { + project_member_credential_retriever_options, + project_admin_credential_retriever_options, + project_authority, + _account_admin_credential_retriever_options: account_admin_credential_retriever_options, + } + } +} diff --git a/implementations/rust/ockam/ockam_api/src/nodes/service/worker.rs b/implementations/rust/ockam/ockam_api/src/nodes/service/worker.rs new file mode 100644 index 00000000000..b8aec1fb7fd --- /dev/null +++ b/implementations/rust/ockam/ockam_api/src/nodes/service/worker.rs @@ -0,0 +1,293 @@ +use crate::nodes::models::policies::SetPolicyRequest; +use crate::nodes::registry::KafkaServiceKind; +use crate::nodes::service::{encode_response, TARGET}; +use crate::nodes::{InMemoryNode, NODEMANAGER_ADDR}; +use crate::DefaultAddress; +use minicbor::Decoder; +use ockam_core::api::{RequestHeader, Response}; +use ockam_core::{Address, Routed, Worker}; +use ockam_node::Context; +use std::error::Error; +use std::sync::Arc; + +#[derive(Clone)] +pub struct NodeManagerWorker { + pub node_manager: Arc, +} + +impl NodeManagerWorker { + pub fn new(node_manager: Arc) -> Self { + NodeManagerWorker { node_manager } + } + + pub async fn stop(&self, ctx: &Context) -> ockam_core::Result<()> { + self.node_manager.stop(ctx).await?; + ctx.stop_worker(NODEMANAGER_ADDR).await?; + Ok(()) + } +} + +impl NodeManagerWorker { + //////// Request matching and response handling //////// + + #[instrument(skip_all, fields(method = ?req.method(), path = req.path()))] + async fn handle_request( + &mut self, + ctx: &mut Context, + req: &RequestHeader, + dec: &mut Decoder<'_>, + ) -> ockam_core::Result> { + debug! { + target: TARGET, + id = %req.id(), + method = ?req.method(), + path = %req.path(), + body = %req.has_body(), + "request" + } + + use ockam_core::api::Method::*; + let path = req.path(); + let path_segments = req.path_segments::<5>(); + let method = match req.method() { + Some(m) => m, + None => todo!(), + }; + + let r = match (method, path_segments.as_slice()) { + // ==*== Basic node information ==*== + // TODO: create, delete, destroy remote nodes + (Get, ["node"]) => encode_response(req, self.get_node_status(ctx).await)?, + + // ==*== Tcp Connection ==*== + (Get, ["node", "tcp", "connection"]) => self.get_tcp_connections(req).await.to_vec()?, + (Get, ["node", "tcp", "connection", address]) => { + encode_response(req, self.get_tcp_connection(address.to_string()).await)? + } + (Post, ["node", "tcp", "connection"]) => { + encode_response(req, self.create_tcp_connection(ctx, dec.decode()?).await)? + } + (Delete, ["node", "tcp", "connection"]) => { + encode_response(req, self.delete_tcp_connection(dec.decode()?).await)? + } + + // ==*== Tcp Listeners ==*== + (Get, ["node", "tcp", "listener"]) => self.get_tcp_listeners(req).await.to_vec()?, + (Get, ["node", "tcp", "listener", address]) => { + encode_response(req, self.get_tcp_listener(address.to_string()).await)? + } + (Post, ["node", "tcp", "listener"]) => { + encode_response(req, self.create_tcp_listener(dec.decode()?).await)? + } + (Delete, ["node", "tcp", "listener"]) => { + encode_response(req, self.delete_tcp_listener(dec.decode()?).await)? + } + + // ==*== Secure channels ==*== + (Get, ["node", "secure_channel"]) => { + encode_response(req, self.list_secure_channels().await)? + } + (Get, ["node", "secure_channel_listener"]) => { + encode_response(req, self.list_secure_channel_listener().await)? + } + (Post, ["node", "secure_channel"]) => { + encode_response(req, self.create_secure_channel(dec.decode()?, ctx).await)? + } + (Delete, ["node", "secure_channel"]) => { + encode_response(req, self.delete_secure_channel(dec.decode()?, ctx).await)? + } + (Get, ["node", "show_secure_channel"]) => { + encode_response(req, self.show_secure_channel(dec.decode()?).await)? + } + (Post, ["node", "secure_channel_listener"]) => encode_response( + req, + self.create_secure_channel_listener(dec.decode()?, ctx) + .await, + )?, + (Delete, ["node", "secure_channel_listener"]) => encode_response( + req, + self.delete_secure_channel_listener(dec.decode()?, ctx) + .await, + )?, + (Get, ["node", "show_secure_channel_listener"]) => { + encode_response(req, self.show_secure_channel_listener(dec.decode()?).await)? + } + + // ==*== Services ==*== + (Post, ["node", "services", DefaultAddress::UPPERCASE_SERVICE]) => { + encode_response(req, self.start_uppercase_service(ctx, dec.decode()?).await)? + } + (Post, ["node", "services", DefaultAddress::ECHO_SERVICE]) => { + encode_response(req, self.start_echoer_service(ctx, dec.decode()?).await)? + } + (Post, ["node", "services", DefaultAddress::HOP_SERVICE]) => { + encode_response(req, self.start_hop_service(ctx, dec.decode()?).await)? + } + (Post, ["node", "services", DefaultAddress::KAFKA_OUTLET]) => encode_response( + req, + self.start_kafka_outlet_service(ctx, dec.decode()?).await, + )?, + (Delete, ["node", "services", DefaultAddress::KAFKA_OUTLET]) => encode_response( + req, + self.delete_kafka_service(ctx, dec.decode()?, KafkaServiceKind::Outlet) + .await, + )?, + (Post, ["node", "services", DefaultAddress::KAFKA_CONSUMER]) => encode_response( + req, + self.start_kafka_consumer_service(ctx, dec.decode()?).await, + )?, + (Delete, ["node", "services", DefaultAddress::KAFKA_CONSUMER]) => encode_response( + req, + self.delete_kafka_service(ctx, dec.decode()?, KafkaServiceKind::Consumer) + .await, + )?, + (Post, ["node", "services", DefaultAddress::KAFKA_PRODUCER]) => encode_response( + req, + self.start_kafka_producer_service(ctx, dec.decode()?).await, + )?, + (Delete, ["node", "services", DefaultAddress::KAFKA_PRODUCER]) => encode_response( + req, + self.delete_kafka_service(ctx, dec.decode()?, KafkaServiceKind::Producer) + .await, + )?, + (Post, ["node", "services", DefaultAddress::KAFKA_DIRECT]) => encode_response( + req, + self.start_kafka_direct_service(ctx, dec.decode()?).await, + )?, + (Delete, ["node", "services", DefaultAddress::KAFKA_DIRECT]) => encode_response( + req, + self.delete_kafka_service(ctx, dec.decode()?, KafkaServiceKind::Direct) + .await, + )?, + (Get, ["node", "services"]) => encode_response(req, self.list_services().await)?, + (Get, ["node", "services", service_type]) => { + encode_response(req, self.list_services_of_type(service_type).await)? + } + + // ==*== Relay commands ==*== + (Get, ["node", "relay", alias]) => { + encode_response(req, self.show_relay(req, alias).await)? + } + (Get, ["node", "relay"]) => encode_response(req, self.get_relays(req).await)?, + (Delete, ["node", "relay", alias]) => { + encode_response(req, self.delete_relay(req, alias).await)? + } + (Post, ["node", "relay"]) => { + encode_response(req, self.create_relay(ctx, req, dec.decode()?).await)? + } + + // ==*== Inlets & Outlets ==*== + (Get, ["node", "inlet"]) => encode_response(req, self.get_inlets().await)?, + (Get, ["node", "inlet", alias]) => encode_response(req, self.show_inlet(alias).await)?, + (Get, ["node", "outlet"]) => self.get_outlets(req).await.to_vec()?, + (Get, ["node", "outlet", addr]) => { + let addr: Address = addr.to_string().into(); + encode_response(req, self.show_outlet(&addr).await)? + } + (Post, ["node", "inlet"]) => { + encode_response(req, self.create_inlet(ctx, dec.decode()?).await)? + } + (Post, ["node", "outlet"]) => { + encode_response(req, self.create_outlet(ctx, dec.decode()?).await)? + } + (Delete, ["node", "outlet", addr]) => { + let addr: Address = addr.to_string().into(); + encode_response(req, self.delete_outlet(&addr).await)? + } + (Delete, ["node", "inlet", alias]) => { + encode_response(req, self.delete_inlet(alias).await)? + } + (Delete, ["node", "portal"]) => todo!(), + + // ==*== Flow Controls ==*== + (Post, ["node", "flow_controls", "add_consumer"]) => { + encode_response(req, self.add_consumer(ctx, dec.decode()?).await)? + } + + // ==*== Workers ==*== + (Get, ["node", "workers"]) => encode_response(req, self.list_workers(ctx).await)?, + + // ==*== Policies ==*== + (Post, ["policy", action]) => { + let payload: SetPolicyRequest = dec.decode()?; + encode_response( + req, + self.add_policy(action, payload.resource, payload.expression) + .await, + )? + } + (Get, ["policy", action]) => { + encode_response(req, self.get_policy(action, dec.decode()?).await)? + } + (Get, ["policy"]) => encode_response(req, self.list_policies(dec.decode()?).await)?, + (Delete, ["policy", action]) => { + encode_response(req, self.delete_policy(action, dec.decode()?).await)? + } + + // ==*== Messages ==*== + (Post, ["v0", "message"]) => { + encode_response(req, self.send_message(ctx, dec.decode()?).await)? + } + + // ==*== Catch-all for Unimplemented APIs ==*== + _ => { + warn!(%method, %path, "Called invalid endpoint"); + Response::bad_request(req, &format!("Invalid endpoint: {} {}", method, path)) + .to_vec()? + } + }; + Ok(r) + } +} + +#[ockam::worker] +impl Worker for NodeManagerWorker { + type Message = Vec; + type Context = Context; + + async fn shutdown(&mut self, ctx: &mut Self::Context) -> ockam_core::Result<()> { + self.node_manager.medic_handle.stop_medic(ctx).await + } + + async fn handle_message( + &mut self, + ctx: &mut Context, + msg: Routed>, + ) -> ockam_core::Result<()> { + let return_route = msg.return_route(); + let body = msg.into_body()?; + let mut dec = Decoder::new(&body); + let req: RequestHeader = match dec.decode() { + Ok(r) => r, + Err(e) => { + error!("Failed to decode request: {:?}", e); + return Ok(()); + } + }; + + let r = match self.handle_request(ctx, &req, &mut dec).await { + Ok(r) => r, + Err(err) => { + error! { + target: TARGET, + re = %req.id(), + method = ?req.method(), + path = %req.path(), + code = %err.code(), + cause = ?err.source(), + "failed to handle request" + } + Response::internal_error(&req, &format!("failed to handle request: {err} {req:?}")) + .to_vec()? + } + }; + debug! { + target: TARGET, + re = %req.id(), + method = ?req.method(), + path = %req.path(), + "responding" + } + ctx.send(return_route, r).await + } +} diff --git a/implementations/rust/ockam/ockam_api/src/test_utils/mod.rs b/implementations/rust/ockam/ockam_api/src/test_utils/mod.rs index f14aec896e2..a20d68f8ba2 100644 --- a/implementations/rust/ockam/ockam_api/src/test_utils/mod.rs +++ b/implementations/rust/ockam/ockam_api/src/test_utils/mod.rs @@ -101,7 +101,9 @@ pub async fn start_manager_for_tests( trust_options.unwrap_or_else(|| { NodeManagerTrustOptions::new( NodeManagerCredentialRetrieverOptions::InMemory(credential), + NodeManagerCredentialRetrieverOptions::None, Some(identifier), + NodeManagerCredentialRetrieverOptions::None, ) }), ) @@ -205,8 +207,10 @@ impl TestNode { &mut context, listen_addr, Some(NodeManagerTrustOptions::new( + NodeManagerCredentialRetrieverOptions::None, NodeManagerCredentialRetrieverOptions::None, None, + NodeManagerCredentialRetrieverOptions::None, )), ) .await diff --git a/implementations/rust/ockam/ockam_api/tests/common/common.rs b/implementations/rust/ockam/ockam_api/tests/common/common.rs index b512d81dd32..a0a02191259 100644 --- a/implementations/rust/ockam/ockam_api/tests/common/common.rs +++ b/implementations/rust/ockam/ockam_api/tests/common/common.rs @@ -39,6 +39,7 @@ pub async fn default_configuration() -> Result { okta: None, account_authority: None, enforce_admin_checks: false, + disable_trust_context_id: false, }; // Hack to create Authority Identity using the same vault and storage diff --git a/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs b/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs index 3ab87c728c9..a771f302a40 100644 --- a/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs +++ b/implementations/rust/ockam/ockam_api/tests/credential_issuer.rs @@ -68,11 +68,13 @@ async fn credential(ctx: &mut Context) -> Result<()> { .add_consumer(auth_worker_addr.clone(), &sc_flow_control_id); let auth = CredentialIssuerWorker::new( members, + identities.identities_attributes(), identities.credentials(), &auth_identifier, + "test".to_string(), None, None, - None, + true, ); ctx.start_worker(auth_worker_addr.clone(), auth).await?; diff --git a/implementations/rust/ockam/ockam_api/tests/latency.rs b/implementations/rust/ockam/ockam_api/tests/latency.rs index 8c70db1d5fc..fd520dfafc6 100644 --- a/implementations/rust/ockam/ockam_api/tests/latency.rs +++ b/implementations/rust/ockam/ockam_api/tests/latency.rs @@ -44,6 +44,7 @@ pub fn measure_message_latency_two_nodes() { None, None, None, + None, ) .await .unwrap(); diff --git a/implementations/rust/ockam/ockam_app_lib/src/enroll/enroll_user.rs b/implementations/rust/ockam/ockam_app_lib/src/enroll/enroll_user.rs index 781d5aeae98..40f92ce2b2d 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/enroll/enroll_user.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/enroll/enroll_user.rs @@ -136,7 +136,7 @@ impl AppState { let node = cli_state.get_node(NODE_NAME).await?; let identifier = node.identifier(); cli_state - .set_identifier_as_enrolled(&identifier) + .set_identifier_as_enrolled(&identifier, &user_info.email) .await .into_diagnostic()?; info!(%identifier, "User enrolled successfully"); diff --git a/implementations/rust/ockam/ockam_app_lib/src/projects/commands.rs b/implementations/rust/ockam/ockam_app_lib/src/projects/commands.rs index 138d80eaeae..703a9265725 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/projects/commands.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/projects/commands.rs @@ -29,11 +29,7 @@ impl AppState { .ok_or_else(|| Error::ProjectNotFound(project_id.to_owned()))? .clone(); let authority_node = self - .authority_node( - &project.authority_identifier().into_diagnostic()?, - project.authority_multiaddr().into_diagnostic()?, - None, - ) + .authority_node(&project, None) .await .into_diagnostic()?; let otc = authority_node diff --git a/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/invitation_access_control.rs b/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/invitation_access_control.rs index 445559385b8..083a07bc077 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/invitation_access_control.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/shared_service/tcp_outlet/invitation_access_control.rs @@ -63,19 +63,20 @@ impl AppState { .await? .identifier(); - let authority = node_manager - .authority() - .ok_or(ockam_core::Error::new_unknown( - Origin::Application, - "NodeManager has no authority", - ))?; + let project_authority = + node_manager + .project_authority() + .ok_or(ockam_core::Error::new_unknown( + Origin::Application, + "NodeManager has no authority", + ))?; Ok(Arc::new(InvitationAccessControl::new( outlet_worker_addr, identities_attributes, invitations, local_identity, - authority, + project_authority, ))) } } diff --git a/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs b/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs index dea2fd192d4..fe242f942ed 100644 --- a/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs +++ b/implementations/rust/ockam/ockam_app_lib/src/state/mod.rs @@ -7,7 +7,6 @@ use tokio::sync::RwLock; use tracing::{error, info, trace, warn}; pub use kind::StateKind; -use ockam::identity::Identifier; use ockam::Context; use ockam::{NodeBuilder, TcpListenerOptions, TcpTransport}; use ockam_api::cli_state::CliState; @@ -19,7 +18,6 @@ use ockam_api::nodes::models::portal::OutletStatus; use ockam_api::nodes::service::{NodeManagerGeneralOptions, NodeManagerTransportOptions}; use ockam_api::nodes::{BackgroundNodeClient, InMemoryNode, NodeManagerWorker, NODEMANAGER_ADDR}; use ockam_core::AsyncTryClone; -use ockam_multiaddr::MultiAddr; use crate::api::notification::rust::{Notification, NotificationCallback}; use crate::api::state::rust::{ @@ -379,18 +377,12 @@ impl AppState { pub async fn authority_node( &self, - authority_identifier: &Identifier, - authority_route: &MultiAddr, + project: &Project, caller_identity_name: Option, ) -> Result { let node_manager = self.node_manager.read().await; Ok(node_manager - .create_authority_client( - authority_identifier, - authority_route, - caller_identity_name, - None, - ) + .create_authority_client(project, caller_identity_name) .await?) } @@ -708,7 +700,7 @@ pub(crate) async fn make_node_manager( .await?; let trust_options = cli_state - .retrieve_trust_options(&None, &None, &None, false) + .retrieve_trust_options(&None, &None, &None, &None) .await .into_diagnostic()?; diff --git a/implementations/rust/ockam/ockam_command/src/authority/create.rs b/implementations/rust/ockam/ockam_command/src/authority/create.rs index b532ff767c2..3acd1938c86 100644 --- a/implementations/rust/ockam/ockam_command/src/authority/create.rs +++ b/implementations/rust/ockam/ockam_command/src/authority/create.rs @@ -101,9 +101,14 @@ pub struct CreateCommand { #[arg(long, value_name = "ACCOUNT_AUTHORITY_CHANGE_HISTORY", default_value = None)] account_authority: Option, - /// Enforce distintion between admins and enrollers + /// Enforce distinction between admins and enrollers #[arg(long, value_name = "ENFORCE_ADMIN_CHECKS", default_value_t = false)] enforce_admin_checks: bool, + + /// Not include trust context id and project id into the credential + /// TODO: Set to true after old clients are updated + #[arg(long, value_name = "DISABLE_TRUST_CONTEXT_ID", default_value_t = false)] + disable_trust_context_id: bool, } impl CreateCommand { @@ -192,6 +197,9 @@ impl CreateCommand { if self.enforce_admin_checks { args.push("--enforce-admin-checks".to_string()); } + if self.disable_trust_context_id { + args.push("--disable_trust_context_id".to_string()); + } args.push(self.node_name.to_string()); run_ockam(args).await @@ -318,6 +326,7 @@ impl CreateCommand { okta: okta_configuration, account_authority, enforce_admin_checks: self.enforce_admin_checks, + disable_trust_context_id: self.disable_trust_context_id, }; authority_node::start_node(ctx, &configuration) diff --git a/implementations/rust/ockam/ockam_command/src/credential/list.rs b/implementations/rust/ockam/ockam_command/src/credential/list.rs index 687e6bc705c..f4ed4e574c1 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/list.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/list.rs @@ -51,7 +51,7 @@ impl ListCommand { let credentials = credentials .into_iter() - .map(|c| CredentialOutput::from_credential(c, true)) + .map(|c| CredentialOutput::from_credential(c.0, c.1, true)) .collect::>>()?; let list = opts.terminal.build_list( diff --git a/implementations/rust/ockam/ockam_command/src/credential/mod.rs b/implementations/rust/ockam/ockam_command/src/credential/mod.rs index 85cb0e21493..f569ae5619b 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/mod.rs @@ -65,6 +65,7 @@ impl CredentialCommand { pub struct CredentialOutput { credential: String, + scope: String, subject: Identifier, issuer: Identifier, created_at: TimestampInSeconds, @@ -75,7 +76,11 @@ pub struct CredentialOutput { } impl CredentialOutput { - pub fn from_credential(credential: CredentialAndPurposeKey, is_verified: bool) -> Result { + pub fn from_credential( + credential: CredentialAndPurposeKey, + scope: String, + is_verified: bool, + ) -> Result { let str = hex::encode(credential.encode_as_cbor_bytes()?); let credential_data = credential.credential.get_credential_data()?; let purpose_key_data = credential.purpose_key_attestation.get_attestation_data()?; @@ -98,6 +103,7 @@ impl CredentialOutput { let s = Self { credential: str, + scope, subject, issuer: purpose_key_data.subject, created_at: credential_data.created_at, @@ -123,6 +129,7 @@ impl Output for CredentialOutput { let output = format!( "Credential:\n\ + \tscope: {scope}\n\ \tsubject: {subject}\n\ \tissuer: {issuer}\n\ \tis_verified: {is_verified}\n\ @@ -131,6 +138,7 @@ impl Output for CredentialOutput { \tschema: {schema}\n\ \tattributes: {attributes}\n\ \tbinary: {credential}", + scope = self.scope, subject = self.subject, issuer = self.issuer, is_verified = is_verified, diff --git a/implementations/rust/ockam/ockam_command/src/credential/store.rs b/implementations/rust/ockam/ockam_command/src/credential/store.rs index 7a9a6d3de1b..198c97ac265 100644 --- a/implementations/rust/ockam/ockam_command/src/credential/store.rs +++ b/implementations/rust/ockam/ockam_command/src/credential/store.rs @@ -21,6 +21,12 @@ pub struct StoreCommand { #[arg(long = "issuer", value_name = "IDENTIFIER", value_parser = identity_identifier_parser)] pub issuer: Identifier, + /// Scope is used to separate credentials given they have the same Issuer&Subject Identifiers + /// Scope can be an arbitrary value, however project admin, project member, and account admin + /// credentials have scope of a specific format. See [`CredentialScope`] + #[arg(long = "scope", value_name = "CREDENTIAL_SCOPE")] + pub scope: String, + #[arg(group = "credential_value", value_name = "CREDENTIAL_STRING", long)] pub credential: Option, @@ -100,6 +106,7 @@ impl StoreCommand { .put( &subject, &purpose_key_data.subject, + &self.scope, credential_data.expires_at, credential.clone(), ) diff --git a/implementations/rust/ockam/ockam_command/src/enroll/command.rs b/implementations/rust/ockam/ockam_command/src/enroll/command.rs index 3961f939c8b..6a19fdd8d9c 100644 --- a/implementations/rust/ockam/ockam_command/src/enroll/command.rs +++ b/implementations/rust/ockam/ockam_command/src/enroll/command.rs @@ -287,7 +287,7 @@ impl EnrollCommand { .await .wrap_err("Failed to enroll your local Identity with Ockam Orchestrator")?; opts.state - .set_identifier_as_enrolled(&node.identifier()) + .set_identifier_as_enrolled(&node.identifier(), &user_info.email) .await .wrap_err("Unable to set your local Identity as enrolled")?; diff --git a/implementations/rust/ockam/ockam_command/src/node/create/foreground.rs b/implementations/rust/ockam/ockam_command/src/node/create/foreground.rs index b08677171fd..082c73f56c3 100644 --- a/implementations/rust/ockam/ockam_command/src/node/create/foreground.rs +++ b/implementations/rust/ockam/ockam_command/src/node/create/foreground.rs @@ -76,7 +76,7 @@ impl CreateCommand { &self.trust_opts.project_name, &self.trust_opts.authority_identity, &self.trust_opts.authority_route, - self.trust_opts.expect_cached_credential, + &self.trust_opts.credential_scope, ) .await .into_diagnostic()?; diff --git a/implementations/rust/ockam/ockam_command/src/node/util.rs b/implementations/rust/ockam/ockam_command/src/node/util.rs index 2e0de933e1c..b235567f6e2 100644 --- a/implementations/rust/ockam/ockam_command/src/node/util.rs +++ b/implementations/rust/ockam/ockam_command/src/node/util.rs @@ -78,7 +78,7 @@ pub async fn spawn_node(opts: &CommandGlobalOpts, cmd: CreateCommand) -> miette: project_name, authority_identity, authority_route, - expect_cached_credential, + credential_scope, } = trust_opts; let mut args = vec![ @@ -94,8 +94,9 @@ pub async fn spawn_node(opts: &CommandGlobalOpts, cmd: CreateCommand) -> miette: "--child-process".to_string(), ]; - if expect_cached_credential { - args.push("--expect-cached-credential".to_string()); + if let Some(credential_scope) = credential_scope { + args.push("--credential-scope".to_string()); + args.push(credential_scope) } if skip_is_running_check { @@ -108,7 +109,7 @@ pub async fn spawn_node(opts: &CommandGlobalOpts, cmd: CreateCommand) -> miette: if let Some(identity_name) = identity_name { args.push("--identity".to_string()); - args.push(identity_name.to_string()); + args.push(identity_name); } if let Some(config) = launch_config { diff --git a/implementations/rust/ockam/ockam_command/src/project/enroll.rs b/implementations/rust/ockam/ockam_command/src/project/enroll.rs index 2f99dba9628..0f8eb3dbecf 100644 --- a/implementations/rust/ockam/ockam_command/src/project/enroll.rs +++ b/implementations/rust/ockam/ockam_command/src/project/enroll.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use clap::Args; use colorful::Colorful; +use miette::miette; use miette::Context as _; -use miette::{miette, IntoDiagnostic}; use ockam::Context; use ockam_api::cli_state::enrollments::EnrollmentTicket; @@ -72,12 +72,7 @@ impl Command for EnrollCommand { ) .await?; let authority_node_client = node - .create_authority_client( - &project.authority_identifier().into_diagnostic()?, - project.authority_multiaddr().into_diagnostic()?, - Some(identity.name()), - None, - ) + .create_authority_client(&project, Some(identity.name())) .await?; // Enroll diff --git a/implementations/rust/ockam/ockam_command/src/project/ticket.rs b/implementations/rust/ockam/ockam_command/src/project/ticket.rs index 22f877cf5bb..1c6774d1f89 100644 --- a/implementations/rust/ockam/ockam_command/src/project/ticket.rs +++ b/implementations/rust/ockam/ockam_command/src/project/ticket.rs @@ -5,17 +5,14 @@ use clap::Args; use colorful::Colorful; use miette::{miette, IntoDiagnostic}; -use ockam::identity::Identifier; use ockam::Context; use ockam_api::authenticator::direct::{ - Members, OCKAM_ROLE_ATTRIBUTE_ENROLLER_VALUE, OCKAM_ROLE_ATTRIBUTE_KEY, + OCKAM_ROLE_ATTRIBUTE_ENROLLER_VALUE, OCKAM_ROLE_ATTRIBUTE_KEY, }; use ockam_api::authenticator::enrollment_tokens::TokenIssuer; use ockam_api::cli_state::enrollments::EnrollmentTicket; -use ockam_api::cli_state::CliState; -use ockam_api::cloud::project::Project; use ockam_api::nodes::InMemoryNode; -use ockam_multiaddr::{proto, MultiAddr, Protocol}; +use ockam_multiaddr::MultiAddr; use crate::fmt_ok; use crate::util::async_cmd; @@ -25,7 +22,6 @@ use crate::{ util::api::{IdentityOpts, TrustOpts}, }; use crate::{terminal::color_primary, util::duration::duration_parser}; -use ockam_api::cloud::project::models::ProjectModel; use tracing::debug; const LONG_ABOUT: &str = include_str!("./static/ticket/long_about.txt"); @@ -49,18 +45,9 @@ pub struct TicketCommand { #[command(flatten)] trust_opts: TrustOpts, - /// Bypass ticket creation, add this member directly to the Project's Membership Authority, with the given attributes - #[arg(value_name = "IDENTIFIER", long, short, conflicts_with = "expires_in")] - member: Option, - /// The Project name from this option is used to create the enrollment ticket. This takes precedence over `--project` - #[arg( - long, - short, - default_value = "/project/default", - value_name = "ROUTE_TO_PROJECT" - )] - to: MultiAddr, + #[arg(long, short, value_name = "ROUTE_TO_PROJECT")] + to: Option, /// Attributes in `key=value` format to be attached to the member. You can specify this option multiple times for multiple attributes #[arg(short, long = "attribute", value_name = "ATTRIBUTE")] @@ -68,15 +55,11 @@ pub struct TicketCommand { // Note: MAX_TOKEN_DURATION holds the default value. /// Duration for which the enrollment ticket is valid, if you don't specify this, the default is 10 minutes. Examples: 10000ms, 600s, 600, 10m, 1h, 1d. If you don't specify a length sigil, it is assumed to be seconds - #[arg(long = "expires-in", value_name = "DURATION", conflicts_with = "member", value_parser = duration_parser)] + #[arg(long = "expires-in", value_name = "DURATION", value_parser = duration_parser)] expires_in: Option, /// Number of times the ticket can be used to enroll, the default is 1 - #[arg( - long = "usage-count", - value_name = "USAGE_COUNT", - conflicts_with = "member" - )] + #[arg(long = "usage-count", value_name = "USAGE_COUNT")] usage_count: Option, /// Name of the relay that the identity using the ticket will be allowed to create. This name is transformed into attributes to prevent collisions when creating relay names. For example: `--relay foo` is shorthand for `--attribute ockam-relay=foo` @@ -127,84 +110,48 @@ impl TicketCommand { )); } + let project = crate::project_member::get_project(&opts.state, &self.to).await?; + let node = InMemoryNode::start_with_project_name( ctx, &opts.state, - self.trust_opts.project_name.clone(), + Some(project.name().to_string()), ) .await?; - let project_model: Option; - - let authority_node_client = if let Some(p) = get_project(&opts.state, &self.to).await? { - let identity = opts - .state - .get_identity_name_or_default(&self.identity_opts.identity) - .await?; - project_model = Some(p.model().clone()); - node.create_authority_client( - &p.authority_identifier().into_diagnostic()?, - p.authority_multiaddr().into_diagnostic()?, - Some(identity), - None, - ) - .await? - } else { - return Err(miette!("Cannot create a ticket. Please specify a route to your project or to an authority node")); - }; + let identity = opts + .state + .get_identity_name_or_default(&self.identity_opts.identity) + .await?; + + let authority_node_client = node + .create_authority_client(&project, Some(identity)) + .await?; let attributes = self.attributes()?; debug!(attributes = ?attributes, "Attributes passed"); - // If an identity identifier is given add it as a member, otherwise - // request an enrollment token that a future member can use to get a + // Request an enrollment token that a future member can use to get a // credential. - if let Some(id) = &self.member { - authority_node_client - .add_member(ctx, id.clone(), attributes) - .await? - } else { - let token = authority_node_client - .create_token(ctx, attributes, self.expires_in, self.usage_count) - .await?; - - let ticket = EnrollmentTicket::new(token, project_model); - let ticket_serialized = ticket.hex_encoded().into_diagnostic()?; - - opts.terminal.write_line(&fmt_ok!( - "{}: {}", - "Created enrollment ticket. You can use it to enroll another machine using", - color_primary("ockam project enroll") - ))?; - - opts.terminal - .clone() - .stdout() - .machine(ticket_serialized) - .write_line()?; - } + let token = authority_node_client + .create_token(ctx, attributes, self.expires_in, self.usage_count) + .await?; - Ok(()) - } -} + let ticket = EnrollmentTicket::new(token, Some(project.model().clone())); + let ticket_serialized = ticket.hex_encoded().into_diagnostic()?; -/// Get the project authority from the first address protocol. -/// -/// If the first protocol is a `/project`, look up the project's config. -async fn get_project(cli_state: &CliState, input: &MultiAddr) -> Result> { - if let Some(proto) = input.first() { - if proto.code() == proto::Project::CODE { - let project_name = proto.cast::().expect("project protocol"); - match cli_state.projects().get_project_by_name(&project_name).await.ok() { - None => Err(miette!("Unknown project '{}'. Run 'ockam project list' to get a list of available projects.", project_name.to_string()))?, - Some(project) => { - Ok(Some(project)) - } - } - } else { - Ok(None) - } - } else { - Ok(None) + opts.terminal.write_line(&fmt_ok!( + "{}: {}", + "Created enrollment ticket. You can use it to enroll another machine using", + color_primary("ockam project enroll") + ))?; + + opts.terminal + .clone() + .stdout() + .machine(ticket_serialized) + .write_line()?; + + Ok(()) } } diff --git a/implementations/rust/ockam/ockam_command/src/project/util.rs b/implementations/rust/ockam/ockam_command/src/project/util.rs index 0ded58b39fb..6d605753777 100644 --- a/implementations/rust/ockam/ockam_command/src/project/util.rs +++ b/implementations/rust/ockam/ockam_command/src/project/util.rs @@ -81,6 +81,7 @@ pub async fn get_projects_secure_channels_from_config_lookup( &project_access_route, project_identifier, identity_name.clone(), + None, timeout, ) .await?; @@ -223,14 +224,7 @@ async fn check_authority_node_accessible( retry_strategy: Take, spinner_option: Option, ) -> Result { - let authority_node = node - .create_authority_client( - &project.authority_identifier()?, - project.authority_multiaddr()?, - None, - None, - ) - .await?; + let authority_node = node.create_authority_client(&project, None).await?; if let Some(spinner) = spinner_option.as_ref() { spinner.set_message("Establishing secure channel to project authority..."); diff --git a/implementations/rust/ockam/ockam_command/src/project_member/mod.rs b/implementations/rust/ockam/ockam_command/src/project_member/mod.rs index 8e9d58b2e39..dcb7a74f582 100644 --- a/implementations/rust/ockam/ockam_command/src/project_member/mod.rs +++ b/implementations/rust/ockam/ockam_command/src/project_member/mod.rs @@ -6,7 +6,7 @@ use clap::Subcommand; use delete::DeleteCommand; use list::ListCommand; use list_ids::ListIdsCommand; -use miette::{miette, IntoDiagnostic}; +use miette::miette; use ockam_api::authenticator::direct::{ OCKAM_ROLE_ATTRIBUTE_ENROLLER_VALUE, OCKAM_ROLE_ATTRIBUTE_KEY, }; @@ -113,12 +113,7 @@ pub(super) async fn create_authority_client( .await?; Ok(node - .create_authority_client( - &project.authority_identifier().into_diagnostic()?, - project.authority_multiaddr().into_diagnostic()?, - Some(identity), - None, - ) + .create_authority_client(project, Some(identity)) .await?) } diff --git a/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs b/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs index 6f3ede56ea5..91e038b13c8 100644 --- a/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs +++ b/implementations/rust/ockam/ockam_command/src/secure_channel/create.rs @@ -4,6 +4,7 @@ use miette::{miette, IntoDiagnostic, WrapErr}; use serde_json::json; use tokio::{sync::Mutex, try_join}; +use ockam::identity::models::CredentialAndPurposeKey; use ockam::identity::DEFAULT_TIMEOUT; use ockam::{identity::Identifier, route, Context}; use ockam_api::address::extract_address_value; @@ -48,6 +49,14 @@ pub struct CreateCommand { #[arg(value_name = "IDENTIFIER", long, short, display_order = 801)] pub authorized: Option>, + #[arg( + short, + long = "credential", + value_name = "CREDENTIAL", + display_order = 802 + )] + pub credential: Option, + #[command(flatten)] identity_opts: IdentityOpts, } @@ -105,13 +114,27 @@ impl CreateCommand { let to = self.parse_to_route(&opts, ctx, &node).await?; let authorized_identifiers = self.authorized.clone(); + let credential = match &self.credential { + Some(c) => { + let c = hex::decode(c).map_err(|_| miette!("Invalid credential hex"))?; + let c: CredentialAndPurposeKey = + minicbor::decode(&c).map_err(|_| miette!("Invalid credential"))?; + Some(c) + } + None => None, + }; + let create_secure_channel = async { let identity_name = opts .state .get_identity_name_or_default(&self.identity_opts.identity) .await?; - let payload = - CreateSecureChannelRequest::new(&to, authorized_identifiers, Some(identity_name)); + let payload = CreateSecureChannelRequest::new( + &to, + authorized_identifiers, + Some(identity_name), + credential, + ); let request = Request::post("/node/secure_channel").body(payload); let response: CreateSecureChannelResponse = node.ask(ctx, request).await?; *is_finished.lock().await = true; diff --git a/implementations/rust/ockam/ockam_command/src/status.rs b/implementations/rust/ockam/ockam_command/src/status.rs index a32ff8ac010..794d39b3a68 100644 --- a/implementations/rust/ockam/ockam_command/src/status.rs +++ b/implementations/rust/ockam/ockam_command/src/status.rs @@ -177,8 +177,8 @@ impl StatusData { let mut identities = vec![]; for identity in identities_details.into_iter() { let mut identity_status = IdentityWithLinkedNodes { - identifier: identity.identifier(), - name: identity.name(), + identifier: identity.identifier().clone(), + name: identity.name().clone(), is_default: identity.is_default(), enrolled_at: identity .enrolled_at() diff --git a/implementations/rust/ockam/ockam_command/src/util/api.rs b/implementations/rust/ockam/ockam_command/src/util/api.rs index f9202d6f954..502450c570a 100644 --- a/implementations/rust/ockam/ockam_command/src/util/api.rs +++ b/implementations/rust/ockam/ockam_command/src/util/api.rs @@ -157,7 +157,7 @@ pub struct TrustOpts { /// Expect credential manually saved to the storage #[arg(long)] - pub expect_cached_credential: bool, + pub credential_scope: Option, } ////////////// !== validators diff --git a/implementations/rust/ockam/ockam_command/tests/bats/authority.bats b/implementations/rust/ockam/ockam_command/tests/bats/authority.bats index 44ada854f74..4c8d1f241d6 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/authority.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/authority.bats @@ -67,11 +67,10 @@ teardown() { # Create a node for the admin, used as a hack to present the project admin credential to the authority port_admin="$(random_port)" - run_success "$OCKAM" node create admin --tcp-listener-address "127.0.0.1:$port_admin" --identity admin --authority-identity $account_authority_full --expect-cached-credential + run_success "$OCKAM" node create admin --tcp-listener-address "127.0.0.1:$port_admin" --identity admin --authority-identity $account_authority_full - # issue and store project admin credentials for admin - $OCKAM credential issue --as account_authority --for "$admin_identifier" --attribute project="1" --encoding hex >"$OCKAM_HOME/admin.cred" - run_success "$OCKAM" credential store --at admin --issuer "$account_authority_identifier" --credential-path "$OCKAM_HOME/admin.cred" + # issue project admin credentials for admin + admin_cred=$($OCKAM credential issue --as account_authority --for "$admin_identifier" --attribute project="1" --encoding hex) # Start the authority node. We pass a set of pre trusted-identities containing m1' identity identifier trusted="{\"$m1_identifier\": {\"sample_attr\": \"sample_val\"} }" @@ -79,7 +78,7 @@ teardown() { sleep 2 # wait for authority to start TCP listener # Make the admin present its project admin credential to the authority - run_success "$OCKAM" secure-channel create --from admin --to "/node/authority/service/api" --identity admin + run_success "$OCKAM" secure-channel create --from admin --to "/node/authority/service/api" --identity admin --credential $admin_cred cat <"$OCKAM_HOME/project.json" { diff --git a/implementations/rust/ockam/ockam_command/tests/bats/authority_orchestrator.bats b/implementations/rust/ockam/ockam_command/tests/bats/authority_orchestrator.bats index 00d78da3565..cc0ab36e16d 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/authority_orchestrator.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/authority_orchestrator.bats @@ -36,3 +36,32 @@ teardown() { run_success "$OCKAM" project-member delete "$m_identifier" } + +@test "project authority - test api authorization rules" { + # Enroller + run "$OCKAM" identity create e + # Member + run "$OCKAM" identity create m + + run "$OCKAM" identity create t + + e_identifier=$($OCKAM identity show e) + m_identifier=$($OCKAM identity show m) + t_identifier=$($OCKAM identity show t) + + run_success "$OCKAM" project-member add "$e_identifier" --enroller + + run_success "$OCKAM" project-member list-ids + run_success "$OCKAM" project-member list + + # TODO: Should not work after we enable all checks on Authority nodes + # run_failure "$OCKAM" project-member add "$m_identifier" --enroller --identity e + run_success "$OCKAM" project-member add "$m_identifier" --identity e + + run_failure "$OCKAM" project-member list --identity m + run_failure "$OCKAM" project-member list-ids --identity m + run_failure "$OCKAM" project-member add "$t_identifier" --identity m + run_failure "$OCKAM" project-member delete "$m_identifier" --identity m + + run_success "$OCKAM" project-member delete "$m_identifier" --identity e +} diff --git a/implementations/rust/ockam/ockam_command/tests/bats/command-reference.bats b/implementations/rust/ockam/ockam_command/tests/bats/command-reference.bats index 27749aaa113..2b089b65070 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/command-reference.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/command-reference.bats @@ -263,10 +263,10 @@ teardown() { --for "$I1_IDENTIFIER" --attribute city="New York" \ --encoding hex) - run_success "$OCKAM" node create "$n1" --identity "$i1" --authority-identity "$AUTHORITY_IDENTITY" --expect-cached-credential + run_success "$OCKAM" node create "$n1" --identity "$i1" --authority-identity "$AUTHORITY_IDENTITY" --credential-scope "test" run_success "$OCKAM" node create "$n2" --identity "$i2" --authority-identity "$AUTHORITY_IDENTITY" - run_success "$OCKAM" credential store --issuer "$AUTHORITY_IDENTIFIER" --credential "$I1_CREDENTIAL" --at "$n1" + run_success "$OCKAM" credential store --issuer "$AUTHORITY_IDENTIFIER" --credential "$I1_CREDENTIAL" --at "$n1" --scope "test" run_success bash -c "$OCKAM secure-channel create --from $n1 --identity $i1 --to /node/$n2/service/api | $OCKAM message send --timeout 1 hello --from $n1 --to -/service/echo" diff --git a/implementations/rust/ockam/ockam_command/tests/bats/credentials.bats b/implementations/rust/ockam/ockam_command/tests/bats/credentials.bats index c9357b9f987..517b83fe9c3 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/credentials.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/credentials.bats @@ -27,7 +27,7 @@ teardown() { run_success "$OCKAM" credential verify --issuer "$idt1_short" --credential-path "$OCKAM_HOME/credential" assert_output --partial "true" - run_success "$OCKAM" credential store --issuer "$idt1_short" --credential-path "$OCKAM_HOME/credential" + run_success "$OCKAM" credential store --issuer "$idt1_short" --credential-path "$OCKAM_HOME/credential" --scope "test" run_success "$OCKAM" credential list assert_output --partial "{\"application\":\"Smart Factory\",\"city\":\"New York\"" @@ -43,6 +43,6 @@ teardown() { run_success "$OCKAM" credential verify --issuer "$idt1_short" --credential-path "$OCKAM_HOME/bad_credential" assert_output --partial "false" - run_failure "$OCKAM" credential store --issuer "$idt1_short" --credential-path "$OCKAM_HOME/bad_credential" + run_failure "$OCKAM" credential store --issuer "$idt1_short" --credential-path "$OCKAM_HOME/bad_credential" --scope "test" assert_output --partial "Credential is not verified" } diff --git a/implementations/rust/ockam/ockam_command/tests/bats/portals.bats b/implementations/rust/ockam/ockam_command/tests/bats/portals.bats index 6eaaaa36343..3ff09ed0af2 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/portals.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/portals.bats @@ -265,18 +265,18 @@ teardown() { authority_identity=$($OCKAM identity show authority --full --encoding hex) # Create a node for alice that trusts authority as a credential authority - run_success "$OCKAM" node create alice --identity alice --authority-identity $authority_identity --expect-cached-credential + run_success "$OCKAM" node create alice --identity alice --authority-identity $authority_identity --credential-scope "test" # Create a node for bob that trusts authority as a credential authority - run_success "$OCKAM" node create bob --tcp-listener-address "127.0.0.1:$node_port" --identity bob --authority-identity $authority_identity --expect-cached-credential + run_success "$OCKAM" node create bob --tcp-listener-address "127.0.0.1:$node_port" --identity bob --authority-identity $authority_identity --credential-scope "test" # issue and store a short-lived credential for alice alice_credential=$($OCKAM credential issue --as authority --for "$alice_identifier" --ttl 5s --encoding hex) - run_success "$OCKAM" credential store --at alice --issuer "$authority_identifier" --credential $alice_credential + run_success "$OCKAM" credential store --at alice --issuer "$authority_identifier" --credential $alice_credential --scope "test" # issue and store credential for bob bob_credential=$($OCKAM credential issue --as authority --for "$bob_identifier" --encoding hex) - run_success "$OCKAM" credential store --at bob --issuer "$authority_identifier" --credential $bob_credential + run_success "$OCKAM" credential store --at bob --issuer "$authority_identifier" --credential $bob_credential --scope "test" run_success "$OCKAM" tcp-outlet create --at /node/bob --to 127.0.0.1:5000 run_success "$OCKAM" tcp-inlet create --at /node/alice --from "127.0.0.1:$inlet_port" --to /node/bob/secure/api/service/outlet @@ -311,18 +311,18 @@ teardown() { authority_identity=$($OCKAM identity show authority --full --encoding hex) # Create a node for alice that trusts authority as a credential authority - run_success "$OCKAM" node create alice --identity alice --authority-identity $authority_identity --expect-cached-credential + run_success "$OCKAM" node create alice --identity alice --authority-identity $authority_identity --credential-scope "test" # Create a node for bob that trusts authority as a credential authority - run_success "$OCKAM" node create bob --tcp-listener-address "127.0.0.1:$node_port" --identity bob --authority-identity $authority_identity --expect-cached-credential + run_success "$OCKAM" node create bob --tcp-listener-address "127.0.0.1:$node_port" --identity bob --authority-identity $authority_identity --credential-scope "test" # issue and store a short-lived credential for alice alice_credential=$($OCKAM credential issue --as authority --for "$alice_identifier" --encoding hex) - run_success "$OCKAM" credential store --at alice --issuer "$authority_identifier" --credential $alice_credential + run_success "$OCKAM" credential store --at alice --issuer "$authority_identifier" --credential $alice_credential --scope "test" # issue and store credential for bob bob_credential=$($OCKAM credential issue --as authority --for "$bob_identifier" --ttl 5s --encoding hex) - run_success "$OCKAM" credential store --at bob --issuer "$authority_identifier" --credential $bob_credential + run_success "$OCKAM" credential store --at bob --issuer "$authority_identifier" --credential $bob_credential --scope "test" run_success "$OCKAM" tcp-outlet create --at /node/bob --to 127.0.0.1:5000 run_success "$OCKAM" tcp-inlet create --at /node/alice --from "127.0.0.1:$inlet_port" --to /node/bob/secure/api/service/outlet diff --git a/implementations/rust/ockam/ockam_command/tests/bats/trust.bats b/implementations/rust/ockam/ockam_command/tests/bats/trust.bats index 4e6876cd369..a54cad1fbfd 100644 --- a/implementations/rust/ockam/ockam_command/tests/bats/trust.bats +++ b/implementations/rust/ockam/ockam_command/tests/bats/trust.bats @@ -32,9 +32,6 @@ teardown() { run_success "$OCKAM" identity create alice alice_identifier=$($OCKAM identity show alice) - run_success "$OCKAM" identity create bob - bob_identifier=$($OCKAM identity show bob) - run_success "$OCKAM" identity create attacker attacker_identifier=$($OCKAM identity show attacker) attacker_identity=$($OCKAM identity show attacker --full --encoding hex) @@ -45,33 +42,21 @@ teardown() { authority_identity=$($OCKAM identity show authority --full --encoding hex) # Create a node for alice that trusts authority as a credential authority - run_success "$OCKAM" node create alice --tcp-listener-address "127.0.0.1:$port" --identity alice --authority-identity $authority_identity --expect-cached-credential - - # Create a node for bob that trusts authority as a credential authority - run_success "$OCKAM" node create bob --identity bob --authority-identity $authority_identity --expect-cached-credential - - # issue and store credentials for alice - $OCKAM credential issue --as authority --for "$alice_identifier" --attribute city="New York" --encoding hex >"$OCKAM_HOME/alice.cred" - run_success "$OCKAM" credential store --at alice --issuer "$authority_identifier" --credential-path "$OCKAM_HOME/alice.cred" - - # issue and store credential for bob - $OCKAM credential issue --as authority --for "$bob_identifier" --attribute city="New York" --encoding hex >"$OCKAM_HOME/bob.cred" - run_success "$OCKAM" credential store --at bob --issuer "$authority_identifier" --credential-path "$OCKAM_HOME/bob.cred" + run_success "$OCKAM" node create alice --tcp-listener-address "127.0.0.1:$port" --identity alice --authority-identity $authority_identity msg=$(random_str) # Create a node for attacker - run_success "$OCKAM" node create attacker --identity attacker --authority-identity attacker_identity + run_success "$OCKAM" node create attacker --identity attacker --authority-identity $attacker_identity # Fail, attacker won't present any credential - run_failure $OCKAM message send --timeout 2 --from attacker --to "/dnsaddr/127.0.0.1/tcp/$port/secure/api/service/echo" $msg + run_failure $OCKAM message send --timeout 2 --from attacker --identity attacker --to "/dnsaddr/127.0.0.1/tcp/$port/secure/api/service/echo" $msg # Fail, attacker will present an invalid credential (self signed rather than signed by authority) - $OCKAM credential issue --as attacker --for $attacker_identifier --encoding hex >"$OCKAM_HOME/attacker.cred" - run_failure "$OCKAM" credential store --node attacker --issuer "$attacker_identity" --credential-path "$OCKAM_HOME/attacker.cred" + attacker_cred=$($OCKAM credential issue --as attacker --for $attacker_identifier --encoding hex) + run_success "$OCKAM" credential store --at attacker --issuer "$attacker_identifier" --scope "test" --credential $attacker_cred - run_success $OCKAM message send --timeout 2 --from bob --to "/dnsaddr/127.0.0.1/tcp/$port/secure/api/service/echo" $msg - assert_output "$msg" + run_failure $OCKAM message send --timeout 2 --from attacker --identity attacker --to "/dnsaddr/127.0.0.1/tcp/$port/secure/api/service/echo" $msg } @test "trust - online authority; Credential Exchange is performed" { @@ -102,7 +87,7 @@ teardown() { run_success "$OCKAM" node create --identity alice --tcp-listener-address 127.0.0.1:$node_port --authority-identity $authority_identity sleep 1 - run_success "$OCKAM" node create bob_node --identity bob --authority-identity $authority_identity --authority-route $authority_route + run_success "$OCKAM" node create bob_node --identity bob --authority-identity $authority_identity --authority-route $authority_route --credential-scope "test" sleep 1 # send a message to alice using the trust context diff --git a/implementations/rust/ockam/ockam_core/src/api.rs b/implementations/rust/ockam/ockam_core/src/api.rs index fce07edcd56..1faaf677d13 100644 --- a/implementations/rust/ockam/ockam_core/src/api.rs +++ b/implementations/rust/ockam/ockam_core/src/api.rs @@ -566,7 +566,7 @@ impl Request { Request::build(Method::Patch, path) } - fn build>(method: Method, path: P) -> Request { + pub fn build>(method: Method, path: P) -> Request { Request { header: RequestHeader::new(method, path, false), body: None, diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/retriever/cache_retriever.rs b/implementations/rust/ockam/ockam_identity/src/credentials/retriever/cache_retriever.rs index f0ef29fb48a..a787034df86 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/retriever/cache_retriever.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/retriever/cache_retriever.rs @@ -6,6 +6,7 @@ use crate::{ }; use async_trait::async_trait; use ockam_core::compat::boxed::Box; +use ockam_core::compat::string::String; use ockam_core::compat::sync::Arc; use ockam_core::{Address, Result}; use tracing::{debug, error}; @@ -18,6 +19,7 @@ pub const DEFAULT_CREDENTIAL_CLOCK_SKEW_GAP: TimestampInSeconds = TimestampInSec pub struct CachedCredentialRetriever { issuer: Identifier, subject: Identifier, + scope: String, cache: Arc, } @@ -26,11 +28,13 @@ impl CachedCredentialRetriever { pub fn new( issuer: Identifier, subject: Identifier, + scope: String, cache: Arc, ) -> Self { Self { issuer, subject, + scope, cache, } } @@ -39,6 +43,7 @@ impl CachedCredentialRetriever { pub async fn retrieve_impl( issuer: &Identifier, for_identity: &Identifier, + scope: &str, now: TimestampInSeconds, cache: Arc, clock_skew_gap: TimestampInSeconds, @@ -49,7 +54,7 @@ impl CachedCredentialRetriever { ); // check if we have a valid cached credential - if let Some(cached_credential) = cache.get(for_identity, issuer).await? { + if let Some(cached_credential) = cache.get(for_identity, issuer, scope).await? { // add an extra minute to have a bit of leeway for clock skew if cached_credential.get_expires_at()? > now + clock_skew_gap { debug!("Found valid cached credential for: {}", for_identity); @@ -59,7 +64,7 @@ impl CachedCredentialRetriever { "Found expired cached credential for: {}. Deleting...", for_identity ); - let delete_res = cache.delete(for_identity, issuer).await; + let delete_res = cache.delete(for_identity, issuer, scope).await; if let Some(err) = delete_res.err() { error!( @@ -80,13 +85,18 @@ impl CachedCredentialRetriever { /// Creator for [`CachedCredentialRetriever`] pub struct CachedCredentialRetrieverCreator { issuer: Identifier, + scope: String, cache: Arc, } impl CachedCredentialRetrieverCreator { /// Constructor - pub fn new(issuer: Identifier, cache: Arc) -> Self { - Self { issuer, cache } + pub fn new(issuer: Identifier, scope: String, cache: Arc) -> Self { + Self { + issuer, + scope, + cache, + } } } @@ -96,6 +106,7 @@ impl CredentialRetrieverCreator for CachedCredentialRetrieverCreator { Ok(Arc::new(CachedCredentialRetriever::new( self.issuer.clone(), subject.clone(), + self.scope.clone(), self.cache.clone(), ))) } @@ -112,6 +123,7 @@ impl CredentialRetriever for CachedCredentialRetriever { match Self::retrieve_impl( &self.issuer, &self.subject, + &self.scope, now, self.cache.clone(), // We can't refresh the credential, so let's still present it even if it's diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/retriever/remote_retriever/info.rs b/implementations/rust/ockam/ockam_identity/src/credentials/retriever/remote_retriever/info.rs index d375ade74f7..9de802cb716 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/retriever/remote_retriever/info.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/retriever/remote_retriever/info.rs @@ -1,27 +1,105 @@ -use serde::{Deserialize, Serialize}; - -use ockam_core::{Address, Route}; +use core::fmt::Display; +use ockam_core::api::Method; +use ockam_core::compat::string::{String, ToString}; +use ockam_core::Route; use crate::Identifier; +enum CredentialIssuerServiceAddress { + Controller, + AuthorityNode, +} + +impl Display for CredentialIssuerServiceAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + CredentialIssuerServiceAddress::Controller => write!(f, "accounts"), + CredentialIssuerServiceAddress::AuthorityNode => write!(f, "credential_issuer"), + } + } +} + +enum CredentialIssuerApiServiceAddress { + ControllerAccount, + ControllerProject { project_id: String }, + AuthorityNode, +} + +impl Display for CredentialIssuerApiServiceAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + CredentialIssuerApiServiceAddress::ControllerAccount => write!(f, "/v0/account"), + CredentialIssuerApiServiceAddress::ControllerProject { project_id } => { + write!(f, "/v0/project/{}", project_id) + } + CredentialIssuerApiServiceAddress::AuthorityNode => write!(f, "/"), + } + } +} + /// Information necessary to connect to a remote credential retriever -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone)] pub struct RemoteCredentialRetrieverInfo { /// Issuer identity, used to validate retrieved credentials pub issuer: Identifier, /// Route used to establish a secure channel to the remote node pub route: Route, - /// Address of the credentials service on the remote node - pub service_address: Address, + /// Address of the credentials service on the remote node, e.g. "credential_issuer" or "accounts" + pub service_address: String, + /// Request path, e.g. "/" or "/v0/project/$project_id" + pub api_service_address: String, + /// Request method, e.g. Post or Get + pub request_method: Method, } impl RemoteCredentialRetrieverInfo { - /// Create new information for a credential retriever - pub fn new(issuer: Identifier, route: Route, service_address: Address) -> Self { + /// Create info for a project member credential that we get from Project Membership Authority + pub fn create_for_project_member(issuer: Identifier, route: Route) -> Self { + Self::new( + issuer, + route, + CredentialIssuerServiceAddress::AuthorityNode.to_string(), + CredentialIssuerApiServiceAddress::AuthorityNode.to_string(), + Method::Post, + ) + } + + /// Create info for a project admin credential that we get from the Orchestrator + pub fn create_for_project_admin(issuer: Identifier, route: Route, project_id: String) -> Self { + Self::new( + issuer, + route, + CredentialIssuerServiceAddress::Controller.to_string(), + CredentialIssuerApiServiceAddress::ControllerProject { project_id }.to_string(), + Method::Get, + ) + } + + /// Create info for a account admin credential that we get from the Orchestrator + pub fn create_for_account_admin(issuer: Identifier, route: Route) -> Self { + Self::new( + issuer, + route, + CredentialIssuerServiceAddress::Controller.to_string(), + CredentialIssuerApiServiceAddress::ControllerAccount.to_string(), + Method::Get, + ) + } + + /// Constructor + pub fn new( + issuer: Identifier, + route: Route, + service_address: String, + api_service_address: String, + request_method: Method, + ) -> Self { Self { issuer, route, service_address, + api_service_address, + request_method, } } } diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/retriever/remote_retriever/remote_retriever.rs b/implementations/rust/ockam/ockam_identity/src/credentials/retriever/remote_retriever/remote_retriever.rs index ba560174faa..6705037af9b 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/retriever/remote_retriever/remote_retriever.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/retriever/remote_retriever/remote_retriever.rs @@ -2,6 +2,7 @@ use core::cmp::max; use tracing::{debug, error, info, trace, warn}; use ockam_core::api::Request; +use ockam_core::compat::string::String; use ockam_core::compat::sync::{Arc, RwLock}; use ockam_core::compat::time::Duration; use ockam_core::compat::vec::Vec; @@ -76,6 +77,7 @@ pub struct RemoteCredentialRetriever { secure_channels: Arc, pub(super) issuer_info: RemoteCredentialRetrieverInfo, pub(super) subject: Identifier, + scope: String, pub(super) timing_options: RemoteCredentialRetrieverTimingOptions, is_initialized: Arc>, @@ -92,6 +94,7 @@ impl RemoteCredentialRetriever { secure_channels: Arc, issuer_info: RemoteCredentialRetrieverInfo, subject: Identifier, + scope: String, timing_options: RemoteCredentialRetrieverTimingOptions, ) -> Self { debug!( @@ -105,6 +108,7 @@ impl RemoteCredentialRetriever { secure_channels, issuer_info, subject, + scope, timing_options, is_initialized: Arc::new(Mutex::new(false)), last_presented_credential: Arc::new(RwLock::new(None)), @@ -129,6 +133,7 @@ impl RemoteCredentialRetriever { let last_presented_credential = match CachedCredentialRetriever::retrieve_impl( &self.issuer_info.issuer, &self.subject, + &self.scope, now, self.secure_channels .identities @@ -285,7 +290,14 @@ impl RemoteCredentialRetriever { ); let credential = client - .ask(&self.ctx, "credential_issuer", Request::post("/")) + .ask( + &self.ctx, + &self.issuer_info.service_address, + Request::build( + self.issuer_info.request_method, + self.issuer_info.api_service_address.clone(), + ), + ) .await? .success()?; @@ -318,6 +330,7 @@ impl RemoteCredentialRetriever { .put( &self.subject, &self.issuer_info.issuer, + &self.scope, expires_at, credential, ) diff --git a/implementations/rust/ockam/ockam_identity/src/credentials/retriever/remote_retriever/remote_retriever_creator.rs b/implementations/rust/ockam/ockam_identity/src/credentials/retriever/remote_retriever/remote_retriever_creator.rs index 616598b572a..c90c2a7b7ad 100644 --- a/implementations/rust/ockam/ockam_identity/src/credentials/retriever/remote_retriever/remote_retriever_creator.rs +++ b/implementations/rust/ockam/ockam_identity/src/credentials/retriever/remote_retriever/remote_retriever_creator.rs @@ -1,5 +1,6 @@ use ockam_core::compat::boxed::Box; use ockam_core::compat::collections::BTreeMap; +use ockam_core::compat::string::String; use ockam_core::compat::sync::Arc; use ockam_core::{async_trait, Address, AllowAll, DenyAll, Mailboxes, Result}; use ockam_node::compat::asynchronous::RwLock; @@ -18,6 +19,7 @@ pub struct RemoteCredentialRetrieverCreator { transport: Arc, secure_channels: Arc, info: RemoteCredentialRetrieverInfo, + scope: String, timing_options: RemoteCredentialRetrieverTimingOptions, // Should be only one retriever per subject Identifier @@ -31,12 +33,14 @@ impl RemoteCredentialRetrieverCreator { transport: Arc, secure_channels: Arc, info: RemoteCredentialRetrieverInfo, + scope: String, ) -> Self { Self { ctx, transport, secure_channels, info, + scope, timing_options: Default::default(), registry: Default::default(), } @@ -48,6 +52,7 @@ impl RemoteCredentialRetrieverCreator { transport: Arc, secure_channels: Arc, info: RemoteCredentialRetrieverInfo, + scope: String, timing_options: RemoteCredentialRetrieverTimingOptions, ) -> Self { Self { @@ -55,6 +60,7 @@ impl RemoteCredentialRetrieverCreator { transport, secure_channels, info, + scope, timing_options, registry: Default::default(), } @@ -105,6 +111,7 @@ impl CredentialRetrieverCreator for RemoteCredentialRetrieverCreator { self.secure_channels.clone(), self.info.clone(), subject.clone(), + self.scope.clone(), self.timing_options, ); debug!( diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/credential_repository.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/credential_repository.rs index 7c0c2eb115a..1ce97a94b89 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/storage/credential_repository.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/storage/credential_repository.rs @@ -12,6 +12,7 @@ pub trait CredentialRepository: Send + Sync + 'static { &self, subject: &Identifier, issuer: &Identifier, + scope: &str, ) -> Result>; /// Put credential (overwriting) @@ -19,10 +20,11 @@ pub trait CredentialRepository: Send + Sync + 'static { &self, subject: &Identifier, issuer: &Identifier, + scope: &str, expires_at: TimestampInSeconds, credential: CredentialAndPurposeKey, ) -> Result<()>; /// Delete credential - async fn delete(&self, subject: &Identifier, issuer: &Identifier) -> Result<()>; + async fn delete(&self, subject: &Identifier, issuer: &Identifier, scope: &str) -> Result<()>; } diff --git a/implementations/rust/ockam/ockam_identity/src/identities/storage/credential_repository_sql.rs b/implementations/rust/ockam/ockam_identity/src/identities/storage/credential_repository_sql.rs index efd55fbf72f..e1af087c286 100644 --- a/implementations/rust/ockam/ockam_identity/src/identities/storage/credential_repository_sql.rs +++ b/implementations/rust/ockam/ockam_identity/src/identities/storage/credential_repository_sql.rs @@ -37,17 +37,20 @@ impl CredentialSqlxDatabase { impl CredentialSqlxDatabase { /// Return all cached credentials for the given node - pub async fn get_all(&self) -> Result> { - let query = query_as("SELECT credential FROM credential WHERE node_name=?") + pub async fn get_all(&self) -> Result> { + let query = query_as("SELECT credential, scope FROM credential WHERE node_name=?") .bind(self.database.node_name()?.to_sql()); - let cached_credential: Vec = + let cached_credential: Vec = query.fetch_all(&*self.database.pool).await.into_core()?; let res = cached_credential .into_iter() - .map(|c| c.credential()) - .collect::>>()?; + .map(|c| { + let cred = c.credential()?; + Ok((cred, c.scope().to_string())) + }) + .collect::>>()?; Ok(res) } @@ -59,12 +62,14 @@ impl CredentialRepository for CredentialSqlxDatabase { &self, subject: &Identifier, issuer: &Identifier, + scope: &str, ) -> Result> { let query = query_as( - "SELECT credential FROM credential WHERE subject_identifier=$1 AND issuer_identifier=$2 AND node_name=$3" + "SELECT credential FROM credential WHERE subject_identifier=$1 AND issuer_identifier=$2 AND scope=$3 AND node_name=$4" ) .bind(subject.to_sql()) .bind(issuer.to_sql()) + .bind(scope.to_sql()) .bind(self.database.node_name()?.to_sql()); let cached_credential: Option = query .fetch_optional(&*self.database.pool) @@ -77,24 +82,27 @@ impl CredentialRepository for CredentialSqlxDatabase { &self, subject: &Identifier, issuer: &Identifier, + scope: &str, expires_at: TimestampInSeconds, credential: CredentialAndPurposeKey, ) -> Result<()> { let query = query( - "INSERT OR REPLACE INTO credential (subject_identifier, issuer_identifier, credential, expires_at, node_name) VALUES (?, ?, ?, ?, ?)" + "INSERT OR REPLACE INTO credential (subject_identifier, issuer_identifier, scope, credential, expires_at, node_name) VALUES (?, ?, ?, ?, ?, ?)" ) .bind(subject.to_sql()) .bind(issuer.to_sql()) + .bind(scope.to_sql()) .bind(credential.encode_as_cbor_bytes()?.to_sql()) .bind(expires_at.to_sql()) .bind(self.database.node_name()?.to_sql()); query.execute(&*self.database.pool).await.void() } - async fn delete(&self, subject: &Identifier, issuer: &Identifier) -> Result<()> { - let query = query("DELETE FROM credential WHERE subject_identifier=$1 AND issuer_identifier=$2 AND node_name=$3") + async fn delete(&self, subject: &Identifier, issuer: &Identifier, scope: &str) -> Result<()> { + let query = query("DELETE FROM credential WHERE subject_identifier=$1 AND issuer_identifier=$2 AND scope=$3 AND node_name=$4") .bind(subject.to_sql()) .bind(issuer.to_sql()) + .bind(scope.to_sql()) .bind(self.database.node_name()?.to_sql()); query.execute(&*self.database.pool).await.void() } @@ -112,6 +120,21 @@ impl CachedCredentialRow { } } +#[derive(FromRow)] +struct CachedCredentialAndScopeRow { + credential: Vec, + scope: String, +} + +impl CachedCredentialAndScopeRow { + fn credential(&self) -> Result { + CredentialAndPurposeKey::decode_from_cbor_bytes(&self.credential) + } + pub fn scope(&self) -> &str { + &self.scope + } +} + #[cfg(test)] mod tests { use ockam_core::compat::rand::random_string; @@ -125,7 +148,12 @@ mod tests { #[tokio::test] async fn test_cached_credential_repository() -> Result<()> { - let repository = create_repository().await?; + let scope = "test".to_string(); + let repository = + Arc::new(CredentialSqlxDatabase::create_with_node_name(&random_string()).await?); + + let all = repository.get_all().await?; + assert_eq!(all.len(), 0); let identities = identities().await?; @@ -145,11 +173,16 @@ mod tests { .put( &subject, &issuer, + &scope, credential1.get_credential_data()?.expires_at, credential1.clone(), ) .await?; - let credential2 = repository.get(&subject, &issuer).await?; + + let all = repository.get_all().await?; + assert_eq!(all.len(), 1); + + let credential2 = repository.get(&subject, &issuer, &scope).await?; assert_eq!(credential2, Some(credential1)); let attributes2 = AttributesBuilder::with_schema(CredentialSchemaIdentifier(1)) @@ -164,24 +197,20 @@ mod tests { .put( &subject, &issuer, + &scope, credential3.get_credential_data()?.expires_at, credential3.clone(), ) .await?; - let credential4 = repository.get(&subject, &issuer).await?; + let all = repository.get_all().await?; + assert_eq!(all.len(), 1); + let credential4 = repository.get(&subject, &issuer, &scope).await?; assert_eq!(credential4, Some(credential3)); - repository.delete(&subject, &issuer).await?; - let result = repository.get(&subject, &issuer).await?; + repository.delete(&subject, &issuer, &scope).await?; + let result = repository.get(&subject, &issuer, &scope).await?; assert_eq!(result, None); Ok(()) } - - /// HELPERS - async fn create_repository() -> Result> { - Ok(Arc::new( - CredentialSqlxDatabase::create_with_node_name(&random_string()).await?, - )) - } } diff --git a/implementations/rust/ockam/ockam_identity/src/models/credential.rs b/implementations/rust/ockam/ockam_identity/src/models/credential.rs index 17815468e47..cd1465ea332 100644 --- a/implementations/rust/ockam/ockam_identity/src/models/credential.rs +++ b/implementations/rust/ockam/ockam_identity/src/models/credential.rs @@ -69,12 +69,14 @@ impl Display for Attributes { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { let mut attributes = vec![]; for (key, value) in self.map.clone() { - if let (Ok(k), Ok(v)) = ( - String::from_utf8(key.to_vec()), - String::from_utf8(value.to_vec()), - ) { - attributes.push(format!("{k}={v}")) - } + let key = Vec::::from(key); + let value = Vec::::from(value); + let key = + String::from_utf8(key.clone()).unwrap_or(format!("HEX:{}", hex::encode(&key))); + let value = + String::from_utf8(value.clone()).unwrap_or(format!("HEX:{}", hex::encode(&value))); + + attributes.push(format!("{key}={value}")) } f.debug_struct("Attributes") .field("attrs", &attributes.join(",")) diff --git a/implementations/rust/ockam/ockam_identity/tests/credentials_refresh.rs b/implementations/rust/ockam/ockam_identity/tests/credentials_refresh.rs index bd57a7bdbc5..426f3556205 100644 --- a/implementations/rust/ockam/ockam_identity/tests/credentials_refresh.rs +++ b/implementations/rust/ockam/ockam_identity/tests/credentials_refresh.rs @@ -277,11 +277,11 @@ async fn init( ctx.async_try_clone().await?, Arc::new(tcp), client_secure_channels.clone(), - RemoteCredentialRetrieverInfo::new( + RemoteCredentialRetrieverInfo::create_for_project_member( authority.clone(), route!["authority_api"], - "credential_issuer".into(), ), + "test".to_string(), timing_options, )); diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/20240307100000_credential_add_scope.sql b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/20240307100000_credential_add_scope.sql new file mode 100644 index 00000000000..89ac8fa5a93 --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/20240307100000_credential_add_scope.sql @@ -0,0 +1,23 @@ +DROP TABLE credential; + +CREATE TABLE credential +( + subject_identifier TEXT NOT NULL, + issuer_identifier TEXT NOT NULL, + scope TEXT NOT NULL, + credential TEXT NOT NULL, + expires_at INTEGER, + node_name TEXT NOT NULL -- node name to isolate credential that each node has +); + +CREATE UNIQUE INDEX credential_issuer_subject_scope_index ON credential (issuer_identifier, subject_identifier, scope); + +-- Replace index +DROP INDEX identity_attributes_attested_by_index; +CREATE INDEX identity_attributes_identifier_attested_by_node_name_index ON identity_attributes (identifier, attested_by, node_name); + +CREATE INDEX identity_attributes_expires_node_name_index ON identity_attributes (expires, node_name); + +CREATE INDEX identity_identifier_index ON identity_attributes (identifier); + +CREATE INDEX identity_node_name_index ON identity_attributes (node_name); diff --git a/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/20240321100000_add_enrollment_email.sql b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/20240321100000_add_enrollment_email.sql new file mode 100644 index 00000000000..9b58c7a667f --- /dev/null +++ b/implementations/rust/ockam/ockam_node/src/storage/database/migrations/node_migrations/sql/20240321100000_add_enrollment_email.sql @@ -0,0 +1,3 @@ +-- Add email that was used during the enrollment +ALTER TABLE identity_enrollment + ADD email TEXT;