Skip to content

Commit

Permalink
refactor(rust): project-member commands, and adds the show command
Browse files Browse the repository at this point in the history
  • Loading branch information
adrianbenavides committed Jun 27, 2024
1 parent 0f368bf commit f6ab35b
Show file tree
Hide file tree
Showing 25 changed files with 654 additions and 219 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ pub trait Members {
attributes: BTreeMap<String, String>,
) -> miette::Result<()>;

async fn delete_member(&self, ctx: &Context, identifier: Identifier) -> miette::Result<()>;
async fn show_member(
&self,
ctx: &Context,
identifier: Identifier,
) -> miette::Result<AttributesEntry>;

async fn delete_all_members(&self, ctx: &Context, except: Identifier) -> miette::Result<()>;
async fn delete_member(&self, ctx: &Context, identifier: Identifier) -> miette::Result<()>;

async fn list_member_ids(&self, ctx: &Context) -> miette::Result<Vec<Identifier>>;

Expand All @@ -49,6 +53,20 @@ impl Members for AuthorityNodeClient {
.into_diagnostic()
}

async fn show_member(
&self,
ctx: &Context,
identifier: Identifier,
) -> miette::Result<AttributesEntry> {
let req = Request::get(format!("/{identifier}"));
self.get_secure_client()
.ask(ctx, DefaultAddress::DIRECT_AUTHENTICATOR, req)
.await
.into_diagnostic()?
.success()
.into_diagnostic()
}

async fn delete_member(&self, ctx: &Context, identifier: Identifier) -> miette::Result<()> {
let req = Request::delete(format!("/{identifier}"));
self.get_secure_client()
Expand All @@ -59,18 +77,6 @@ impl Members for AuthorityNodeClient {
.into_diagnostic()
}

async fn delete_all_members(&self, ctx: &Context, except: Identifier) -> miette::Result<()> {
let member_ids = self.list_member_ids(ctx).await?;
for id in member_ids {
if id != except {
if let Err(e) = self.delete_member(ctx, id.clone()).await {
warn!("Failed to delete member {}: {}", id, e);
}
}
}
Ok(())
}

async fn list_member_ids(&self, ctx: &Context) -> miette::Result<Vec<Identifier>> {
let req = Request::get("/member_ids");
self.get_secure_client()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,47 @@ impl DirectAuthenticator {
Ok(Either::Left(()))
}

#[instrument(skip_all, fields(enroller = %enroller))]
pub async fn show_member(
&self,
enroller: &Identifier,
identifier: &Identifier,
) -> Result<DirectAuthenticatorResult<AttributesEntry>> {
let check = EnrollerAccessControlChecks::check_identifier(
self.members.clone(),
self.identities_attributes.clone(),
enroller,
&self.account_authority,
)
.await?;

if !check.is_enroller {
warn!("Non-enroller {} is trying to retrieve a member", enroller);
return Ok(Either::Right(DirectAuthenticatorError(
"Non-enroller is trying to retrieve a member".to_string(),
)));
}

match self.members.get_member(identifier).await? {
Some(member) => {
let entry = AttributesEntry::new(
member.attributes().clone(),
member.added_at(),
None,
Some(member.added_by().clone()),
);
Ok(Either::Left(entry))
}
None => {
warn!("Member {} not found", identifier);
Ok(Either::Right(DirectAuthenticatorError(format!(
"Member {} not found",
identifier
))))
}
}
}

#[instrument(skip_all, fields(enroller = %enroller))]
pub async fn list_members(
&self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ impl DirectAuthenticatorWorker {

#[ockam_core::worker]
impl Worker for DirectAuthenticatorWorker {
type Context = Context;
type Message = Vec<u8>;
type Context = Context;

async fn handle_message(&mut self, c: &mut Context, m: Routed<Self::Message>) -> Result<()> {
let secure_channel_info = match IdentitySecureChannelLocalInfo::find_info(m.local_message())
Expand Down Expand Up @@ -97,6 +97,15 @@ impl Worker for DirectAuthenticatorWorker {
Either::Right(error) => Response::forbidden(&req, &error.0).to_vec()?,
}
}
(Some(Method::Get), [id]) | (Some(Method::Get), ["members", id]) => {
let identifier = Identifier::try_from(id.to_string())?;
let res = self.authenticator.show_member(&from, &identifier).await?;

match res {
Either::Left(body) => Response::ok().with_headers(&req).body(body).to_vec()?,
Either::Right(error) => Response::forbidden(&req, &error.0).to_vec()?,
}
}
(Some(Method::Delete), [id]) | (Some(Method::Delete), ["members", id]) => {
let identifier = Identifier::try_from(id.to_string())?;
let res = self.authenticator.delete_member(&from, &identifier).await?;
Expand Down
4 changes: 2 additions & 2 deletions implementations/rust/ockam/ockam_command/src/lease/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,14 @@ async fn create_project_client(
let node = InMemoryNode::start_with_project_name_and_identity(
ctx,
&opts.state,
identity_opts.identity.clone(),
identity_opts.identity_name.clone(),
trust_opts.project_name.clone(),
)
.await?;

let identity = opts
.state
.get_identity_name_or_default(&identity_opts.identity)
.get_identity_name_or_default(&identity_opts.identity_name)
.await?;
let project = opts
.state
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ impl Command for SendCommand {
} else {
let identity_name = opts
.state
.get_identity_name_or_default(&self.identity_opts.identity)
.get_identity_name_or_default(&self.identity_opts.identity_name)
.await?;

info!("starting an in memory node to send a message");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ impl Command for EnrollCommand {

let identity = opts
.state
.get_named_identity_or_default(&self.identity_opts.identity)
.get_named_identity_or_default(&self.identity_opts.identity_name)
.await?;
let project = self.store_project(&opts).await?;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ impl Command for TicketCommand {

let identity = opts
.state
.get_identity_name_or_default(&self.identity_opts.identity)
.get_identity_name_or_default(&self.identity_opts.identity_name)
.await?;

let authority_node_client = node
Expand Down
112 changes: 78 additions & 34 deletions implementations/rust/ockam/ockam_command/src/project_member/add.rs
Original file line number Diff line number Diff line change
@@ -1,26 +1,30 @@
use async_trait::async_trait;
use clap::Args;
use colorful::Colorful;
use serde::Serialize;
use std::collections::BTreeMap;
use std::fmt::Display;

use ockam::identity::Identifier;
use ockam::Context;
use ockam_api::authenticator::direct::Members;
use ockam_api::fmt_ok;
use ockam_api::nodes::InMemoryNode;
use ockam_api::colors::color_primary;
use ockam_api::{fmt_log, fmt_ok};
use ockam_multiaddr::MultiAddr;

use crate::project_member::{create_authority_client, create_member_attributes, get_project};
use crate::project_member::{authority_client, create_member_attributes};
use crate::shared_args::{IdentityOpts, RetryOpts};
use crate::{docs, Command, CommandGlobalOpts, Error};

const LONG_ABOUT: &str = include_str!("./static/add/long_about.txt");
const AFTER_LONG_HELP: &str = include_str!("./static/add/after_long_help.txt");

/// This attribute in credential allows member to create a relay on the Project node, the name of the relay should be
/// equal to the value of that attribute. If the value is `*` then any name is allowed
/// This credential attribute allows a member to establish a relay on the Project node.
/// The relay's name should match this attribute's value.
/// If the attribute's value is "*", it implies that any name is acceptable for the relay.
pub const OCKAM_RELAY_ATTRIBUTE: &str = "ockam-relay";

/// Add members to a Project, as an authorized enroller directly
/// Add or update members of a Project, directly as an authorized enroller
#[derive(Clone, Debug, Args)]
#[command(
long_about = docs::about(LONG_ABOUT),
Expand All @@ -30,22 +34,29 @@ pub struct AddCommand {
#[command(flatten)]
identity_opts: IdentityOpts,

/// Which project add member to
/// The route to the Project to which a member should be added
#[arg(long, short, value_name = "ROUTE_TO_PROJECT")]
to: Option<MultiAddr>,

/// The Identifier of the member to add
#[arg(value_name = "IDENTIFIER")]
member: Identifier,

/// Attributes in `key=value` format to be attached to the member. You can specify this option multiple times for multiple attributes
/// 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")]
attributes: Vec<String>,

/// Name of the relay that the identity 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`
#[arg(long = "relay", value_name = "ENROLLEE_ALLOWED_RELAY_NAME")]
/// Name of the relay that the identity will be allowed to create.
/// This name is transformed into an attribute, and it's used to prevent collisions with other relays names.
/// E.g. `--relay foo` is a shorthand for `--attribute ockam-relay=foo`
#[arg(long = "relay", value_name = "ALLOWED_RELAY_NAME")]
allowed_relay_name: Option<String>,

/// Add the enroller role. If you specify it, this flag is transformed into the attributes `--attribute ockam-role=enroller`. This role allows the Identity using the ticket to enroll other Identities into the Project, typically something that only admins can do
/// Set the enroller role for the member.
/// When this flag is set, it is transformed into the attribute `ockam-role=enroller`.
/// This role grants the Identity holding the ticket the ability to enroll other Identities
/// into the Project, which is a privilege usually reserved for administrators.
#[arg(long = "enroller")]
enroller: bool,

Expand All @@ -62,36 +73,69 @@ impl Command for AddCommand {
}

async fn async_run(self, ctx: &Context, opts: CommandGlobalOpts) -> crate::Result<()> {
let project = get_project(&opts.state, &self.to).await?;
let (authority_node_client, project_name) =
authority_client(ctx, &opts, &self.identity_opts, &self.to).await?;

let node = InMemoryNode::start_with_project_name(
ctx,
&opts.state,
Some(project.name().to_string()),
)
.await?;

let authority_node_client =
create_authority_client(&node, &opts.state, &self.identity_opts, &project).await?;
let attributes =
create_member_attributes(&self.attributes, &self.allowed_relay_name, self.enroller)?;

authority_node_client
.add_member(
ctx,
self.member.clone(),
create_member_attributes(
&self.attributes,
&self.allowed_relay_name,
self.enroller,
)?,
)
.add_member(ctx, self.member.clone(), attributes.clone())
.await
.map_err(Error::Retry)?;

opts.terminal.stdout().plain(fmt_ok!(
"Identifier {} is now a Project member. It can get a credential and access Project resources, like portals of other members",
self.member
));
let output = AddMemberOutput {
project: project_name,
identifier: self.member.clone(),
attributes,
};

opts.terminal
.stdout()
.plain(output.to_string())
.json_obj(&output)?
.write_line()?;

Ok(())
}
}

#[derive(Serialize)]
struct AddMemberOutput {
project: String,
identifier: Identifier,
attributes: BTreeMap<String, String>,
}

impl Display for AddMemberOutput {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
writeln!(
f,
"{}",
fmt_ok!(
"Identifier {} is now a member of the Project {}",
color_primary(self.identifier.to_string()),
color_primary(&self.project)
)
)?;
if !self.attributes.is_empty() {
let attributes = self
.attributes
.iter()
.map(|(k, v)| format!("{}={}", k, v))
.collect::<Vec<_>>()
.join(", ");
writeln!(
f,
"{}",
fmt_log!("With attributes: {}", color_primary(attributes))
)?;
}
writeln!(
f,
"{}",
fmt_log!("It can get a credential and access Project resources, like portals of other members")
)?;
Ok(())
}
}
Loading

0 comments on commit f6ab35b

Please sign in to comment.