Skip to content

Commit

Permalink
Add invite_new_shamir command
Browse files Browse the repository at this point in the history
  • Loading branch information
vxgmichel committed Oct 30, 2024
1 parent 1df909a commit fdda601
Show file tree
Hide file tree
Showing 18 changed files with 638 additions and 2 deletions.
3 changes: 3 additions & 0 deletions cli/src/commands/invite/greet.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ pub async fn main(args: Args) -> anyhow::Result<()> {
let ctx = step4_device(ctx).await?;
step5_device(ctx).await
}
InviteListItem::ShamirRecovery { .. } => {
todo!();
}
}
}

Expand Down
10 changes: 10 additions & 0 deletions cli/src/commands/invite/list.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,16 @@ pub async fn main(args: Args) -> anyhow::Result<()> {
..
} => (token, status, format!("user (email={claimer_email}")),
InviteListItem::Device { status, token, .. } => (token, status, "device".into()),
InviteListItem::ShamirRecovery {
status,
token,
claimer_user_id,
..
} => (
token,
status,
format!("shamir recovery (user_id={claimer_user_id}"),
),
};

let token = token.hex();
Expand Down
4 changes: 4 additions & 0 deletions libparsec/crates/client/src/invite/claimer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,10 @@ pub async fn claimer_retrieve_info(
time_provider,
),
)),
UserOrDevice::ShamirRecovery {
recipients,
threshold,
} => todo!("Implement UserOrDeviceClaimInitialCtx::Shamir {recipients:?} {threshold}"),
},
bad_rep @ Rep::UnknownStatus { .. } => {
Err(anyhow::anyhow!("Unexpected server response: {:?}", bad_rep).into())
Expand Down
55 changes: 55 additions & 0 deletions libparsec/crates/client/src/invite/greeter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,61 @@ pub async fn new_device_invitation(
}
}

/*
* new_shamir_invitation
*/

#[derive(Debug, thiserror::Error)]
pub enum NewShamirInvitationError {
#[error("Cannot reach the server")]
Offline,
#[error(transparent)]
Internal(#[from] anyhow::Error),
}

impl From<ConnectionError> for NewShamirInvitationError {
fn from(value: ConnectionError) -> Self {
match value {
ConnectionError::NoResponse(_) => Self::Offline,
err => Self::Internal(err.into()),
}
}
}

pub async fn new_shamir_invitation(
cmds: &AuthenticatedCmds,
send_email: bool,
claimer_user_id: UserID,
) -> Result<(InvitationToken, InvitationEmailSentStatus), NewDeviceInvitationError> {
use authenticated_cmds::latest::invite_new_shamir::{
InvitationEmailSentStatus as ApiInvitationEmailSentStatus, Rep, Req,
};

let req = Req {
send_email,
claimer_user_id,
};
let rep = cmds.send(req).await?;

match rep {
Rep::Ok { token, email_sent } => {
let email_sent = match email_sent {
ApiInvitationEmailSentStatus::Success => InvitationEmailSentStatus::Success,
ApiInvitationEmailSentStatus::ServerUnavailable => {
InvitationEmailSentStatus::ServerUnavailable
}
ApiInvitationEmailSentStatus::RecipientRefused => {
InvitationEmailSentStatus::RecipientRefused
}
};
Ok((token, email_sent))
}
rep @ Rep::UnknownStatus { .. } => {
Err(anyhow::anyhow!("Unexpected server response: {:?}", rep).into())
}
}
}

/*
* delete_invitation
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,28 @@
"type": "InvitationStatus"
}
]
},
{
"name": "ShamirRecovery",
"discriminant_value": "SHAMIR_RECOVERY",
"fields": [
{
"name": "token",
"type": "InvitationToken"
},
{
"name": "created_on",
"type": "DateTime"
},
{
"name": "claimer_user_id",
"type": "UserID"
},
{
"name": "status",
"type": "InvitationStatus"
}
]
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
[
{
"major_versions": [
4
],
"req": {
"cmd": "invite_new_shamir",
"fields": [
{
"name": "claimer_user_id",
"type": "UserID"
},
{
"name": "send_email",
"type": "Boolean"
}
]
},
"reps": [
{
"status": "ok",
"fields": [
{
"name": "token",
"type": "InvitationToken"
},
// Field used when the invitation is correctly created but the invitation email cannot be sent
{
"name": "email_sent",
"type": "InvitationEmailSentStatus"
}
]
}
],
"nested_types": [
{
"name": "InvitationEmailSentStatus",
"variants": [
{
// Also returned when `send_email=false`
"name": "Success",
"discriminant_value": "SUCCESS"
},
{
"name": "ServerUnavailable",
"discriminant_value": "SERVER_UNAVAILABLE"
},
{
"name": "RecipientRefused",
"discriminant_value": "RECIPIENT_REFUSED"
}
]
}
]
}
]
31 changes: 31 additions & 0 deletions libparsec/crates/protocol/schema/invited_cmds/invite_info.json5
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,37 @@
"type": "HumanHandle"
}
]
},
{
"name": "ShamirRecovery",
"discriminant_value": "SHAMIR_RECOVERY",
"fields": [
{
"name": "threshold",
"type": "NonZeroInteger"
},
{
"name": "recipients",
"type": "List<ShamirRecoveryRecipient>"
}
]
}
]
},
{
"name": "ShamirRecoveryRecipient",
"fields": [
{
"name": "user_id",
"type": "UserID"
},
{
"name": "human_handle",
"type": "RequiredOption<HumanHandle>"
},
{
"name": "shares",
"type": "NonZeroInteger"
}
]
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Parsec Cloud (https://parsec.cloud) Copyright (c) BUSL-1.1 2016-present Scille SAS

// `allow-unwrap-in-test` don't behave as expected, see:
// https://github.com/rust-lang/rust-clippy/issues/11119
#![allow(clippy::unwrap_used)]

use super::authenticated_cmds;

use libparsec_tests_lite::{hex, p_assert_eq};
use libparsec_types::InvitationToken;
use libparsec_types::UserID;

// Request

pub fn req() {
// Generated from Parsec 3.1.1-a.0+dev
// Content:
// cmd: 'invite_new_shamir'
// claimer_user_id: ext(2, 0x109b68ba5cdf428ea0017fc6bcc04d4a)
// send_email: True
let raw: &[u8] = hex!(
"83a3636d64b1696e766974655f6e65775f7368616d6972af636c61696d65725f757365"
"725f6964d802109b68ba5cdf428ea0017fc6bcc04d4aaa73656e645f656d61696cc3"
)
.as_ref();

let req = authenticated_cmds::invite_new_shamir::Req {
send_email: true,
claimer_user_id: UserID::from_hex("109b68ba5cdf428ea0017fc6bcc04d4a").unwrap(),
};
println!("***expected: {:?}", req.dump().unwrap());

let expected = authenticated_cmds::AnyCmdReq::InviteNewShamir(req);
let data = authenticated_cmds::AnyCmdReq::load(raw).unwrap();

p_assert_eq!(data, expected);

// Also test serialization round trip
let authenticated_cmds::AnyCmdReq::InviteNewShamir(data2) = data else {
unreachable!()
};
let raw2 = data2.dump().unwrap();

let data2 = authenticated_cmds::AnyCmdReq::load(&raw2).unwrap();

p_assert_eq!(data2, expected);
}

// Responses

pub fn rep_ok() {
// Generated from Rust implementation (Parsec v3.0.0-b.6+dev 2024-03-29)
// Content:
// status: "ok"
// token: ext(2, hex!("d864b93ded264aae9ae583fd3d40c45a"))
//
// Note that raw data does not contain "email_sent".
// This was valid behavior in api v2 but is no longer valid from v3 onwards.
// The corresponding expected values used here are therefore not important
// since loading raw data should fail.
//
let raw = hex!("82a6737461747573a26f6ba5746f6b656ec410d864b93ded264aae9ae583fd3d40c45a");
let err = authenticated_cmds::invite_new_shamir::Rep::load(&raw).unwrap_err();
p_assert_eq!(err.to_string(), "missing field `email_sent`");

// Generated from Python implementation (Parsec v3.0.0-b.6+dev 2024-03-29)
// Content:
// email_sent: "SUCCESS"
// status: "ok"
// token: ext(2, hex!("d864b93ded264aae9ae583fd3d40c45a"))
let raw = hex!(
"83aa656d61696c5f73656e74a753554343455353a6737461747573a26f6ba5746f6b656ec4"
"10d864b93ded264aae9ae583fd3d40c45a"
);
let expected = authenticated_cmds::invite_new_shamir::Rep::Ok {
token: InvitationToken::from_hex("d864b93ded264aae9ae583fd3d40c45a").unwrap(),
email_sent: authenticated_cmds::invite_new_shamir::InvitationEmailSentStatus::Success,
};

rep_helper(&raw, expected);
}

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

p_assert_eq!(data, expected);

// Also test serialization round trip
let raw2 = data.dump().unwrap();

let data2 = authenticated_cmds::invite_new_shamir::Rep::load(&raw2).unwrap();

p_assert_eq!(data2, expected);
}
8 changes: 8 additions & 0 deletions libparsec/src/invite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -855,6 +855,14 @@ pub async fn client_list_invitations(
token,
}
}
libparsec_client::InviteListItem::ShamirRecovery {
created_on,
status,
token,
claimer_user_id,
} => {
todo!("{created_on} {status:?} {token} {claimer_user_id:?}")
}
})
.collect();

Expand Down
1 change: 1 addition & 0 deletions server/parsec/_parsec_pyi/enumerate.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ class InvitationStatus:
class InvitationType:
DEVICE: InvitationType
USER: InvitationType
SHAMIR_RECOVERY: InvitationType
VALUES: tuple[InvitationType, ...]

@classmethod
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ from . import (
invite_greeter_step,
invite_list,
invite_new_device,
invite_new_shamir,
invite_new_user,
ping,
pki_enrollment_accept,
Expand Down Expand Up @@ -56,6 +57,7 @@ class AnyCmdReq:
| invite_greeter_step.Req
| invite_list.Req
| invite_new_device.Req
| invite_new_shamir.Req
| invite_new_user.Req
| ping.Req
| pki_enrollment_accept.Req
Expand Down Expand Up @@ -92,6 +94,7 @@ __all__ = [
"invite_greeter_step",
"invite_list",
"invite_new_device",
"invite_new_shamir",
"invite_new_user",
"ping",
"pki_enrollment_accept",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from __future__ import annotations

from parsec._parsec import DateTime, InvitationStatus, InvitationToken
from parsec._parsec import DateTime, InvitationStatus, InvitationToken, UserID

class InviteListItem:
pass
Expand Down Expand Up @@ -37,6 +37,23 @@ class InviteListItemDevice(InviteListItem):
@property
def token(self) -> InvitationToken: ...

class InviteListItemShamirRecovery(InviteListItem):
def __init__(
self,
token: InvitationToken,
created_on: DateTime,
claimer_user_id: UserID,
status: InvitationStatus,
) -> None: ...
@property
def claimer_user_id(self) -> UserID: ...
@property
def created_on(self) -> DateTime: ...
@property
def status(self) -> InvitationStatus: ...
@property
def token(self) -> InvitationToken: ...

class Req:
def __init__(
self,
Expand Down
Loading

0 comments on commit fdda601

Please sign in to comment.