Skip to content

Commit

Permalink
Add user not found status to invite_new_shamir_recovery
Browse files Browse the repository at this point in the history
  • Loading branch information
vxgmichel committed Oct 31, 2024
1 parent 171f6c6 commit 2b2654b
Show file tree
Hide file tree
Showing 10 changed files with 144 additions and 2 deletions.
3 changes: 3 additions & 0 deletions bindings/generator/api/invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,9 @@ class Offline:
class NotAllowed:
pass

class UserNotFound:
pass

class Internal:
pass

Expand Down
4 changes: 4 additions & 0 deletions cli/src/commands/invite/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ mod claim;
mod device;
mod greet;
mod list;
mod shared_recovery;
mod user;

#[derive(clap::Subcommand)]
Expand All @@ -19,6 +20,8 @@ pub enum Group {
User(user::Args),
/// Create device invitation
Device(device::Args),
/// Create shared recovery invitation
SharedRecovery(shared_recovery::Args),
}

pub async fn dispatch_command(command: Group) -> anyhow::Result<()> {
Expand All @@ -29,5 +32,6 @@ pub async fn dispatch_command(command: Group) -> anyhow::Result<()> {
Group::List(args) => list::main(args).await,
Group::User(args) => user::main(args).await,
Group::Device(args) => device::main(args).await,
Group::SharedRecovery(args) => shared_recovery::main(args).await,
}
}
90 changes: 90 additions & 0 deletions cli/src/commands/invite/shared_recovery.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

use libparsec::{InvitationEmailSentStatus, InvitationType, ParsecInvitationAddr};

use crate::utils::*;

crate::clap_parser_with_shared_opts_builder!(
#[with = config_dir, device, password_stdin]
pub struct Args {
/// Claimer email (i.e.: The invitee)
#[arg(short, long)]
email: String,
/// Send email to the invitee
#[arg(short, long, default_value_t)]
send_email: bool,
}
);

pub async fn main(args: Args) -> anyhow::Result<()> {
let Args {
email,
send_email,
device,
config_dir,
password_stdin,
} = args;
log::trace!(
"Inviting an user to perform a shared recovery (confdir={}, device={})",
config_dir.display(),
device.as_deref().unwrap_or("N/A")
);

let client = load_client(&config_dir, device, password_stdin).await?;

let mut handle = start_spinner("Poll server for new certificates".into());
client.poll_server_for_new_certificates().await?;
handle.stop();

let users = client.list_users(true, None, None).await?;
let user_info = users
.iter()
.find(|u| u.human_handle.email() == email)
.ok_or_else(|| anyhow::anyhow!("User with email {} not found", email))?;

let mut handle = start_spinner("Creating a shared recovery invitation".into());
let (url, email_sent_status) = match client
.new_shamir_recovery_invitation(user_info.id, send_email)
.await
{
Ok((token, email_sent_status)) => (
ParsecInvitationAddr::new(
client.organization_addr().clone(),
client.organization_id().clone(),
InvitationType::ShamirRecovery,
token,
)
.to_url(),
email_sent_status,
),
Err(e) => {
return Err(anyhow::anyhow!(
"Server refused to create shared recovery invitation: {e}"
));
}
};

handle.stop_with_message(format!("Invitation URL: {YELLOW}{url}{RESET}"));

if send_email {
match email_sent_status {
InvitationEmailSentStatus::Success => {
println!("Invitation email sent to {}", email);
}
InvitationEmailSentStatus::RecipientRefused => {
println!(
"Invitation email not sent to {} because the recipient was refused",
email
);
}
InvitationEmailSentStatus::ServerUnavailable => {
println!(
"Invitation email not sent to {} because the server is unavailable",
email
);
}
}
}

Ok(())
}
3 changes: 3 additions & 0 deletions libparsec/crates/client/src/invite/greeter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ pub enum NewShamirRecoveryInvitationError {
Offline,
#[error("Not part of the user's current recipients")]
NotAllowed,
#[error("Provided user not found")]
UserNotFound,
#[error(transparent)]
Internal(#[from] anyhow::Error),
}
Expand Down Expand Up @@ -183,6 +185,7 @@ pub async fn new_shamir_recovery_invitation(
Ok((token, email_sent))
}
Rep::AuthorNotAllowed => Err(NewShamirRecoveryInvitationError::NotAllowed),
Rep::UserNotFound => Err(NewShamirRecoveryInvitationError::UserNotFound),
rep @ Rep::UnknownStatus { .. } => {
Err(anyhow::anyhow!("Unexpected server response: {:?}", rep).into())
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@
},
{
"status": "author_not_allowed"
},
{
"status": "user_not_found"
}
],
"nested_types": [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,18 @@ pub fn rep_author_not_allowed() {
rep_helper(raw, expected);
}

pub fn rep_user_not_found() {
// Generated from Parsec 3.1.1-a.0+dev
// Content:
// status: 'user_not_found'
let raw: &[u8] = hex!("81a6737461747573ae757365725f6e6f745f666f756e64").as_ref();

let expected = authenticated_cmds::invite_new_shamir_recovery::Rep::UserNotFound;
println!("***expected: {:?}", expected.dump().unwrap());

rep_helper(raw, expected);
}

fn rep_helper(raw: &[u8], expected: authenticated_cmds::invite_new_shamir_recovery::Rep) {
let data = authenticated_cmds::invite_new_shamir_recovery::Rep::load(raw).unwrap();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,8 @@ class RepAuthorNotAllowed(Rep):
def __init__(
self,
) -> None: ...

class RepUserNotFound(Rep):
def __init__(
self,
) -> None: ...
3 changes: 3 additions & 0 deletions server/parsec/components/invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@ class InviteNewForShamirBadOutcome(BadOutcomeEnum):
AUTHOR_NOT_FOUND = auto()
AUTHOR_REVOKED = auto()
AUTHOR_NOT_ALLOWED = auto()
USER_NOT_FOUND = auto()


class InviteCancelBadOutcome(BadOutcomeEnum):
Expand Down Expand Up @@ -840,6 +841,8 @@ async def api_invite_new_shamir_recovery(
email_sent = authenticated_cmds.latest.invite_new_shamir_recovery.InvitationEmailSentStatus.RECIPIENT_REFUSED
case InviteNewForShamirBadOutcome.AUTHOR_NOT_ALLOWED:
return authenticated_cmds.latest.invite_new_shamir_recovery.RepAuthorNotAllowed()
case InviteNewForShamirBadOutcome.USER_NOT_FOUND:
return authenticated_cmds.latest.invite_new_shamir_recovery.RepUserNotFound()
case InviteNewForShamirBadOutcome.ORGANIZATION_NOT_FOUND:
client_ctx.organization_not_found_abort()
case InviteNewForShamirBadOutcome.ORGANIZATION_EXPIRED:
Expand Down
10 changes: 8 additions & 2 deletions server/parsec/components/memory/invite.py
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,12 @@ async def new_for_shamir_recovery(
if author_user.is_revoked:
return InviteNewForShamirBadOutcome.AUTHOR_REVOKED

# Check that the claimer exists
claimer = org.users.get(claimer_user_id)
if claimer is None:
return InviteNewForShamirBadOutcome.USER_NOT_FOUND
claimer_human_handle = claimer.cooked.human_handle

# Check that a shamir setup exists
shamir_setup = org.shamir_setup.get(claimer_user_id)
if shamir_setup is None:
Expand Down Expand Up @@ -309,7 +315,7 @@ async def new_for_shamir_recovery(
type=InvitationType.SHAMIR_RECOVERY,
created_by_user_id=author_user_id,
created_by_device_id=author,
claimer_email=None,
claimer_email=claimer_human_handle.email,
created_on=now,
claimer_user_id=claimer_user_id,
)
Expand All @@ -326,7 +332,7 @@ async def new_for_shamir_recovery(
if send_email:
send_email_outcome = await self._send_shamir_recovery_invitation_email(
organization_id=organization_id,
email=author_user.cooked.human_handle.email,
email=claimer_human_handle.email,
token=token,
greeter_human_handle=author_user.cooked.human_handle,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
InvitationToken,
ShamirRecoveryBriefCertificate,
ShamirRecoveryShareCertificate,
UserID,
authenticated_cmds,
)
from parsec.components.invite import (
Expand Down Expand Up @@ -137,6 +138,18 @@ async def test_authenticated_invite_new_shamir_recovery_author_not_allowed(
assert isinstance(rep, authenticated_cmds.v4.invite_new_shamir_recovery.RepAuthorNotAllowed)


@pytest.mark.parametrize("send_email", (False, True))
async def test_authenticated_invite_new_shamir_recovery_user_not_found(
send_email: bool, coolorg: CoolorgRpcClients, backend: Backend, alice_shamir: None
) -> None:
# Shamir setup exists but author is not part of the recipients
rep = await coolorg.alice.invite_new_shamir_recovery(
send_email=send_email,
claimer_user_id=UserID.new(),
)
assert isinstance(rep, authenticated_cmds.v4.invite_new_shamir_recovery.RepUserNotFound)


async def test_authenticated_invite_new_shamir_recovery_ok_already_exist(
coolorg: CoolorgRpcClients, backend: Backend, alice_shamir: None
) -> None:
Expand Down

0 comments on commit 2b2654b

Please sign in to comment.